Add macOS Support (#537)

This commit is contained in:
David Chavez
2025-03-14 21:07:07 +01:00
committed by GitHub
parent 91db87632c
commit 25e7b31228
21 changed files with 594 additions and 65 deletions
+14 -8
View File
@@ -13,6 +13,8 @@
#elif defined(__linux__)
#include <unistd.h>
#include <pwd.h>
#elif defined(__APPLE__)
#include "apple/rt64_apple.h"
#endif
constexpr std::u8string_view general_filename = u8"general.json";
@@ -71,7 +73,7 @@ T from_or_default(const json& j, const std::string& key, T default_value) {
else {
ret = default_value;
}
return ret;
}
@@ -129,7 +131,7 @@ namespace recomp {
}
std::filesystem::path zelda64::get_app_folder_path() {
// directly check for portable.txt (windows and native linux binary)
// directly check for portable.txt (windows and native linux binary)
if (std::filesystem::exists("portable.txt")) {
return std::filesystem::current_path();
}
@@ -145,8 +147,8 @@ std::filesystem::path zelda64::get_app_folder_path() {
}
CoTaskMemFree(known_path);
#elif defined(__linux__)
// check for APP_FOLDER_PATH env var used by AppImage
#elif defined(__linux__) || defined(__APPLE__)
// check for APP_FOLDER_PATH env var
if (getenv("APP_FOLDER_PATH") != nullptr) {
return std::filesystem::path{getenv("APP_FOLDER_PATH")};
}
@@ -154,7 +156,11 @@ std::filesystem::path zelda64::get_app_folder_path() {
const char *homedir;
if ((homedir = getenv("HOME")) == nullptr) {
#if defined(__linux__)
homedir = getpwuid(getuid())->pw_dir;
#elif defined(__APPLE__)
homedir = GetHomeDirectory();
#endif
}
if (homedir != nullptr) {
@@ -206,7 +212,7 @@ bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::j
return recomp::finalize_output_file_with_backup(path);
}
bool save_general_config(const std::filesystem::path& path) {
bool save_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
zelda64::to_json(config_json["targeting_mode"], zelda64::get_targeting_mode());
@@ -220,7 +226,7 @@ bool save_general_config(const std::filesystem::path& path) {
config_json["analog_cam_mode"] = zelda64::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = zelda64::get_analog_camera_invert_mode();
config_json["debug_mode"] = zelda64::get_debug_mode_enabled();
return save_json_with_backups(path, config_json);
}
@@ -438,7 +444,7 @@ bool save_sound_config(const std::filesystem::path& path) {
config_json["main_volume"] = zelda64::get_main_volume();
config_json["bgm_volume"] = zelda64::get_bgm_volume();
config_json["low_health_beeps"] = zelda64::get_low_health_beeps_enabled();
return save_json_with_backups(path, config_json);
}
@@ -500,7 +506,7 @@ void zelda64::save_config() {
}
std::filesystem::create_directories(recomp_dir);
// TODO error handling for failing to save config files.
save_general_config(recomp_dir / general_filename);
+9 -2
View File
@@ -25,6 +25,7 @@
#include "zelda_config.h"
#include "zelda_sound.h"
#include "zelda_render.h"
#include "zelda_support.h"
#include "zelda_game.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
@@ -51,7 +52,8 @@ void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
((void)fprintf(stderr, str, args), ...);
assert(false);
std::quick_exit(EXIT_FAILURE);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
@@ -125,7 +127,9 @@ SDL_Window* window;
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
uint32_t flags = SDL_WINDOW_RESIZABLE;
#if defined(RT64_SDL_WINDOW_VULKAN)
#if defined(__APPLE__)
flags |= SDL_WINDOW_METAL;
#elif defined(RT64_SDL_WINDOW_VULKAN)
flags |= SDL_WINDOW_VULKAN;
#endif
@@ -151,6 +155,9 @@ ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::
return ultramodern::renderer::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
#elif defined(__linux__) || defined(__ANDROID__)
return ultramodern::renderer::WindowHandle{ window };
#elif defined(__APPLE__)
SDL_MetalView view = SDL_Metal_CreateView(window);
return ultramodern::renderer::WindowHandle{ wmInfo.info.cocoa.window, SDL_Metal_GetLayer(view) };
#else
static_assert(false && "Unimplemented");
#endif
+3
View File
@@ -263,6 +263,9 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
case ultramodern::renderer::GraphicsApi::Vulkan:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Vulkan;
break;
case ultramodern::renderer::GraphicsApi::Metal:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Metal;
break;
default:
case ultramodern::renderer::GraphicsApi::Auto:
// Don't override if auto is selected.
+58
View File
@@ -0,0 +1,58 @@
#include "zelda_support.h"
#include <SDL.h>
#include "nfd.h"
#include "RmlUi/Core.h"
namespace zelda64 {
// MARK: - Internal Helpers
void perform_file_dialog_operation(const std::function<void(bool, const std::filesystem::path&)>& callback) {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
bool success = (result == NFD_OKAY);
std::filesystem::path path;
if (success) {
path = std::filesystem::path{native_path};
NFD_FreePathN(native_path);
}
callback(success, path);
}
// MARK: - Public API
std::filesystem::path get_asset_path(const char* asset) {
std::filesystem::path base_path = "";
#if defined(__APPLE__)
const char* resource_dir = get_bundle_resource_directory();
base_path = resource_dir;
free((void*)resource_dir);
#endif
return base_path / "assets" / asset;
}
void open_file_dialog(std::function<void(bool success, const std::filesystem::path& path)> callback) {
#ifdef __APPLE__
dispatch_on_ui_thread([callback]() {
perform_file_dialog_operation(callback);
});
#else
perform_file_dialog_operation(callback);
#endif
}
void show_error_message_box(const char *title, const char *message) {
#ifdef __APPLE__
std::string title_copy(title);
std::string message_copy(message);
dispatch_on_ui_thread([title_copy, message_copy] {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title_copy.c_str(), message_copy.c_str(), nullptr);
});
#else
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, message, nullptr);
#endif
}
}
+54
View File
@@ -0,0 +1,54 @@
#include "zelda_support.h"
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>
#include <SDL.h>
#include "nfd.h"
namespace zelda64 {
void dispatch_on_ui_thread(std::function<void()> func) {
dispatch_async(dispatch_get_main_queue(), ^{
func();
});
}
const char* get_bundle_resource_directory() {
NSString *bundlePath = [[NSBundle mainBundle] resourcePath];
return strdup([bundlePath UTF8String]);
}
}
// Used to swizzle the updateDrawableSize method in SDL_cocoametalview to not
// automatically resize the underlying CAMetalLayer when the window size changes.
static void MySwizzleSDLMetalView(void) {
Class cls = objc_getClass("SDL_cocoametalview");
if (!cls) {
// Probably means SDL is using a different name, or the symbol is still hidden.
return;
}
SEL originalSelector = sel_registerName("updateDrawableSize");
SEL swizzledSelector = sel_registerName("my_updateDrawableSize");
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
if (!originalMethod) {
// The method might not exist or might get inlined in some SDL builds.
return;
}
// Implementation of our replacement method
IMP swizzledIMP = imp_implementationWithBlock(^void(id selfObj) {
// (no-op)
});
// Swizzle method
class_addMethod(cls, swizzledSelector, swizzledIMP, method_getTypeEncoding(originalMethod));
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
__attribute__((constructor))
static void PatchSDLMetalViewConstructor() {
// This runs as soon as the dynamic library/executable is loaded, before main().
MySwizzleSDLMetalView();
}
+4 -2
View File
@@ -4,6 +4,7 @@
#include "zelda_config.h"
#include "zelda_debug.h"
#include "zelda_render.h"
#include "zelda_support.h"
#include "promptfont.h"
#include "ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
@@ -519,7 +520,8 @@ public:
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
return context->LoadDocument("assets/config_menu.rml");
const std::filesystem::path asset = zelda64::get_asset_path("config_menu.rml");
return context->LoadDocument(asset.string());
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "apply_options",
@@ -725,7 +727,7 @@ public:
throw std::runtime_error("Failed to make RmlUi data model for the controls config menu");
}
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = recomp::get_num_inputs(); } );
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = static_cast<uint64_t>(recomp::get_num_inputs()); } );
constructor.BindFunc("input_device_is_keyboard", [](Rml::Variant& out) { out = cur_device == recomp::InputDevice::Keyboard; } );
constructor.RegisterTransformFunc("get_input_name", [](const Rml::VariantList& inputs) {
+33 -36
View File
@@ -1,5 +1,6 @@
#include "recomp_ui.h"
#include "zelda_config.h"
#include "zelda_support.h"
#include "librecomp/game.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
@@ -15,41 +16,36 @@ extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
if (result == NFD_OKAY) {
std::filesystem::path path{native_path};
NFD_FreePathN(native_path);
native_path = nullptr;
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
zelda64::open_file_dialog([](bool success, const std::filesystem::path& path) {
if (success) {
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
});
}
class LauncherMenu : public recompui::MenuController {
@@ -61,7 +57,8 @@ public:
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
return context->LoadDocument("assets/launcher.rml");
const std::filesystem::path asset = zelda64::get_asset_path("launcher.rml");
return context->LoadDocument(asset.string());
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
+13 -3
View File
@@ -15,6 +15,7 @@
#include "recomp_input.h"
#include "librecomp/game.hpp"
#include "zelda_config.h"
#include "zelda_support.h"
#include "ui_rml_hacks.hpp"
#include "concurrentqueue.h"
@@ -34,6 +35,9 @@
#ifdef _WIN32
# include "InterfaceVS.hlsl.dxil.h"
# include "InterfacePS.hlsl.dxil.h"
#elif defined(__APPLE__)
# include "InterfaceVS.hlsl.metal.h"
# include "InterfacePS.hlsl.metal.h"
#endif
#ifdef _WIN32
@@ -43,6 +47,13 @@
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::DXIL ? std::size(name##BlobDXIL) : 0)
#elif defined(__APPLE__)
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : \
(format) == RT64::RenderShaderFormat::METAL ? name##BlobMSL : nullptr)
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::METAL ? std::size(name##BlobMSL) : 0)
#else
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : nullptr)
@@ -1144,8 +1155,6 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
Rml::Debugger::Initialise(ui_context->rml.context);
{
const Rml::String directory = "assets/";
struct FontFace {
const char* filename;
bool fallback_face;
@@ -1162,7 +1171,8 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
};
for (const FontFace& face : font_faces) {
Rml::LoadFontFace(directory + face.filename, face.fallback_face);
auto font = zelda64::get_asset_path(face.filename);
Rml::LoadFontFace(font.string(), face.fallback_face);
}
}