iOS: gate MetalFX upscaler out of simulator builds

MetalFX.framework is not part of the iphonesimulator SDK, so building the
iOS app for the simulator failed at compile time on every MetalFX reference
(`<MetalFX/MetalFX.h>`, `MTLFXSpatialScalerDescriptor`, the cached scaler
members) even though the upscaler is correctly runtime-gated on real
hardware.

Introduce a compile-time switch that follows the SDK target:

- GSDeviceMTL.h defines PCSX2_HAS_METALFX (1 on device, 0 on sim) and
  wraps the MetalFX include, the m_mfx_spatial cache members, and the
  scaler function declarations behind it. The sim build gets a stub
  EnsureMetalFXSpatial that returns false, and DoMetalFXSpatial short-
  circuits the same way; m_features.metalfx_spatial keeps its default
  false so the UI reports the upscaler as unavailable.
- ARMSX2Bridge.mm gates its MetalFX import the same way (ARMSX2_HAS_METALFX)
  and isMetalFXSupported returns NO on sim without touching the descriptor.
- pcsx2/CMakeLists.txt only emits -weak_framework MetalFX for device or
  non-iOS builds; on sim there is nothing to link against.

Adds generate-ios-sim-xcode.sh, a simulator counterpart to the existing
generate-ios-xcode.sh, so the CMake-generated Xcode project can target
iphonesimulator (ARMSX2_REAL_DEVICE=OFF) for simulator debug and test
workflows. Device IPA builds keep using the existing scripts unchanged.
This commit is contained in:
J1coding
2026-07-24 12:15:28 +02:00
committed by Jeen
parent 8e23439b26
commit 2e50c8bfbc
5 changed files with 127 additions and 34 deletions
+6 -2
View File
@@ -1485,8 +1485,12 @@ if (APPLE)
${COREMEDIA_LIBRARY}
)
endif()
# Weak-link MetalFX so older OS versions still load; each use is @available-guarded.
target_link_options(PCSX2_FLAGS INTERFACE "SHELL:-weak_framework MetalFX")
# Weak-link MetalFX so older OS versions still load. Skip on the iOS simulator:
# MetalFX.framework isn't in the iphonesimulator SDK (see PCSX2_HAS_METALFX in
# GSDeviceMTL.h), so linking it there fails with "framework not found".
if(ARMSX2_REAL_DEVICE OR NOT CMAKE_SYSTEM_NAME STREQUAL "iOS")
target_link_options(PCSX2_FLAGS INTERFACE "SHELL:-weak_framework MetalFX")
endif()
endif()
set_property(GLOBAL PROPERTY PCSX2_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
+17 -5
View File
@@ -27,10 +27,15 @@ using GSMTLView = UIView;
#include <AppKit/AppKit.h>
using GSMTLView = NSView;
#endif
// MetalFX spatial upscaler ships on macOS 13+ and iOS 16+. It is weak-linked
// (see CMakeLists.txt) so the binary still loads on older OS revisions, and
// every call site is guarded by @available plus a runtime supportsDevice: probe.
#include <MetalFX/MetalFX.h>
// MetalFX upscaler: macOS 13+ / iOS 16+, weak-linked on device. The simulator
// SDK has no MetalFX headers, so PCSX2_HAS_METALFX is 0 there and every
// MetalFX reference is compiled out; m_features.metalfx_spatial stays false.
#if TARGET_OS_SIMULATOR
#define PCSX2_HAS_METALFX 0
#else
#define PCSX2_HAS_METALFX 1
#include <MetalFX/MetalFX.h>
#endif
#include <Metal/Metal.h>
#include <QuartzCore/QuartzCore.h>
#include <atomic>
@@ -265,10 +270,12 @@ public:
// MetalFX spatial upscaler. Creating the scaler is expensive, so it's cached and
// only rebuilt when the input/output size or format changes (the cache key below).
// Available macOS 13+ / iOS 16+; weak-linked so this compiles on all targets.
// macOS 13+ / iOS 16+ device, weak-linked. Compiled out on the simulator.
#if PCSX2_HAS_METALFX
API_AVAILABLE(macos(13.0), ios(16.0)) MRCOwned<id<MTLFXSpatialScaler>> m_mfx_spatial;
int m_mfx_in_w = 0, m_mfx_in_h = 0, m_mfx_out_w = 0, m_mfx_out_h = 0;
MTLPixelFormat m_mfx_in_fmt = MTLPixelFormatInvalid, m_mfx_out_fmt = MTLPixelFormatInvalid;
#endif
std::vector<MRCOwned<id<MTLRenderPipelineState>>> m_convert_pipeline;
MRCOwned<id<MTLRenderPipelineState>> m_present_pipeline[static_cast<int>(PresentShader::Count)];
MRCOwned<id<MTLRenderPipelineState>> m_merge_pipeline[4];
@@ -416,7 +423,12 @@ public:
bool DoCAS(GSTexture* sTex, GSTexture* dTex, bool sharpen_only, const std::array<u32, NUM_CAS_CONSTANTS>& constants) override;
/// (Re)builds m_mfx_spatial when the src/dst size or format changes. Returns false on failure.
/// On simulator builds (PCSX2_HAS_METALFX=0) this is a no-op stub returning false.
#if PCSX2_HAS_METALFX
API_AVAILABLE(macos(13.0), ios(16.0)) bool EnsureMetalFXSpatial(GSTexture* sTex, GSTexture* dTex);
#else
bool EnsureMetalFXSpatial(GSTexture* sTex, GSTexture* dTex);
#endif
bool DoMetalFXSpatial(GSTexture* sTex, GSTexture* dTex) override;
MRCOwned<id<MTLFunction>> LoadShader(NSString* name);
+36 -15
View File
@@ -782,6 +782,7 @@ bool GSDeviceMTL::DoCAS(GSTexture* sTex, GSTexture* dTex, bool sharpen_only, con
return true;
}}
#if PCSX2_HAS_METALFX
bool GSDeviceMTL::EnsureMetalFXSpatial(GSTexture* sTex, GSTexture* dTex)
{ @autoreleasepool {
id<MTLTexture> src = static_cast<GSTextureMTL*>(sTex)->GetTexture();
@@ -822,23 +823,39 @@ bool GSDeviceMTL::EnsureMetalFXSpatial(GSTexture* sTex, GSTexture* dTex)
m_mfx_in_fmt = in_fmt; m_mfx_out_fmt = out_fmt;
return true;
}}
#else
bool GSDeviceMTL::EnsureMetalFXSpatial(GSTexture* sTex, GSTexture* dTex)
{
// Statically compiled out on the iOS Simulator (PCSX2_HAS_METALFX=0).
(void)sTex; (void)dTex;
return false;
}
#endif
bool GSDeviceMTL::DoMetalFXSpatial(GSTexture* sTex, GSTexture* dTex)
{ @autoreleasepool {
if (@available(macOS 13.0, iOS 16.0, *))
{
if (!EnsureMetalFXSpatial(sTex, dTex))
return false;
{
#if PCSX2_HAS_METALFX
@autoreleasepool {
if (@available(macOS 13.0, iOS 16.0, *))
{
if (!EnsureMetalFXSpatial(sTex, dTex))
return false;
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
EndRenderPass(); // MetalFX manages its own encoder; must not be inside one.
[m_mfx_spatial setColorTexture:static_cast<GSTextureMTL*>(sTex)->GetTexture()];
[m_mfx_spatial setOutputTexture:static_cast<GSTextureMTL*>(dTex)->GetTexture()];
[m_mfx_spatial encodeToCommandBuffer:GetRenderCmdBuf()];
return true;
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
EndRenderPass(); // MetalFX manages its own encoder; must not be inside one.
[m_mfx_spatial setColorTexture:static_cast<GSTextureMTL*>(sTex)->GetTexture()];
[m_mfx_spatial setOutputTexture:static_cast<GSTextureMTL*>(dTex)->GetTexture()];
[m_mfx_spatial encodeToCommandBuffer:GetRenderCmdBuf()];
return true;
}
return false;
}
#else
// Statically compiled out on the iOS Simulator (PCSX2_HAS_METALFX=0).
(void)sTex; (void)dTex;
return false;
}}
#endif
}
MRCOwned<id<MTLFunction>> GSDeviceMTL::LoadShader(NSString* name)
{
@@ -1164,11 +1181,15 @@ bool GSDeviceMTL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
m_features.test_and_sample_depth = true;
m_features.depth_feedback = getDepthFeedback(m_dev, m_features.framebuffer_fetch);
m_features.aa1 = GSConfig.HWAA1 && m_features.vs_expand;
// MetalFX spatial upscaler: macOS 13+ / iOS 16+. The supportsDevice: probe
// returns NO on the iOS Simulator and on devices whose GPU lacks the hardware,
// so this is safe to run unconditionally on every Apple platform.
// MetalFX spatial upscaler: macOS 13+ / iOS 16+ device. The supportsDevice:
// probe returns NO on devices whose GPU lacks the hardware. On the iOS Simulator
// the MetalFX framework is absent at compile time (PCSX2_HAS_METALFX=0), so the
// feature is statically disabled here -- m_features.metalfx_spatial keeps its
// default false value and the upscaler UI will report unavailable.
#if PCSX2_HAS_METALFX
if (@available(macOS 13.0, iOS 16.0, *))
m_features.metalfx_spatial = [MTLFXSpatialScalerDescriptor supportsDevice:m_dev.dev];
#endif
m_features.rov = m_dev.features.rov && !m_features.framebuffer_fetch;
m_max_texture_size = m_dev.features.max_texsize;
+24 -12
View File
@@ -14,10 +14,17 @@
#define ARMSX2_HAS_SWIFTUI_HOST 0
#endif
// MetalFX spatial upscaler is iOS 16+ and weak-linked (see PCSX2 CMake). Both
// headers are pulled in here so isMetalFXSupported can probe device capability.
// MetalFX spatial upscaler is iOS 16+ device and weak-linked (see PCSX2 CMake).
// The iOS Simulator SDK does not ship the MetalFX framework, so the import is
// gated off when targeting the sim; isMetalFXSupported then returns NO without
// referencing MTLFXSpatialScalerDescriptor.
#import <Metal/Metal.h>
#import <MetalFX/MetalFX.h>
#if !TARGET_OS_SIMULATOR
#import <MetalFX/MetalFX.h>
#define ARMSX2_HAS_METALFX 1
#else
#define ARMSX2_HAS_METALFX 0
#endif
#include "common/Darwin/DarwinMisc.h"
#include <SDL3/SDL.h>
@@ -3515,16 +3522,21 @@ static std::string ARMSX2PerGameSettingsPath(const std::string& serial, u32 crc)
// Probes whether MetalFX Spatial upscaling is available on this device. This is
// a standalone check that works from the main menu before any GS device exists,
// so the settings UI can decide whether to show the Upscaler section at all. It
// returns NO on pre-iOS-16, the simulator, and any GPU that fails the framework
// capability probe.
// returns NO on pre-iOS-16, the simulator (statically compiled out), and any
// GPU that fails the framework capability probe.
+ (BOOL)isMetalFXSupported {
if (@available(iOS 16.0, *)) {
MRCOwned<id<MTLDevice>> device = MRCTransfer(MTLCreateSystemDefaultDevice());
if (!device)
return NO;
return [MTLFXSpatialScalerDescriptor supportsDevice:device];
}
return NO;
#if ARMSX2_HAS_METALFX
if (@available(iOS 16.0, *)) {
MRCOwned<id<MTLDevice>> device = MRCTransfer(MTLCreateSystemDefaultDevice());
if (!device)
return NO;
return [MTLFXSpatialScalerDescriptor supportsDevice:device];
}
return NO;
#else
// iOS Simulator build: MetalFX framework absent at compile time.
return NO;
#endif
}
#pragma mark - Per-game INI getter/setter
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
# Generates a CMake Xcode project configured for the iOS Simulator (SDK
# iphonesimulator, ARMSX2_REAL_DEVICE=OFF). This is the project XcodeBuildMCP
# should target for simulator workflows (debug, test, UI automation). Device
# IPAs continue to use generate-ios-xcode.sh + build-ios-ipa.sh.
#
# The sim project disables the -weak_framework MetalFX link flag (see
# pcsx2/CMakeLists.txt) because MetalFX.framework is absent from the
# iphonesimulator SDK; all source-level MetalFX references are also
# compile-guarded by PCSX2_HAS_METALFX=0 (see GSDeviceMTL.h).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SOURCE_DIR="$ROOT_DIR/app/src/main/cpp"
BUILD_DIR="${BUILD_DIR:-$ROOT_DIR/build-ios-sim-xcode}"
BUNDLE_ID="${BUNDLE_ID:-com.armsx2.ios}"
if ! command -v cmake >/dev/null 2>&1; then
echo "error: cmake is required to generate the iOS Simulator Xcode project." >&2
echo "Install CMake from https://cmake.org/download/ or through your package manager." >&2
exit 1
fi
cmake -S "$SOURCE_DIR" -B "$BUILD_DIR" -G Xcode \
-DCMAKE_SYSTEM_NAME=iOS \
-DARMSX2_REAL_DEVICE=OFF \
-DARMSX2_BUNDLE_IDENTIFIER="$BUNDLE_ID"
cat <<EOF
Generated iOS Simulator Xcode project:
$BUILD_DIR/ARMSX2iOS.xcodeproj
Open in Xcode:
open "$BUILD_DIR/ARMSX2iOS.xcodeproj"
Simulator build:
xcodebuild -project "$BUILD_DIR/ARMSX2iOS.xcodeproj" -scheme ARMSX2iOS -configuration Debug -sdk iphonesimulator build
XcodeBuildMCP defaults (set after first build):
projectPath: $BUILD_DIR/ARMSX2iOS.xcodeproj
EOF