diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index a538fe1b..fa13b251 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -465,14 +465,12 @@ if (CMAKE_SYSTEM_NAME STREQUAL "iOS" AND TARGET SDL2::SDL2main) list(PREPEND UNLEASHED_RECOMP_SDL_LIBS SDL2::SDL2main) endif() -set(UNLEASHED_RECOMP_PLATFORM_LIBS) -if (CMAKE_SYSTEM_NAME STREQUAL "iOS") - list(APPEND UNLEASHED_RECOMP_PLATFORM_LIBS MoltenVK) - target_link_options(UnleashedRecomp PRIVATE - "LINKER:-u,_vkGetInstanceProcAddr" - "LINKER:-ObjC" - ) -endif() +set(UNLEASHED_RECOMP_PLATFORM_LIBS) +if (CMAKE_SYSTEM_NAME STREQUAL "iOS") + target_link_options(UnleashedRecomp PRIVATE + "LINKER:-ObjC" + ) +endif() target_link_libraries(UnleashedRecomp PRIVATE fmt::fmt diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index 418d5fe3..ca85f59a 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -100,17 +100,30 @@ extern "C" } #endif +#if defined(UNLEASHED_RECOMP_IOS) && defined(__APPLE__) && !defined(SDL_VULKAN_ENABLED) +#define UNLEASHED_RECOMP_USE_METAL 1 +#endif + namespace plume { #ifdef UNLEASHED_RECOMP_D3D12 extern std::unique_ptr CreateD3D12Interface(); #endif +#ifdef UNLEASHED_RECOMP_USE_METAL + extern std::unique_ptr CreateMetalInterface(); +#else #ifdef SDL_VULKAN_ENABLED extern std::unique_ptr CreateVulkanInterface(RenderWindow sdlWindow); #else extern std::unique_ptr CreateVulkanInterface(); #endif +#endif +#ifdef UNLEASHED_RECOMP_USE_METAL + static std::unique_ptr CreateMetalInterfaceWrapper() { + return CreateMetalInterface(); + } +#else static std::unique_ptr CreateVulkanInterfaceWrapper() { #ifdef SDL_VULKAN_ENABLED return CreateVulkanInterface(GameWindow::s_renderWindow); @@ -118,6 +131,7 @@ namespace plume return CreateVulkanInterface(); #endif } +#endif } #pragma pack(push, 1) @@ -291,6 +305,15 @@ static bool g_vulkan = false; static constexpr bool g_vulkan = true; #endif +static const char* GetGraphicsApiName() +{ +#ifdef UNLEASHED_RECOMP_USE_METAL + return "Metal"; +#else + return g_vulkan ? "Vulkan" : "D3D12"; +#endif +} + static bool g_triangleStripWorkaround = false; static bool g_hardwareResolve = true; @@ -1755,6 +1778,8 @@ bool Video::CreateHostDevice(const char *sdlVideoDriver, bool graphicsApiRetry) interfaceFunctions.push_back(g_vulkan ? CreateVulkanInterfaceWrapper : CreateD3D12Interface); interfaceFunctions.push_back(g_vulkan ? CreateD3D12Interface : CreateVulkanInterfaceWrapper); +#elif defined(UNLEASHED_RECOMP_USE_METAL) + interfaceFunctions.push_back(CreateMetalInterfaceWrapper); #else interfaceFunctions.push_back(CreateVulkanInterfaceWrapper); #endif @@ -2529,7 +2554,7 @@ static void DrawProfiler() ImGui::Text("Hardware Depth Resolve: %s", g_hardwareDepthResolve ? "Enabled" : "Disabled"); ImGui::NewLine(); - ImGui::Text("API: %s", g_vulkan ? "Vulkan" : "D3D12"); + ImGui::Text("API: %s", GetGraphicsApiName()); ImGui::Text("Device: %s", g_device->getDescription().name.c_str()); ImGui::Text("Device Type: %s", DeviceTypeName(g_device->getDescription().type)); ImGui::Text("VRAM: %.2f MiB", (double)(g_device->getDescription().dedicatedVideoMemory) / (1024.0 * 1024.0)); diff --git a/UnleashedRecomp/ui/game_window.cpp b/UnleashedRecomp/ui/game_window.cpp index f8745517..a958a31c 100644 --- a/UnleashedRecomp/ui/game_window.cpp +++ b/UnleashedRecomp/ui/game_window.cpp @@ -6,6 +6,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #if _WIN32 #include @@ -227,8 +230,13 @@ void GameWindow::Init(const char* sdlVideoDriver) #elif defined(__linux__) s_renderWindow = { info.info.x11.display, info.info.x11.window }; #elif defined(__APPLE__) + s_metalView = SDL_Metal_CreateView(s_pWindow); +#ifdef UNLEASHED_RECOMP_IOS + s_renderWindow.window = s_metalView; +#else s_renderWindow.window = info.info.cocoa.window; - s_renderWindow.view = SDL_Metal_GetLayer(SDL_Metal_CreateView(s_pWindow)); +#endif + s_renderWindow.view = s_metalView != nullptr ? SDL_Metal_GetLayer(s_metalView) : nullptr; #else static_assert(false, "Unknown platform."); #endif @@ -449,6 +457,8 @@ uint32_t GameWindow::GetWindowFlags() #ifdef SDL_VULKAN_ENABLED flags |= SDL_WINDOW_VULKAN; +#elif defined(__APPLE__) + flags |= SDL_WINDOW_METAL; #endif return flags; diff --git a/UnleashedRecomp/ui/game_window.h b/UnleashedRecomp/ui/game_window.h index b666f4bf..57bd8764 100644 --- a/UnleashedRecomp/ui/game_window.h +++ b/UnleashedRecomp/ui/game_window.h @@ -3,6 +3,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #define DEFAULT_WIDTH 1280 #define DEFAULT_HEIGHT 720 @@ -14,6 +17,9 @@ class GameWindow public: static inline SDL_Window* s_pWindow = nullptr; static inline plume::RenderWindow s_renderWindow; +#ifdef __APPLE__ + static inline SDL_MetalView s_metalView = nullptr; +#endif static inline int s_x; static inline int s_y; diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index ca68d6ae..c63fd7f5 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -15,7 +15,10 @@ set(SDL2MIXER_OPUS OFF) set(SDL2MIXER_VORBIS "VORBISFILE") set(SDL2MIXER_WAVPACK OFF) -if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "iOS") +if (CMAKE_SYSTEM_NAME STREQUAL "iOS") + set(SDL_VULKAN_ENABLED OFF CACHE BOOL "" FORCE) + set(SDL_VULKAN OFF CACHE BOOL "" FORCE) +elseif (CMAKE_SYSTEM_NAME MATCHES "Linux") set(SDL_VULKAN_ENABLED ON CACHE BOOL "") endif() @@ -38,8 +41,9 @@ endif() add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/o1heap") add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/SDL") add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/SDL_mixer") -add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/plume") if (APPLE) add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/MoltenVK") endif() + +add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/plume") diff --git a/tools/apply_ios_submodule_patches.sh b/tools/apply_ios_submodule_patches.sh index cefffea4..7877315c 100755 --- a/tools/apply_ios_submodule_patches.sh +++ b/tools/apply_ios_submodule_patches.sh @@ -3,11 +3,9 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PLUME_DIR="$ROOT/thirdparty/plume" -PLUME_PATCH_FILE="$ROOT/tools/patches/plume-ios-sdl-vulkan.patch" +PLUME_PATCH_FILE="$ROOT/tools/patches/plume-ios-metal.patch" XENON_RECOMP_DIR="$ROOT/tools/XenonRecomp" XENON_RECOMP_PATCH_FILE="$ROOT/tools/patches/xenonrecomp-ios-streaming-memory-map.patch" -PLUME_VOLK_HEADER="$PLUME_DIR/contrib/volk/volk.h" -PLUME_VOLK_RENAME_HEADER="$PLUME_DIR/plume_volk_rename_ios.h" if [[ ! -d "$PLUME_DIR/.git" && ! -f "$PLUME_DIR/.git" ]]; then printf 'Missing Plume submodule. Run: git submodule update --init --recursive\n' >&2 @@ -31,18 +29,6 @@ else fi fi -if [[ ! -f "$PLUME_VOLK_HEADER" ]]; then - printf 'Missing Plume volk header: %s\n' "$PLUME_VOLK_HEADER" >&2 - exit 1 -fi - -{ - printf '#pragma once\n\n' - printf '// Keep Plume'\''s embedded volk function-pointer globals from colliding with statically linked MoltenVK on iOS.\n' - awk '/^extern PFN_vk/ { name = $3; sub(/;$/, "", name); print "#define " name " plumeVolk_" name }' "$PLUME_VOLK_HEADER" -} > "$PLUME_VOLK_RENAME_HEADER" -printf 'Generated Plume iOS volk rename header.\n' - if git -C "$XENON_RECOMP_DIR" apply --check "$XENON_RECOMP_PATCH_FILE" >/dev/null 2>&1; then git -C "$XENON_RECOMP_DIR" apply "$XENON_RECOMP_PATCH_FILE" printf 'Applied XenonRecomp iOS patch.\n' diff --git a/tools/patches/plume-ios-metal.patch b/tools/patches/plume-ios-metal.patch new file mode 100644 index 00000000..b2b63edb --- /dev/null +++ b/tools/patches/plume-ios-metal.patch @@ -0,0 +1,583 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 6a7645a..5417620 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -98,6 +98,34 @@ if(APPLE) + target_include_directories(plume PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/contrib/metal-cpp + ) ++ ++ if(TARGET spirv-cross-msl) ++ target_link_libraries(plume PUBLIC spirv-cross-msl) ++ endif() ++ ++ find_library(FOUNDATION_LIBRARY Foundation REQUIRED) ++ find_library(METAL_LIBRARY Metal REQUIRED) ++ find_library(QUARTZCORE_LIBRARY QuartzCore REQUIRED) ++ ++ if(CMAKE_SYSTEM_NAME STREQUAL "iOS") ++ find_library(UIKIT_LIBRARY UIKit REQUIRED) ++ target_link_libraries(plume PUBLIC ++ ${FOUNDATION_LIBRARY} ++ ${METAL_LIBRARY} ++ ${QUARTZCORE_LIBRARY} ++ ${UIKIT_LIBRARY} ++ ) ++ else() ++ find_library(APPKIT_LIBRARY AppKit REQUIRED) ++ find_library(IOKIT_LIBRARY IOKit REQUIRED) ++ target_link_libraries(plume PUBLIC ++ ${APPKIT_LIBRARY} ++ ${FOUNDATION_LIBRARY} ++ ${IOKIT_LIBRARY} ++ ${METAL_LIBRARY} ++ ${QUARTZCORE_LIBRARY} ++ ) ++ endif() + endif() + + # Add examples if requested +diff --git a/plume_apple.mm b/plume_apple.mm +index 64e4dc9..1d14600 100644 +--- a/plume_apple.mm ++++ b/plume_apple.mm +@@ -7,10 +7,20 @@ + + #include "plume_apple.h" + ++#include ++ ++#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST ++#import ++#else + #import +-#import + #import ++#endif ++ ++#import + ++#include ++ ++#if !(TARGET_OS_IOS && !TARGET_OS_MACCATALYST) + static uint32_t plumeGetEntryProperty(io_registry_entry_t entry, CFStringRef propertyName) { + uint32_t value = 0; + CFTypeRef cfProp = IORegistryEntrySearchCFProperty(entry, kIOServicePlane, propertyName, kCFAllocatorDefault, kIORegistryIterateRecursively | kIORegistryIterateParents); +@@ -27,9 +37,62 @@ static uint32_t plumeGetEntryProperty(io_registry_entry_t entry, CFStringRef pro + + return value; + } ++#endif ++ ++static plume::CocoaWindowAttributes plumeGetWindowAttributes(void* windowHandle) { ++#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST ++ UIView *view = (__bridge UIView *)windowHandle; ++ CGRect bounds = [view bounds]; ++ CGFloat scaleFactor = [view contentScaleFactor]; ++ if (scaleFactor <= 0.0) { ++ scaleFactor = [[UIScreen mainScreen] scale]; ++ } ++ ++ return { ++ 0, ++ 0, ++ (int)round(bounds.size.width * scaleFactor), ++ (int)round(bounds.size.height * scaleFactor) ++ }; ++#else ++ NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; ++ NSRect contentFrame = [[nsWindow contentView] frame]; ++ CGFloat scaleFactor = [nsWindow backingScaleFactor]; ++ ++ return { ++ (int)round(contentFrame.origin.x), ++ (int)round(contentFrame.origin.y), ++ (int)round(contentFrame.size.width * scaleFactor), ++ (int)round(contentFrame.size.height * scaleFactor) ++ }; ++#endif ++} ++ ++static int plumeGetRefreshRate(void* windowHandle, int fallbackRate) { ++#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST ++ UIView *view = (__bridge UIView *)windowHandle; ++ UIScreen *screen = [[view window] screen] ?: [UIScreen mainScreen]; ++ if (@available(iOS 10.3, *)) { ++ return (int)[screen maximumFramesPerSecond]; ++ } ++ ++ return fallbackRate > 0 ? fallbackRate : 60; ++#else ++ NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; ++ NSScreen *screen = [nsWindow screen]; ++ if (@available(macOS 12.0, *)) { ++ return (int)[screen maximumFramesPerSecond]; ++ } ++ ++ return fallbackRate; ++#endif ++} + + namespace plume { + RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { ++#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST ++ return RenderDeviceVendor::APPLE; ++#else + io_service_t entry = IOServiceGetMatchingService(MACH_PORT_NULL, IORegistryEntryIDMatching(registryID)); + + if (entry) { +@@ -44,6 +107,7 @@ RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { + } + + return RenderDeviceVendor::UNKNOWN; ++#endif + } + + // MARK: - CocoaWindow +@@ -53,19 +117,8 @@ RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { + cachedAttributes = {0, 0, 0, 0}; + + if ([NSThread isMainThread]) { +- NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; +- NSRect contentFrame = [[nsWindow contentView] frame]; +- CGFloat scaleFactor = [nsWindow backingScaleFactor]; +- +- cachedAttributes.x = (int)round(contentFrame.origin.x); +- cachedAttributes.y = (int)round(contentFrame.origin.y); +- cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor); +- cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor); +- +- NSScreen *screen = [nsWindow screen]; +- if (@available(macOS 12.0, *)) { +- cachedRefreshRate.store((int)[screen maximumFramesPerSecond]); +- } ++ cachedAttributes = plumeGetWindowAttributes(windowHandle); ++ cachedRefreshRate.store(plumeGetRefreshRate(windowHandle, cachedRefreshRate.load())); + } else { + updateWindowAttributesInternal(true); + updateRefreshRateInternal(true); +@@ -76,15 +129,8 @@ RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { + + void CocoaWindow::updateWindowAttributesInternal(bool forceSync) { + auto updateBlock = ^{ +- NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; +- NSRect contentFrame = [[nsWindow contentView] frame]; +- CGFloat scaleFactor = [nsWindow backingScaleFactor]; +- + std::lock_guard lock(attributesMutex); +- cachedAttributes.x = (int)round(contentFrame.origin.x); +- cachedAttributes.y = (int)round(contentFrame.origin.y); +- cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor); +- cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor); ++ cachedAttributes = plumeGetWindowAttributes(windowHandle); + }; + + if (forceSync) { +@@ -96,11 +142,7 @@ RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { + + void CocoaWindow::updateRefreshRateInternal(bool forceSync) { + auto updateBlock = ^{ +- NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; +- NSScreen *screen = [nsWindow screen]; +- if (@available(macOS 12.0, *)) { +- cachedRefreshRate.store((int)[screen maximumFramesPerSecond]); +- } ++ cachedRefreshRate.store(plumeGetRefreshRate(windowHandle, cachedRefreshRate.load())); + }; + + if (forceSync) { +@@ -112,16 +154,9 @@ RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { + + void CocoaWindow::getWindowAttributes(CocoaWindowAttributes* attributes) const { + if ([NSThread isMainThread]) { +- NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; +- NSRect contentFrame = [[nsWindow contentView] frame]; +- CGFloat scaleFactor = [nsWindow backingScaleFactor]; +- + { + std::lock_guard lock(attributesMutex); +- const_cast(this)->cachedAttributes.x = (int)round(contentFrame.origin.x); +- const_cast(this)->cachedAttributes.y = (int)round(contentFrame.origin.y); +- const_cast(this)->cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor); +- const_cast(this)->cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor); ++ const_cast(this)->cachedAttributes = plumeGetWindowAttributes(windowHandle); + + *attributes = cachedAttributes; + } +@@ -137,16 +172,9 @@ RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { + + int CocoaWindow::getRefreshRate() const { + if ([NSThread isMainThread]) { +- NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; +- NSScreen *screen = [nsWindow screen]; +- +- if (@available(macOS 12.0, *)) { +- int freshRate = (int)[screen maximumFramesPerSecond]; +- const_cast(this)->cachedRefreshRate.store(freshRate); +- return freshRate; +- } +- +- return cachedRefreshRate.load(); ++ int freshRate = plumeGetRefreshRate(windowHandle, cachedRefreshRate.load()); ++ const_cast(this)->cachedRefreshRate.store(freshRate); ++ return freshRate; + } else { + int rate = cachedRefreshRate.load(); + +@@ -157,6 +185,9 @@ RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { + } + + void CocoaWindow::toggleFullscreen() { ++#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST ++ return; ++#else + if ([NSThread isMainThread]) { + NSWindow *nsWindow = (__bridge NSWindow *)windowHandle; + [nsWindow toggleFullScreen:NULL]; +@@ -166,5 +197,6 @@ RenderDeviceVendor getRenderDeviceVendor(uint64_t registryID) { + [nsWindow toggleFullScreen:NULL]; + }); + } ++#endif + } + } +diff --git a/plume_metal.cpp b/plume_metal.cpp +index ebbbaa3..6f5a82c 100644 +--- a/plume_metal.cpp ++++ b/plume_metal.cpp +@@ -14,8 +14,12 @@ + #include + #include + ++#include ++ + #include + #include ++#include ++#include + + #include "plume_metal.h" + +@@ -38,6 +42,141 @@ namespace plume { + return (n + alignment - 1) & ~(alignment - 1); + } + ++ uint32_t getSpirvResourceArrayCount(const spirv_cross::SPIRType& type) { ++ uint32_t count = 1; ++ for (const uint32_t dimension : type.array) { ++ if (dimension == 0) { ++ return count; ++ } ++ count *= dimension; ++ } ++ ++ return count; ++ } ++ ++ void addSpirvResourceBinding(spirv_cross::CompilerMSL& compiler, const spirv_cross::Resource& resource) { ++ if (!compiler.has_decoration(resource.id, spv::DecorationDescriptorSet) || ++ !compiler.has_decoration(resource.id, spv::DecorationBinding)) { ++ return; ++ } ++ ++ const uint32_t descriptorSet = compiler.get_decoration(resource.id, spv::DecorationDescriptorSet); ++ const uint32_t bindingIndex = compiler.get_decoration(resource.id, spv::DecorationBinding); ++ const spirv_cross::SPIRType& type = compiler.get_type(resource.type_id); ++ ++ spirv_cross::MSLResourceBinding binding = {}; ++ binding.stage = compiler.get_execution_model(); ++ binding.basetype = type.basetype; ++ binding.desc_set = descriptorSet; ++ binding.binding = bindingIndex; ++ binding.count = getSpirvResourceArrayCount(type); ++ binding.msl_buffer = bindingIndex; ++ binding.msl_texture = bindingIndex; ++ binding.msl_sampler = bindingIndex; ++ ++ compiler.add_msl_resource_binding(binding); ++ } ++ ++ struct MetalShaderSource { ++ std::string source; ++ std::string entryPointName; ++ }; ++ ++ MetalShaderSource compileSpirvToMetalSource(const MetalDevice* device, const void* data, const uint64_t size, const char* entryPointName) { ++ if ((size % sizeof(uint32_t)) != 0) { ++ throw std::runtime_error("SPIR-V shader data is not 32-bit aligned."); ++ } ++ ++ spirv_cross::CompilerMSL compiler(static_cast(data), size / sizeof(uint32_t)); ++ std::string spirvEntryPointName = entryPointName != nullptr ? entryPointName : ""; ++ spv::ExecutionModel executionModel = compiler.get_execution_model(); ++ ++ if (!spirvEntryPointName.empty()) { ++ for (const auto& entryPoint : compiler.get_entry_points_and_stages()) { ++ if (entryPoint.name == spirvEntryPointName) { ++ compiler.set_entry_point(entryPoint.name, entryPoint.execution_model); ++ executionModel = entryPoint.execution_model; ++ break; ++ } ++ } ++ } ++ ++ if (spirvEntryPointName.empty()) { ++ const auto entryPoints = compiler.get_entry_points_and_stages(); ++ if (!entryPoints.empty()) { ++ spirvEntryPointName = entryPoints.front().name; ++ executionModel = entryPoints.front().execution_model; ++ } ++ } ++ ++ spirv_cross::CompilerMSL::Options options = compiler.get_msl_options(); ++#if PLUME_IOS ++ options.platform = spirv_cross::CompilerMSL::Options::iOS; ++ options.ios_support_base_vertex_instance = true; ++ options.emulate_cube_array = true; ++#else ++ options.platform = spirv_cross::CompilerMSL::Options::macOS; ++#endif ++ options.set_msl_version(3, 0); ++ options.argument_buffers = true; ++ options.argument_buffers_tier = ++ device->mtl->argumentBuffersSupport() == MTL::ArgumentBuffersTier2 ++ ? spirv_cross::CompilerMSL::Options::ArgumentBuffersTier::Tier2 ++ : spirv_cross::CompilerMSL::Options::ArgumentBuffersTier::Tier1; ++ options.enable_base_index_zero = true; ++ options.enable_decoration_binding = true; ++ options.force_active_argument_buffer_resources = true; ++ options.texture_buffer_native = true; ++ ++ compiler.set_msl_options(options); ++ ++ for (uint32_t descriptorSet = 0; descriptorSet < MAX_DESCRIPTOR_SET_BINDINGS; descriptorSet++) { ++ spirv_cross::MSLResourceBinding argumentBufferBinding = {}; ++ argumentBufferBinding.stage = executionModel; ++ argumentBufferBinding.desc_set = descriptorSet; ++ argumentBufferBinding.binding = spirv_cross::kArgumentBufferBinding; ++ argumentBufferBinding.msl_buffer = DESCRIPTOR_SETS_BINDING_INDEX + descriptorSet; ++ compiler.add_msl_resource_binding(argumentBufferBinding); ++ } ++ ++ const spirv_cross::ShaderResources resources = compiler.get_shader_resources(); ++ const auto addResourceList = [&](const auto& list) { ++ for (const spirv_cross::Resource& resource : list) { ++ addSpirvResourceBinding(compiler, resource); ++ } ++ }; ++ ++ addResourceList(resources.uniform_buffers); ++ addResourceList(resources.storage_buffers); ++ addResourceList(resources.stage_inputs); ++ addResourceList(resources.stage_outputs); ++ addResourceList(resources.subpass_inputs); ++ addResourceList(resources.storage_images); ++ addResourceList(resources.sampled_images); ++ addResourceList(resources.separate_images); ++ addResourceList(resources.separate_samplers); ++ addResourceList(resources.atomic_counters); ++ addResourceList(resources.acceleration_structures); ++ ++ if (!resources.push_constant_buffers.empty()) { ++ const spirv_cross::SPIRType& pushConstantType = compiler.get_type(resources.push_constant_buffers.front().type_id); ++ spirv_cross::MSLResourceBinding pushConstantsBinding = {}; ++ pushConstantsBinding.stage = executionModel; ++ pushConstantsBinding.basetype = pushConstantType.basetype; ++ pushConstantsBinding.desc_set = spirv_cross::kPushConstDescSet; ++ pushConstantsBinding.binding = spirv_cross::kPushConstBinding; ++ pushConstantsBinding.count = 1; ++ pushConstantsBinding.msl_buffer = PUSH_CONSTANTS_BINDING_INDEX; ++ compiler.add_msl_resource_binding(pushConstantsBinding); ++ } ++ ++ MetalShaderSource translatedSource = {}; ++ translatedSource.source = compiler.compile(); ++ translatedSource.entryPointName = compiler.get_cleansed_entry_point_name(spirvEntryPointName, executionModel); ++ ++ return translatedSource; ++ } ++ + uint64_t createClearPipelineKey(MTL::RenderPipelineDescriptor *pipelineDesc, bool depthWriteEnabled, bool stencilWriteEnabled) { + auto colorFormat = [&](uint32_t index) { + if (auto colorAttachment = pipelineDesc->colorAttachments()->object(index)) { +@@ -1278,24 +1417,46 @@ namespace plume { + assert(device != nullptr); + assert(data != nullptr); + assert(size > 0); +- assert(format == RenderShaderFormat::METAL); + + this->format = format; + this->functionName = (entryPointName != nullptr) ? NS::String::string(entryPointName, NS::UTF8StringEncoding) : MTLSTR(""); + + NS::Error *error = nullptr; +- const dispatch_data_t dispatchData = dispatch_data_create(data, size, dispatch_get_main_queue(), ^{}); +- library = device->mtl->newLibrary(dispatchData, &error); ++ if (format == RenderShaderFormat::METAL) { ++ const dispatch_data_t dispatchData = dispatch_data_create(data, size, dispatch_get_main_queue(), ^{}); ++ library = device->mtl->newLibrary(dispatchData, &error); ++ } else if (format == RenderShaderFormat::SPIRV) { ++ try { ++ const MetalShaderSource metalSource = compileSpirvToMetalSource(device, data, size, entryPointName); ++ functionName->release(); ++ functionName = NS::String::string(metalSource.entryPointName.c_str(), NS::UTF8StringEncoding); ++ MTL::CompileOptions *compileOptions = MTL::CompileOptions::alloc()->init(); ++ compileOptions->setFastMathEnabled(true); ++ compileOptions->setLanguageVersion(MTL::LanguageVersion3_0); ++ library = device->mtl->newLibrary(NS::String::string(metalSource.source.c_str(), NS::UTF8StringEncoding), compileOptions, &error); ++ compileOptions->release(); ++ } catch (const std::exception& e) { ++ fprintf(stderr, "SPIR-V to MSL translation failed: %s.\n", e.what()); ++ return; ++ } ++ } else { ++ assert(false && "Unsupported Metal shader format."); ++ return; ++ } + + if (error != nullptr) { +- fprintf(stderr, "MTLDevice newLibraryWithSource: failed with error %s.\n", error->localizedDescription()->utf8String()); ++ fprintf(stderr, "MTLDevice newLibrary: failed with error %s.\n", error->localizedDescription()->utf8String()); + return; + } + } + + MetalShader::~MetalShader() { +- functionName->release(); +- library->release(); ++ if (functionName) { ++ functionName->release(); ++ } ++ if (library) { ++ library->release(); ++ } + if (debugName) { + debugName->release(); + } +@@ -1306,10 +1467,16 @@ namespace plume { + debugName->release(); + } + debugName = NS::String::string(name.c_str(), NS::UTF8StringEncoding); +- library->setLabel(debugName); ++ if (library) { ++ library->setLabel(debugName); ++ } + } + + MTL::Function* MetalShader::createFunction(const RenderSpecConstant *specConstants, const uint32_t specConstantsCount) const { ++ if (library == nullptr) { ++ return nullptr; ++ } ++ + MTL::FunctionConstantValues *values = MTL::FunctionConstantValues::alloc()->init(); + if (specConstants != nullptr) { + for (uint32_t i = 0; i < specConstantsCount; i++) { +@@ -3308,6 +3475,9 @@ namespace plume { + this->renderInterface = renderInterface; + + // Device Selection ++#if PLUME_IOS ++ mtl = MTL::CreateSystemDefaultDevice(); ++#else + const NS::Array* devices = MTL::CopyAllDevices(); + MTL::Device *preferredDevice = nullptr; + for (NS::UInteger i = 0; i < devices->count(); i++) { +@@ -3320,9 +3490,18 @@ namespace plume { + } + + mtl = preferredDevice ? preferredDevice : MTL::CreateSystemDefaultDevice();; ++#endif ++ if (mtl == nullptr) { ++ return; ++ } ++ + const std::string deviceName(mtl->name()->utf8String()); + description.name = deviceName; ++#if PLUME_IOS ++ description.type = RenderDeviceType::INTEGRATED; ++#else + description.type = mapDeviceType(mtl->location()); ++#endif + description.driverVersion = 1; // Unavailable + description.vendor = mtl->supportsFamily(MTL::GPUFamilyApple1) ? RenderDeviceVendor::APPLE : getRenderDeviceVendor(mtl->registryID()); + description.dedicatedVideoMemory = mtl->recommendedMaxWorkingSetSize(); +@@ -3357,17 +3536,19 @@ namespace plume { + } + + MetalDevice::~MetalDevice() { +- mtl->release(); ++ if (mtl) { ++ mtl->release(); ++ } + + for (const auto& [key, state] : clearRenderPipelineStates) { + state->release(); + } + +- resolveTexturePipelineState->release(); +- clearVertexFunction->release(); +- clearColorFunction->release(); +- clearDepthFunction->release(); +- sharedBlitDescriptor->release(); ++ if (resolveTexturePipelineState) resolveTexturePipelineState->release(); ++ if (clearVertexFunction) clearVertexFunction->release(); ++ if (clearColorFunction) clearColorFunction->release(); ++ if (clearDepthFunction) clearDepthFunction->release(); ++ if (sharedBlitDescriptor) sharedBlitDescriptor->release(); + } + + std::unique_ptr MetalDevice::createDescriptorSet(const RenderDescriptorSetDesc &desc) { +@@ -3635,16 +3816,24 @@ namespace plume { + + MetalInterface::MetalInterface() { + NS::AutoreleasePool *releasePool = NS::AutoreleasePool::alloc()->init(); +- capabilities.shaderFormat = RenderShaderFormat::METAL; ++ capabilities.shaderFormat = RenderShaderFormat::SPIRV; + + releasePool->release(); + + // Fill device names. ++#if PLUME_IOS ++ MTL::Device* device = MTL::CreateSystemDefaultDevice(); ++ if (device != nullptr) { ++ deviceNames.push_back(std::string(device->name()->utf8String())); ++ device->release(); ++ } ++#else + const NS::Array* devices = MTL::CopyAllDevices(); + for (NS::UInteger i = 0; i < devices->count(); i++) { + NS::String* deviceName = ((MTL::Device *)devices->object(i))->name(); + deviceNames.push_back(std::string(deviceName->utf8String())); + } ++#endif + } + + MetalInterface::~MetalInterface() {} +diff --git a/plume_metal.h b/plume_metal.h +index fd367ed..557686b 100644 +--- a/plume_metal.h ++++ b/plume_metal.h +@@ -608,16 +608,16 @@ namespace plume { + RenderDeviceDescription description; + + // Resolve functionality +- MTL::ComputePipelineState *resolveTexturePipelineState; ++ MTL::ComputePipelineState *resolveTexturePipelineState = nullptr; + + // Clear functionality +- MTL::Function* clearVertexFunction; +- MTL::Function* clearColorFunction; +- MTL::Function* clearDepthFunction; +- MTL::Function* clearStencilFunction; +- MTL::DepthStencilState *clearDepthState; +- MTL::DepthStencilState *clearStencilState; +- MTL::DepthStencilState *clearDepthStencilState; ++ MTL::Function* clearVertexFunction = nullptr; ++ MTL::Function* clearColorFunction = nullptr; ++ MTL::Function* clearDepthFunction = nullptr; ++ MTL::Function* clearStencilFunction = nullptr; ++ MTL::DepthStencilState *clearDepthState = nullptr; ++ MTL::DepthStencilState *clearStencilState = nullptr; ++ MTL::DepthStencilState *clearDepthStencilState = nullptr; + + std::mutex clearPipelineStateMutex; + std::unordered_map clearRenderPipelineStates; diff --git a/tools/patches/plume-ios-sdl-vulkan.patch b/tools/patches/plume-ios-sdl-vulkan.patch deleted file mode 100644 index 15f7a88e..00000000 --- a/tools/patches/plume-ios-sdl-vulkan.patch +++ /dev/null @@ -1,178 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 6a7645a..6e6e887 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -8,10 +8,16 @@ if(APPLE) - endif() - - string(COMPARE EQUAL ${CMAKE_SYSTEM_NAME} "Linux" IS_LINUX) -+string(COMPARE EQUAL ${CMAKE_SYSTEM_NAME} "iOS" IS_IOS) -+if(IS_LINUX OR IS_IOS) -+ set(IS_SDL_VULKAN_PLATFORM ON) -+else() -+ set(IS_SDL_VULKAN_PLATFORM OFF) -+endif() - - # Project options - include(CMakeDependentOption) --cmake_dependent_option(SDL_VULKAN_ENABLED "Enable SDL Vulkan integration" OFF IS_LINUX OFF) -+cmake_dependent_option(SDL_VULKAN_ENABLED "Enable SDL Vulkan integration" OFF IS_SDL_VULKAN_PLATFORM OFF) - cmake_dependent_option(D3D12_AGILITY_SDK_ENABLED "Enable D3D12 Agility SDK" OFF WIN32 OFF) - option(PLUME_BUILD_EXAMPLES "Build example applications" OFF) - -@@ -50,7 +56,7 @@ set(PLUME_SOURCES - ) - - # Platform-specific files --if(APPLE) -+if(APPLE AND NOT IS_IOS) - list(APPEND PLUME_SOURCES - plume_metal.cpp - plume_metal.h -@@ -94,7 +100,7 @@ if(D3D12_AGILITY_SDK_ENABLED) - endif() - - # Platform-specific includes --if(APPLE) -+if(APPLE AND NOT IS_IOS) - target_include_directories(plume PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/contrib/metal-cpp - ) -diff --git a/plume_vulkan.cpp b/plume_vulkan.cpp -index 9103ca8..63de1cc 100644 ---- a/plume_vulkan.cpp -+++ b/plume_vulkan.cpp -@@ -8,6 +8,9 @@ - #define VMA_IMPLEMENTATION - #define VOLK_IMPLEMENTATION - -+#if defined(__APPLE__) && defined(SDL_VULKAN_ENABLED) -+#include "plume_volk_rename_ios.h" -+#endif - #include "plume_vulkan.h" - - #include -@@ -19,6 +22,10 @@ - # include "render/plume_dlss.h" - #endif - -+#if defined(__APPLE__) && defined(SDL_VULKAN_ENABLED) -+extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL mvkGetInstanceProcAddr(VkInstance instance, const char *pName) __asm("_vkGetInstanceProcAddr"); -+#endif -+ - #ifndef NDEBUG - # define VULKAN_VALIDATION_LAYER_ENABLED - # define VULKAN_OBJECT_NAMES_ENABLED -@@ -88,6 +95,19 @@ namespace plume { - VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME, - }; - -+ static std::unordered_set getRequiredDeviceExtensions(uint32_t apiVersion) { -+ std::unordered_set extensions = RequiredDeviceExtensions; -+ -+ // MoltenVK and other Vulkan 1.2+ implementations expose these as core features -+ // and no longer enumerate them as separate device extensions. -+ if (apiVersion >= VK_API_VERSION_1_2) { -+ extensions.erase(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); -+ extensions.erase(VK_KHR_SAMPLER_MIRROR_CLAMP_TO_EDGE_EXTENSION_NAME); -+ } -+ -+ return extensions; -+ } -+ - // Common functions. - - static uint32_t roundUp(uint32_t value, uint32_t powerOf2Alignment) { -@@ -2112,6 +2132,12 @@ namespace plume { - fprintf(stderr, "vkCreateXlibSurfaceKHR failed with error code 0x%X.\n", res); - return; - } -+# elif defined(__APPLE__) && defined(SDL_VULKAN_ENABLED) -+ VulkanInterface *renderInterface = commandQueue->device->renderInterface; -+ if (!SDL_Vulkan_CreateSurface(renderWindow, renderInterface->instance, &surface)) { -+ fprintf(stderr, "SDL_Vulkan_CreateSurface failed: %s.\n", SDL_GetError()); -+ return; -+ } - # elif defined(__APPLE__) - assert(renderWindow.window != 0); - assert(renderWindow.view != 0); -@@ -2443,7 +2469,7 @@ namespace plume { - // The attributes width and height members do not include the border. - dstWidth = attributes.width; - dstHeight = attributes.height; --# elif defined(__APPLE__) -+# elif defined(__APPLE__) && !defined(SDL_VULKAN_ENABLED) - CocoaWindowAttributes attributes; - windowWrapper->getWindowAttributes(&attributes); - dstWidth = attributes.width; -@@ -3754,6 +3780,10 @@ namespace plume { - return; - } - -+ VkPhysicalDeviceProperties selectedDeviceProperties; -+ vkGetPhysicalDeviceProperties(physicalDevice, &selectedDeviceProperties); -+ const std::unordered_set requiredDeviceExtensions = getRequiredDeviceExtensions(selectedDeviceProperties.apiVersion); -+ - // Check for extensions. - uint32_t extensionCount; - vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr); -@@ -3761,7 +3791,7 @@ namespace plume { - std::vector availableExtensions(extensionCount); - vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, availableExtensions.data()); - -- std::unordered_set missingRequiredExtensions = RequiredDeviceExtensions; -+ std::unordered_set missingRequiredExtensions = requiredDeviceExtensions; - std::unordered_set supportedOptionalExtensions; - # if DLSS_ENABLED - const std::unordered_set dlssExtensions = DLSS::getRequiredDeviceExtensionsVulkan(this); -@@ -3969,7 +3999,7 @@ namespace plume { - } - - std::vector enabledExtensions; -- for (const std::string &extension : RequiredDeviceExtensions) { -+ for (const std::string &extension : requiredDeviceExtensions) { - enabledExtensions.push_back(extension.c_str()); - } - -@@ -4398,11 +4428,16 @@ namespace plume { - #else - VulkanInterface::VulkanInterface() { - #endif -- VkResult res = volkInitialize(); -+ VkResult res = VK_SUCCESS; -+#if defined(__APPLE__) && defined(SDL_VULKAN_ENABLED) -+ volkInitializeCustom(mvkGetInstanceProcAddr); -+#else -+ res = volkInitialize(); - if (res != VK_SUCCESS) { - fprintf(stderr, "volkInitialize failed with error code 0x%X.\n", res); - return; - } -+#endif - - appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; - appInfo.pApplicationName = "plume"; -diff --git a/plume_vulkan.h b/plume_vulkan.h -index 73022bb..9d89adf 100644 ---- a/plume_vulkan.h -+++ b/plume_vulkan.h -@@ -22,8 +22,10 @@ - #define VK_USE_PLATFORM_XLIB_KHR - #elif defined(__APPLE__) - #define VK_USE_PLATFORM_METAL_EXT -+#ifndef SDL_VULKAN_ENABLED - #include "plume_apple.h" - #endif -+#endif - - // For VK_KHR_portability_subset - #define VK_ENABLE_BETA_EXTENSIONS -@@ -226,7 +228,7 @@ namespace plume { - VulkanCommandQueue *commandQueue = nullptr; - VkSurfaceKHR surface = VK_NULL_HANDLE; - RenderWindow renderWindow = {}; --#if defined(__APPLE__) -+#if defined(__APPLE__) && !defined(SDL_VULKAN_ENABLED) - std::unique_ptr windowWrapper; - #endif - uint32_t textureCount = 0;