From 943d26e2d9d6df30c1512e7a0299fa1ebbe2d972 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:15:18 +0100 Subject: [PATCH] add macOS build and launch support --- CMakeLists.txt | 43 +++++++++++--- CMakePresets.json | 41 +++++++++++++ src/skate3_app_common.cpp | 95 ++++++++++++++++++++++++++++--- src/skate3_iso_installer.cpp | 61 ++++++++++++++++---- src/skate3_iso_installer.h | 3 + src/skate3_iso_installer_macos.mm | 53 +++++++++++++++++ third_party/rexglue-sdk | 2 +- 7 files changed, 271 insertions(+), 27 deletions(-) create mode 100644 src/skate3_iso_installer_macos.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index aed4f6c..aebf948 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,9 @@ project(skate3 VERSION 1.0.0 LANGUAGES CXX) if(WIN32) enable_language(RC) endif() +if(APPLE) + enable_language(OBJCXX) +endif() set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -16,24 +19,38 @@ skate3_resolve_version(SKATE3_FULL_VERSION SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) string(REGEX REPLACE "-.*$" "" SKATE3_NUMERIC_VERSION "${SKATE3_FULL_VERSION}") +set(SKATE3_TARGET_PROCESSOR "${CMAKE_SYSTEM_PROCESSOR}") +if(APPLE AND CMAKE_OSX_ARCHITECTURES) + list(LENGTH CMAKE_OSX_ARCHITECTURES SKATE3_OSX_ARCH_COUNT) + if(SKATE3_OSX_ARCH_COUNT EQUAL 1) + set(SKATE3_TARGET_PROCESSOR "${CMAKE_OSX_ARCHITECTURES}") + endif() +endif() + if(WIN32) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64") + if(SKATE3_TARGET_PROCESSOR MATCHES "AMD64|x86_64") set(SKATE3_BUILD_PLATFORM "win-amd64") - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") + elseif(SKATE3_TARGET_PROCESSOR MATCHES "aarch64|ARM64") set(SKATE3_BUILD_PLATFORM "win-arm64") else() - set(SKATE3_BUILD_PLATFORM "win-${CMAKE_SYSTEM_PROCESSOR}") + set(SKATE3_BUILD_PLATFORM "win-${SKATE3_TARGET_PROCESSOR}") + endif() +elseif(APPLE) + if(SKATE3_TARGET_PROCESSOR MATCHES "aarch64|arm64|ARM64") + set(SKATE3_BUILD_PLATFORM "macos-arm64") + else() + message(FATAL_ERROR "Unsupported macOS architecture: ${SKATE3_TARGET_PROCESSOR}. macOS builds are ARM64-only.") endif() elseif(UNIX AND NOT APPLE) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64") + if(SKATE3_TARGET_PROCESSOR MATCHES "AMD64|x86_64") set(SKATE3_BUILD_PLATFORM "linux-amd64") - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") + elseif(SKATE3_TARGET_PROCESSOR MATCHES "aarch64|ARM64") set(SKATE3_BUILD_PLATFORM "linux-arm64") else() - set(SKATE3_BUILD_PLATFORM "linux-${CMAKE_SYSTEM_PROCESSOR}") + set(SKATE3_BUILD_PLATFORM "linux-${SKATE3_TARGET_PROCESSOR}") endif() else() - set(SKATE3_BUILD_PLATFORM "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") + set(SKATE3_BUILD_PLATFORM "${CMAKE_SYSTEM_NAME}-${SKATE3_TARGET_PROCESSOR}") endif() string(TIMESTAMP SKATE3_BUILD_TIMESTAMP "%Y%m%d_%H%M") @@ -50,7 +67,7 @@ set(SKATE3_GAME_DATA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/game" CACHE PATH "Path to an extracted Skate 3 retail game dump") -if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64") +if(NOT APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64") if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") add_compile_options(-march=x86-64-v3) set(SNAPPY_REQUIRE_AVX2 ON CACHE BOOL @@ -82,7 +99,7 @@ configure_file( @ONLY ) set(REXGLUE_ENABLE_TRACY OFF CACHE BOOL "Disable Tracy instrumentation for normal Skate 3 builds" FORCE) -set(REXGLUE_ENABLE_PERF_COUNTERS OFF CACHE BOOL "Enable lightweight rexglue perf counter instrumentation" FORCE) +set(REXGLUE_ENABLE_PERF_COUNTERS OFF CACHE BOOL "Enable lightweight rexglue perf counter instrumentation") include(cmake/RexGlue.cmake) include(cmake/CodegenTargets.cmake) @@ -93,6 +110,9 @@ set(SKATE3_COMMON_SOURCES src/skate3_iso_installer.cpp src/skate3_user_settings.cpp ) +if(APPLE) + list(APPEND SKATE3_COMMON_SOURCES src/skate3_iso_installer_macos.mm) +endif() set(SKATE3_SOURCES src/main.cpp @@ -140,6 +160,11 @@ endif() target_link_libraries(skate3 PRIVATE rex::runtime) rexglue_configure_target(skate3) +if(APPLE) + target_link_options(skate3 PRIVATE -Wl,-stack_size,0x4000000) + target_link_libraries(skate3 PRIVATE "-framework Cocoa") +endif() + if(WIN32) target_link_libraries(skate3 PRIVATE comdlg32) endif() diff --git a/CMakePresets.json b/CMakePresets.json index c9256ec..475387f 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -31,6 +31,23 @@ "rhs": "Linux" } }, + { + "name": "macos-base", + "hidden": true, + "inherits": "base", + "cacheVariables": { + "CMAKE_C_COMPILER": "/opt/homebrew/opt/llvm/bin/clang", + "CMAKE_CXX_COMPILER": "/opt/homebrew/opt/llvm/bin/clang++", + "CMAKE_AR": "/opt/homebrew/opt/llvm/bin/llvm-ar", + "CMAKE_RANLIB": "/opt/homebrew/opt/llvm/bin/llvm-ranlib", + "CMAKE_OSX_DEPLOYMENT_TARGET": "12.0" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, { "name": "debug", "displayName": "Debug", @@ -70,6 +87,22 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "macos-relwithdebinfo", + "displayName": "macOS RelWithDebInfo", + "inherits": "macos-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "macos-release", + "displayName": "macOS Release", + "inherits": "macos-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } } ], "buildPresets": [ @@ -92,6 +125,14 @@ { "name": "linux-release", "configurePreset": "linux-release" + }, + { + "name": "macos-relwithdebinfo", + "configurePreset": "macos-relwithdebinfo" + }, + { + "name": "macos-release", + "configurePreset": "macos-release" } ] } diff --git a/src/skate3_app_common.cpp b/src/skate3_app_common.cpp index a0784f9..cb50b4f 100644 --- a/src/skate3_app_common.cpp +++ b/src/skate3_app_common.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #if defined(_WIN32) @@ -25,8 +26,11 @@ #define NOMINMAX #endif #include -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) #include +#if defined(__APPLE__) +#include +#endif #endif #include @@ -37,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -49,7 +54,7 @@ #include #include -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) extern char** environ; #endif @@ -61,10 +66,18 @@ extern "C" REX_FUNC(__restgprlr_19); namespace { -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) std::vector CurrentProcessArgumentsForRestart( const std::filesystem::path& executable_path) { std::vector args; +#if defined(__APPLE__) + int argc = *_NSGetArgc(); + char** argv = *_NSGetArgv(); + args.reserve(static_cast(argc > 0 ? argc : 1)); + for (int i = 0; i < argc; ++i) { + args.emplace_back(argv[i] ? argv[i] : ""); + } +#else std::ifstream cmdline("/proc/self/cmdline", std::ios::binary); std::string arg; char ch = 0; @@ -79,6 +92,7 @@ std::vector CurrentProcessArgumentsForRestart( if (!arg.empty()) { args.push_back(std::move(arg)); } +#endif const std::string executable = rex::path_to_utf8(executable_path); if (args.empty()) { @@ -88,6 +102,28 @@ std::vector CurrentProcessArgumentsForRestart( } return args; } + +void SetRestartArgument(std::vector& args, std::string name, std::string value) { + const std::string option = "--" + name; + const std::string option_with_equals = option + "="; + for (size_t i = 1; i < args.size(); ++i) { + if (args[i] == option) { + if (i + 1 < args.size()) { + args[i + 1] = std::move(value); + } else { + args.push_back(std::move(value)); + } + return; + } + if (args[i].starts_with(option_with_equals)) { + args[i] = option_with_equals + value; + return; + } + } + + args.push_back(std::move(option)); + args.push_back(std::move(value)); +} #endif constexpr std::string_view kUserDirectoryName = "skate3"; @@ -190,6 +226,19 @@ void LoadAndNormalizeSimpleSettings(const std::filesystem::path& settings_path, rex::ui::EnsureSimpleSettingsConfig(settings_path); } +std::filesystem::path ResolveRuntimeGameDataRoot(const rex::PathConfig& paths) { + if (!paths.game_data_root.empty()) { + return paths.game_data_root; + } + + const auto working_directory_game = std::filesystem::current_path() / "game"; + if (skate3::IsGameInstalled(working_directory_game)) { + return working_directory_game; + } + + return paths.config_path.parent_path() / "game"; +} + const char* FirstExistingFontPath(std::initializer_list paths) { for (const char* path : paths) { if (std::filesystem::exists(path)) { @@ -283,13 +332,19 @@ std::optional Skate3BaseApp::OnFinalizePaths( skate3::SaveProfiles(profiles_path_, profiles); } auto runtime_paths = defaults; - if (runtime_paths.game_data_root.empty()) { - runtime_paths.game_data_root = config_path_.parent_path() / "game"; - } + runtime_paths.game_data_root = ResolveRuntimeGameDataRoot(runtime_paths); runtime_paths.config_path.clear(); if (!skate3::IsGameInstalled(runtime_paths.game_data_root)) { REXLOG_INFO("Game files not found at {}; launching rexglue ISO installer", runtime_paths.game_data_root.string()); +#if defined(__APPLE__) + if (const char* automated_iso = std::getenv("SKATE3_INSTALL_ISO"); + automated_iso == nullptr || *automated_iso == '\0') { + skate3::ShowRexglueIsoInstallWizard(imgui_drawer(), std::move(runtime_paths), + std::move(resume)); + return std::nullopt; + } +#endif rex::PathConfig installed_paths; const bool installed = skate3::RunRexglueIsoInstallWizardBlocking( app_context(), window(), imgui_drawer(), runtime_paths, installed_paths); @@ -357,6 +412,7 @@ void Skate3BaseApp::OnPostSetup() { void Skate3BaseApp::OnPostLaunchModule(rex::system::XThread* thread) { (void)thread; + if (std::getenv("SKATE3_INVALIDATE_GPU_MEMORY_EVERY_FRAME") == nullptr) { return; } @@ -445,12 +501,23 @@ void Skate3BaseApp::ToggleSimpleSettings() { }; auto close_settings = [this]() { ApplyGameplayCursorMode(); }; auto close_game = [this]() { +#if REX_PLATFORM_MAC + std::thread([]() { + std::this_thread::sleep_for(std::chrono::seconds(10)); + REXLOG_WARN("macOS Close Game watchdog exiting process after shutdown timeout"); + std::_Exit(EXIT_SUCCESS); + }).detach(); +#endif app_context().CallInUIThreadDeferred([this]() { +#if REX_PLATFORM_MAC + app_context().QuitFromUIThread(); +#else if (window()) { window()->RequestClose(); } else { app_context().QuitFromUIThread(); } +#endif }); }; auto restart_game = [this]() { RestartGame(); }; @@ -494,7 +561,7 @@ void Skate3BaseApp::RestartGame() { } CloseHandle(process_info.hThread); CloseHandle(process_info.hProcess); -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) const auto executable_path = rex::filesystem::GetExecutablePath(); if (executable_path.empty()) { REXLOG_WARN("Restart requested, but the executable path could not be resolved"); @@ -502,6 +569,16 @@ void Skate3BaseApp::RestartGame() { } auto args = CurrentProcessArgumentsForRestart(executable_path); + std::filesystem::path restart_game_data_root; + if (runtime()) { + restart_game_data_root = runtime()->game_data_root(); + } + if (restart_game_data_root.empty()) { + restart_game_data_root = game_data_root(); + } + if (!restart_game_data_root.empty()) { + SetRestartArgument(args, "game_data_root", rex::path_to_utf8(restart_game_data_root)); + } std::vector argv; argv.reserve(args.size() + 1); for (auto& arg : args) { @@ -523,11 +600,15 @@ void Skate3BaseApp::RestartGame() { return; #endif + #if REX_PLATFORM_MAC + app_context().QuitFromUIThread(); + #else if (window()) { window()->RequestClose(); } else { app_context().QuitFromUIThread(); } + #endif }); } diff --git a/src/skate3_iso_installer.cpp b/src/skate3_iso_installer.cpp index 01f056e..01567d0 100644 --- a/src/skate3_iso_installer.cpp +++ b/src/skate3_iso_installer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -25,11 +26,17 @@ #include #include +#elif defined(__APPLE__) #else #include #endif namespace skate3 { + +#if defined(__APPLE__) +std::filesystem::path PickIsoFileMacOS(); +#endif + namespace { constexpr uint64_t kSectorSize = 2048; @@ -93,6 +100,10 @@ std::filesystem::path PickIsoFile() { } return filename; } +#elif defined(__APPLE__) +std::filesystem::path PickIsoFile() { + return skate3::PickIsoFileMacOS(); +} #else std::filesystem::path PickIsoFile() { GtkWidget* dialog = gtk_file_chooser_dialog_new( @@ -352,6 +363,38 @@ bool IsGameInstalled(const std::filesystem::path& game_root) { return std::filesystem::is_regular_file(game_root / std::string(kDefaultXex)); } +void ShowRexglueIsoInstallWizard(rex::ui::ImGuiDrawer* drawer, rex::PathConfig runtime_paths, + std::function complete) { + auto pick_source = []() { return PickIsoFile(); }; + auto install = [game_root = runtime_paths.game_data_root]( + const std::filesystem::path& source, std::atomic& copied_bytes, + std::atomic& total_bytes, std::string& error) { + XboxIsoReader iso; + if (!iso.Open(source, error)) { + return false; + } + total_bytes = iso.TotalSize(); + if (!iso.ExtractAll(game_root, copied_bytes, error)) { + return false; + } + if (!IsGameInstalled(game_root)) { + error = "Installation completed, but default.xex was not found in the install directory."; + return false; + } + return true; + }; + + new rex::ui::InstallWizardDialog( + drawer, "Skate 3 Setup", + "Skate 3 game files were not found. Select your Xbox 360 ISO to install them.", + runtime_paths.game_data_root.string(), std::move(pick_source), std::move(install), + [runtime_paths = std::move(runtime_paths), complete = std::move(complete)]() mutable { + if (complete) { + complete(std::move(runtime_paths)); + } + }); +} + bool RunRexglueIsoInstallWizardBlocking(rex::ui::WindowedAppContext& app_context, rex::ui::Window* window, rex::ui::ImGuiDrawer* drawer, @@ -364,7 +407,6 @@ bool RunRexglueIsoInstallWizardBlocking(rex::ui::WindowedAppContext& app_context }; auto result = std::make_shared(); - auto pick_source = []() { return PickIsoFile(); }; auto install = [game_root = runtime_paths.game_data_root]( const std::filesystem::path& source, std::atomic& copied_bytes, std::atomic& total_bytes, std::string& error) { @@ -398,15 +440,12 @@ bool RunRexglueIsoInstallWizardBlocking(rex::ui::WindowedAppContext& app_context return true; } - new rex::ui::InstallWizardDialog( - drawer, "Skate 3 Setup", - "Skate 3 game files were not found. Select your Xbox 360 ISO to install them.", - runtime_paths.game_data_root.string(), std::move(pick_source), std::move(install), - [result, runtime_paths = std::move(runtime_paths)]() mutable { - result->paths = std::move(runtime_paths); - result->ok = true; - result->done = true; - }); + ShowRexglueIsoInstallWizard(drawer, runtime_paths, + [result](rex::PathConfig runtime_paths) mutable { + result->paths = std::move(runtime_paths); + result->ok = true; + result->done = true; + }); #if defined(_WIN32) HWND hwnd = nullptr; @@ -442,9 +481,11 @@ bool RunRexglueIsoInstallWizardBlocking(rex::ui::WindowedAppContext& app_context if (window) { window->RequestPaint(); } +#if !defined(__APPLE__) while (gtk_events_pending()) { gtk_main_iteration_do(FALSE); } +#endif #endif std::this_thread::sleep_for(std::chrono::milliseconds(16)); } diff --git a/src/skate3_iso_installer.h b/src/skate3_iso_installer.h index 3019d81..0750141 100644 --- a/src/skate3_iso_installer.h +++ b/src/skate3_iso_installer.h @@ -1,12 +1,15 @@ #pragma once #include +#include #include namespace skate3 { bool IsGameInstalled(const std::filesystem::path& game_root); +void ShowRexglueIsoInstallWizard(rex::ui::ImGuiDrawer* drawer, rex::PathConfig runtime_paths, + std::function complete); bool RunRexglueIsoInstallWizardBlocking(rex::ui::WindowedAppContext& app_context, rex::ui::Window* window, rex::ui::ImGuiDrawer* drawer, diff --git a/src/skate3_iso_installer_macos.mm b/src/skate3_iso_installer_macos.mm new file mode 100644 index 0000000..214ead0 --- /dev/null +++ b/src/skate3_iso_installer_macos.mm @@ -0,0 +1,53 @@ +#include +#include + +#include + +#import +#import + +namespace skate3 { + +std::filesystem::path PickIsoFileMacOS() { + @autoreleasepool { + SDL_Window* sdl_window = SDL_GetKeyboardFocus(); + NSWindow* ns_window = nil; + + if (sdl_window) { + SDL_PropertiesID properties = SDL_GetWindowProperties(sdl_window); + ns_window = static_cast( + SDL_GetPointerProperty(properties, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr)); + SDL_RaiseWindow(sdl_window); + } + + [NSApp activateIgnoringOtherApps:YES]; + if (ns_window) { + [ns_window makeKeyAndOrderFront:nil]; + } + + NSOpenPanel* panel = [NSOpenPanel openPanel]; + [panel setTitle:@"Select Skate 3 Xbox 360 ISO"]; + [panel setPrompt:@"Select ISO"]; + [panel setCanChooseFiles:YES]; + [panel setCanChooseDirectories:NO]; + [panel setAllowsMultipleSelection:NO]; + [panel setResolvesAliases:YES]; + UTType* iso_type = [UTType typeWithFilenameExtension:@"iso"]; + if (iso_type) { + [panel setAllowedContentTypes:@[ iso_type ]]; + } + + NSInteger response = [panel runModal]; + std::filesystem::path result; + if (response == NSModalResponseOK) { + NSURL* url = [[panel URLs] firstObject]; + if (url && [url isFileURL]) { + result = std::string([[url path] UTF8String]); + } + } + + return result; + } +} + +} // namespace skate3 diff --git a/third_party/rexglue-sdk b/third_party/rexglue-sdk index 028c1ad..3184592 160000 --- a/third_party/rexglue-sdk +++ b/third_party/rexglue-sdk @@ -1 +1 @@ -Subproject commit 028c1ada911f02d11d08a37ca18e9e99a1f5502e +Subproject commit 3184592404a70a4c1fcd07c8dd6f2494c4bd8b4f