Switch iOS renderer to Metal

This commit is contained in:
aperezro 2026-06-07 19:02:23 -06:00
parent bf92b2cc4d
commit 42ed8f1580
8 changed files with 639 additions and 205 deletions

View file

@ -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

View file

@ -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<RenderInterface> CreateD3D12Interface();
#endif
#ifdef UNLEASHED_RECOMP_USE_METAL
extern std::unique_ptr<RenderInterface> CreateMetalInterface();
#else
#ifdef SDL_VULKAN_ENABLED
extern std::unique_ptr<RenderInterface> CreateVulkanInterface(RenderWindow sdlWindow);
#else
extern std::unique_ptr<RenderInterface> CreateVulkanInterface();
#endif
#endif
#ifdef UNLEASHED_RECOMP_USE_METAL
static std::unique_ptr<RenderInterface> CreateMetalInterfaceWrapper() {
return CreateMetalInterface();
}
#else
static std::unique_ptr<RenderInterface> 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));

View file

@ -6,6 +6,9 @@
#include <app.h>
#include <sdl_listener.h>
#include <SDL_syswm.h>
#ifdef __APPLE__
#include <SDL_metal.h>
#endif
#if _WIN32
#include <dwmapi.h>
@ -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;

View file

@ -3,6 +3,9 @@
#include <plume_render_interface_types.h>
#include <user/config.h>
#include <sdl_events.h>
#ifdef __APPLE__
#include <SDL_metal.h>
#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;

View file

@ -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")

View file

@ -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'

View file

@ -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 <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+#import <UIKit/UIKit.h>
+#else
#import <AppKit/AppKit.h>
-#import <Foundation/Foundation.h>
#import <IOKit/IOKitLib.h>
+#endif
+
+#import <Foundation/Foundation.h>
+#include <cmath>
+
+#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<std::mutex> 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<std::mutex> lock(attributesMutex);
- const_cast<CocoaWindow*>(this)->cachedAttributes.x = (int)round(contentFrame.origin.x);
- const_cast<CocoaWindow*>(this)->cachedAttributes.y = (int)round(contentFrame.origin.y);
- const_cast<CocoaWindow*>(this)->cachedAttributes.width = (int)round(contentFrame.size.width * scaleFactor);
- const_cast<CocoaWindow*>(this)->cachedAttributes.height = (int)round(contentFrame.size.height * scaleFactor);
+ const_cast<CocoaWindow*>(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<CocoaWindow*>(this)->cachedRefreshRate.store(freshRate);
- return freshRate;
- }
-
- return cachedRefreshRate.load();
+ int freshRate = plumeGetRefreshRate(windowHandle, cachedRefreshRate.load());
+ const_cast<CocoaWindow*>(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 <QuartzCore/QuartzCore.hpp>
#include <CoreFoundation/CoreFoundation.h>
+#include <spirv_msl.hpp>
+
#include <algorithm>
#include <mutex>
+#include <stdexcept>
+#include <string>
#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<const uint32_t*>(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<RenderDescriptorSet> 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<uint64_t, MTL::RenderPipelineState *> clearRenderPipelineStates;

View file

@ -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 <algorithm>
@@ -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<std::string> getRequiredDeviceExtensions(uint32_t apiVersion) {
+ std::unordered_set<std::string> 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<std::string> requiredDeviceExtensions = getRequiredDeviceExtensions(selectedDeviceProperties.apiVersion);
+
// Check for extensions.
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr);
@@ -3761,7 +3791,7 @@ namespace plume {
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, availableExtensions.data());
- std::unordered_set<std::string> missingRequiredExtensions = RequiredDeviceExtensions;
+ std::unordered_set<std::string> missingRequiredExtensions = requiredDeviceExtensions;
std::unordered_set<std::string> supportedOptionalExtensions;
# if DLSS_ENABLED
const std::unordered_set<std::string> dlssExtensions = DLSS::getRequiredDeviceExtensionsVulkan(this);
@@ -3969,7 +3999,7 @@ namespace plume {
}
std::vector<const char *> 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<CocoaWindow> windowWrapper;
#endif
uint32_t textureCount = 0;