From 2e50c8bfbc1ce6bf31d7a12deb91d1366065ec05 Mon Sep 17 00:00:00 2001 From: J1coding Date: Thu, 23 Jul 2026 00:29:26 +0200 Subject: [PATCH] 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 (``, `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. --- pcsx2/CMakeLists.txt | 8 ++- pcsx2/GS/Renderers/Metal/GSDeviceMTL.h | 22 ++++++-- pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm | 51 +++++++++++++------ .../ios/app/src/main/cpp/ARMSX2Bridge.mm | 36 ++++++++----- .../ios/scripts/generate-ios-sim-xcode.sh | 44 ++++++++++++++++ 5 files changed, 127 insertions(+), 34 deletions(-) create mode 100755 platforms/ios/scripts/generate-ios-sim-xcode.sh diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index 4401cc6c7f..cdd8b7b79a 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -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}) diff --git a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h index 434e3005aa..38bb32ae1f 100644 --- a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h +++ b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h @@ -27,10 +27,15 @@ using GSMTLView = UIView; #include 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 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 +#endif #include #include #include @@ -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> 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>> m_convert_pipeline; MRCOwned> m_present_pipeline[static_cast(PresentShader::Count)]; MRCOwned> m_merge_pipeline[4]; @@ -416,7 +423,12 @@ public: bool DoCAS(GSTexture* sTex, GSTexture* dTex, bool sharpen_only, const std::array& 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> LoadShader(NSString* name); diff --git a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm index 481c63b670..5760e687f3 100644 --- a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm +++ b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm @@ -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 src = static_cast(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(sTex)->GetTexture()]; - [m_mfx_spatial setOutputTexture:static_cast(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(sTex)->GetTexture()]; + [m_mfx_spatial setOutputTexture:static_cast(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> 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; diff --git a/platforms/ios/app/src/main/cpp/ARMSX2Bridge.mm b/platforms/ios/app/src/main/cpp/ARMSX2Bridge.mm index f8115c5a89..a740f178f1 100644 --- a/platforms/ios/app/src/main/cpp/ARMSX2Bridge.mm +++ b/platforms/ios/app/src/main/cpp/ARMSX2Bridge.mm @@ -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 -#import +#if !TARGET_OS_SIMULATOR + #import + #define ARMSX2_HAS_METALFX 1 +#else + #define ARMSX2_HAS_METALFX 0 +#endif #include "common/Darwin/DarwinMisc.h" #include @@ -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> device = MRCTransfer(MTLCreateSystemDefaultDevice()); - if (!device) - return NO; - return [MTLFXSpatialScalerDescriptor supportsDevice:device]; - } - return NO; +#if ARMSX2_HAS_METALFX + if (@available(iOS 16.0, *)) { + MRCOwned> 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 diff --git a/platforms/ios/scripts/generate-ios-sim-xcode.sh b/platforms/ios/scripts/generate-ios-sim-xcode.sh new file mode 100755 index 0000000000..7550c18738 --- /dev/null +++ b/platforms/ios/scripts/generate-ios-sim-xcode.sh @@ -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 <