[macOS] Replace custom main loop with [NSApp run] and CFRunLoop observer.

This commit is contained in:
Pāvels Nadtočajevs 2025-03-20 09:21:36 +02:00
parent 0595bb8a42
commit a317ce75a6
No known key found for this signature in database
GPG Key ID: 8413210218EF35D2
7 changed files with 167 additions and 126 deletions

View File

@ -441,6 +441,7 @@ public:
virtual Key keyboard_get_label_from_physical(Key p_keycode) const override; virtual Key keyboard_get_label_from_physical(Key p_keycode) const override;
virtual void show_emoji_and_symbol_picker() const override; virtual void show_emoji_and_symbol_picker() const override;
void _process_events(bool p_pump);
virtual void process_events() override; virtual void process_events() override;
virtual void force_process_and_drop_events() override; virtual void force_process_and_drop_events() override;

View File

@ -1901,6 +1901,11 @@ DisplayServer::WindowID DisplayServerMacOS::create_sub_window(WindowMode p_mode,
void DisplayServerMacOS::show_window(WindowID p_id) { void DisplayServerMacOS::show_window(WindowID p_id) {
WindowData &wd = windows[p_id]; WindowData &wd = windows[p_id];
if (p_id == MAIN_WINDOW_ID) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
static_cast<OS_MacOS *>(OS::get_singleton())->activate();
}
popup_open(p_id); popup_open(p_id);
if ([wd.window_object isMiniaturized]) { if ([wd.window_object isMiniaturized]) {
return; return;
@ -3204,8 +3209,13 @@ void DisplayServerMacOS::show_emoji_and_symbol_picker() const {
} }
void DisplayServerMacOS::process_events() { void DisplayServerMacOS::process_events() {
_process_events(true);
}
void DisplayServerMacOS::_process_events(bool p_pump) {
ERR_FAIL_COND(!Thread::is_main_thread()); ERR_FAIL_COND(!Thread::is_main_thread());
if (p_pump) {
while (true) { while (true) {
NSEvent *event = [NSApp NSEvent *event = [NSApp
nextEventMatchingMask:NSEventMaskAny nextEventMatchingMask:NSEventMaskAny
@ -3219,6 +3229,7 @@ void DisplayServerMacOS::process_events() {
[NSApp sendEvent:event]; [NSApp sendEvent:event];
} }
}
// Process "menu_callback"s. // Process "menu_callback"s.
while (List<MenuCall>::Element *call_p = deferred_menu_calls.front()) { while (List<MenuCall>::Element *call_p = deferred_menu_calls.front()) {

View File

@ -36,8 +36,8 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@interface GodotApplicationDelegate : NSObject <NSUserInterfaceItemSearching, NSApplicationDelegate> @interface GodotApplicationDelegate : NSObject <NSUserInterfaceItemSearching, NSApplicationDelegate>
- (void)activate;
- (void)forceUnbundledWindowActivationHackStep1; - (void)forceUnbundledWindowActivationHackStep1;
- (void)forceUnbundledWindowActivationHackStep2; - (void)forceUnbundledWindowActivationHackStep2;
- (void)forceUnbundledWindowActivationHackStep3; - (void)forceUnbundledWindowActivationHackStep3;
- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
@end @end

View File

@ -124,7 +124,13 @@
} }
} }
- (void)applicationDidFinishLaunching:(NSNotification *)notice { - (void)applicationDidFinishLaunching:(NSNotification *)notification {
static_cast<OS_MacOS *>(OS::get_singleton())->start_main();
}
- (void)activate {
[NSApp activateIgnoringOtherApps:YES];
NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"];
const char *bundled_id = getenv("__CFBundleIdentifier"); const char *bundled_id = getenv("__CFBundleIdentifier");
NSString *nsbundleid_env = [NSString stringWithUTF8String:(bundled_id != nullptr) ? bundled_id : ""]; NSString *nsbundleid_env = [NSString stringWithUTF8String:(bundled_id != nullptr) ? bundled_id : ""];
@ -139,11 +145,6 @@
- (id)init { - (id)init {
self = [super init]; self = [super init];
NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
[aem setEventHandler:self andSelector:@selector(handleAppleEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
[aem setEventHandler:self andSelector:@selector(handleAppleEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEOpenDocuments];
return self; return self;
} }
@ -152,36 +153,45 @@
[[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:@"AppleColorPreferencesChangedNotification" object:nil]; [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:@"AppleColorPreferencesChangedNotification" object:nil];
} }
- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { - (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
OS_MacOS *os = (OS_MacOS *)OS::get_singleton(); OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
if (!event || !os) { if (!os) {
return; return;
} }
List<String> args; List<String> args;
if (([event eventClass] == kInternetEventClass) && ([event eventID] == kAEGetURL)) { for (NSURL *url in urls) {
// Opening URL scheme. if ([url isFileURL]) {
NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
args.push_back(vformat("--uri=\"%s\"", String::utf8([url UTF8String])));
}
if (([event eventClass] == kCoreEventClass) && ([event eventID] == kAEOpenDocuments)) {
// Opening file association.
NSAppleEventDescriptor *files = [event paramDescriptorForKeyword:keyDirectObject];
if (files) {
NSInteger count = [files numberOfItems];
for (NSInteger i = 1; i <= count; i++) {
NSURL *url = [NSURL URLWithString:[[files descriptorAtIndex:i] stringValue]];
args.push_back(String::utf8([url.path UTF8String])); args.push_back(String::utf8([url.path UTF8String]));
} else {
args.push_back(vformat("--uri=\"%s\"", String::utf8([url.absoluteString UTF8String])));
} }
} }
}
if (!args.is_empty()) { if (!args.is_empty()) {
if (os->get_main_loop()) { if (os->get_main_loop()) {
// Application is already running, open a new instance with the URL/files as command line arguments. // Application is already running, open a new instance with the URL/files as command line arguments.
os->create_instance(args); os->create_instance(args);
} else { } else if (os->get_cmd_argc() == 0) {
// Application is just started, add to the list of command line arguments and continue.
os->set_cmdline_platform_args(args);
}
}
}
- (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames {
OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
if (!os) {
return;
}
List<String> args;
for (NSString *filename in filenames) {
NSURL *url = [NSURL URLWithString:filename];
args.push_back(String::utf8([url.path UTF8String]));
}
if (!args.is_empty()) {
if (os->get_main_loop()) {
// Application is already running, open a new instance with the URL/files as command line arguments.
os->create_instance(args);
} else if (os->get_cmd_argc() == 0) {
// Application is just started, add to the list of command line arguments and continue. // Application is just started, add to the list of command line arguments and continue.
os->set_cmdline_platform_args(args); os->set_cmdline_platform_args(args);
} }
@ -220,11 +230,23 @@
} }
} }
- (void)applicationWillTerminate:(NSNotification *)notification {
OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
if (os) {
os->cleanup();
exit(os->get_exit_code());
}
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton();
if (ds) { if (ds) {
ds->send_window_event(ds->get_window(DisplayServerMacOS::MAIN_WINDOW_ID), DisplayServerMacOS::WINDOW_EVENT_CLOSE_REQUEST); ds->send_window_event(ds->get_window(DisplayServerMacOS::MAIN_WINDOW_ID), DisplayServerMacOS::WINDOW_EVENT_CLOSE_REQUEST);
} }
OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
if (!os || os->os_should_terminate()) {
return NSTerminateNow;
}
return NSTerminateCancel; return NSTerminateCancel;
} }

View File

@ -59,36 +59,12 @@ int main(int argc, char **argv) {
} }
} }
OS_MacOS os; OS_MacOS os(argv[0], argc - first_arg, &argv[first_arg]);
Error err;
// We must override main when testing is enabled. // We must override main when testing is enabled.
TEST_MAIN_OVERRIDE TEST_MAIN_OVERRIDE
@autoreleasepool { os.run(); // Note: This function will never return.
err = Main::setup(argv[0], argc - first_arg, &argv[first_arg]);
}
if (err != OK) {
if (err == ERR_HELP) { // Returned by --help and --version, so success.
return EXIT_SUCCESS;
}
return EXIT_FAILURE;
}
int ret;
@autoreleasepool {
ret = Main::start();
}
if (ret == EXIT_SUCCESS) {
os.run();
} else {
os.set_exit_code(EXIT_FAILURE);
}
@autoreleasepool {
Main::cleanup();
}
return os.get_exit_code(); return os.get_exit_code();
} }

View File

@ -40,6 +40,13 @@
#include "servers/audio_server.h" #include "servers/audio_server.h"
class OS_MacOS : public OS_Unix { class OS_MacOS : public OS_Unix {
const char *execpath = nullptr;
int argc = 0;
char **argv = nullptr;
id delegate = nullptr;
bool should_terminate = false;
JoypadApple *joypad_apple = nullptr; JoypadApple *joypad_apple = nullptr;
#ifdef COREAUDIO_ENABLED #ifdef COREAUDIO_ENABLED
@ -51,7 +58,7 @@ class OS_MacOS : public OS_Unix {
CrashHandler crash_handler; CrashHandler crash_handler;
CFRunLoopObserverRef pre_wait_observer; CFRunLoopObserverRef pre_wait_observer = nil;
MainLoop *main_loop = nullptr; MainLoop *main_loop = nullptr;
@ -64,6 +71,8 @@ class OS_MacOS : public OS_Unix {
static _FORCE_INLINE_ String get_framework_executable(const String &p_path); static _FORCE_INLINE_ String get_framework_executable(const String &p_path);
static void pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context); static void pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context);
void terminate();
protected: protected:
virtual void initialize_core() override; virtual void initialize_core() override;
virtual void initialize() override; virtual void initialize() override;
@ -131,8 +140,13 @@ public:
virtual String get_system_ca_certificates() override; virtual String get_system_ca_certificates() override;
virtual OS::PreferredTextureFormat get_preferred_texture_format() const override; virtual OS::PreferredTextureFormat get_preferred_texture_format() const override;
void run(); void run(); // Runs macOS native event loop.
void start_main(); // Initializes and runs Godot main loop.
void activate();
void cleanup();
bool os_should_terminate() const { return should_terminate; }
int get_cmd_argc() const { return argc; }
OS_MacOS(); OS_MacOS(const char *p_execpath, int p_argc, char **p_argv);
~OS_MacOS(); ~OS_MacOS();
}; };

View File

@ -47,14 +47,20 @@
#include <sys/sysctl.h> #include <sys/sysctl.h>
void OS_MacOS::pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context) { void OS_MacOS::pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context) {
// Prevent main loop from sleeping and redraw window during modal popup display. OS_MacOS *os = static_cast<OS_MacOS *>(OS::get_singleton());
// Do not redraw when rendering is done from the separate thread, it will conflict with the OpenGL context updates.
DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); @autoreleasepool {
if (get_singleton()->get_main_loop() && ds && !get_singleton()->is_separate_thread_rendering_enabled() && !ds->get_is_resizing()) { @try {
Main::force_redraw(); if (DisplayServer::get_singleton()) {
if (!Main::is_iterating()) { // Avoid cyclic loop. static_cast<DisplayServerMacOS *>(DisplayServer::get_singleton())->_process_events(false); // Get rid of pending events.
Main::iteration(); }
os->joypad_apple->process_joypads();
if (Main::iteration()) {
os->terminate();
}
} @catch (NSException *exception) {
ERR_PRINT("NSException: " + String::utf8([exception reason].UTF8String));
} }
} }
@ -823,36 +829,69 @@ OS::PreferredTextureFormat OS_MacOS::get_preferred_texture_format() const {
} }
void OS_MacOS::run() { void OS_MacOS::run() {
if (!main_loop) { [NSApp run];
return; }
void OS_MacOS::start_main() {
Error err;
@autoreleasepool {
err = Main::setup(execpath, argc, argv);
} }
if (err == OK) {
int ret;
@autoreleasepool {
ret = Main::start();
}
if (ret == EXIT_SUCCESS) {
if (main_loop) {
@autoreleasepool { @autoreleasepool {
main_loop->initialize(); main_loop->initialize();
} }
pre_wait_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, &pre_wait_observer_cb, nullptr);
bool quit = false; CFRunLoopAddObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes);
while (!quit) { return;
@autoreleasepool {
@try {
if (DisplayServer::get_singleton()) {
DisplayServer::get_singleton()->process_events(); // Get rid of pending events.
}
joypad_apple->process_joypads();
if (Main::iteration()) {
quit = true;
}
} @catch (NSException *exception) {
ERR_PRINT("NSException: " + String::utf8([exception reason].UTF8String));
} }
} else {
set_exit_code(EXIT_FAILURE);
} }
} else if (err == ERR_HELP) { // Returned by --help and --version, so success.
set_exit_code(EXIT_SUCCESS);
} else {
set_exit_code(EXIT_FAILURE);
} }
main_loop->finalize(); terminate();
} }
OS_MacOS::OS_MacOS() { void OS_MacOS::activate() {
[delegate activate];
}
void OS_MacOS::terminate() {
if (pre_wait_observer) {
CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes);
CFRelease(pre_wait_observer);
pre_wait_observer = nil;
}
should_terminate = true;
[NSApp terminate:nil];
}
void OS_MacOS::cleanup() {
if (main_loop) {
main_loop->finalize();
@autoreleasepool {
Main::cleanup();
}
}
}
OS_MacOS::OS_MacOS(const char *p_execpath, int p_argc, char **p_argv) {
execpath = p_execpath;
argc = p_argc;
argv = p_argv;
if (is_sandboxed()) { if (is_sandboxed()) {
// Load security-scoped bookmarks, request access, remove stale or invalid bookmarks. // Load security-scoped bookmarks, request access, remove stale or invalid bookmarks.
NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
@ -886,7 +925,7 @@ OS_MacOS::OS_MacOS() {
[GodotApplication sharedApplication]; [GodotApplication sharedApplication];
// In case we are unbundled, make us a proper UI application. // In case we are unbundled, make us a proper UI application.
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
// Menu bar setup must go between sharedApplication above and // Menu bar setup must go between sharedApplication above and
// finishLaunching below, in order to properly emulate the behavior // finishLaunching below, in order to properly emulate the behavior
@ -894,35 +933,13 @@ OS_MacOS::OS_MacOS() {
NSMenu *main_menu = [[NSMenu alloc] initWithTitle:@""]; NSMenu *main_menu = [[NSMenu alloc] initWithTitle:@""];
[NSApp setMainMenu:main_menu]; [NSApp setMainMenu:main_menu];
[NSApp finishLaunching];
id delegate = [[GodotApplicationDelegate alloc] init]; delegate = [[GodotApplicationDelegate alloc] init];
ERR_FAIL_NULL(delegate); ERR_FAIL_NULL(delegate);
[NSApp setDelegate:delegate]; [NSApp setDelegate:delegate];
[NSApp registerUserInterfaceItemSearchHandler:delegate]; [NSApp registerUserInterfaceItemSearchHandler:delegate];
pre_wait_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, &pre_wait_observer_cb, nullptr);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes);
// Process application:openFile: event.
while (true) {
NSEvent *event = [NSApp
nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (event == nil) {
break;
}
[NSApp sendEvent:event];
}
[NSApp activateIgnoringOtherApps:YES];
} }
OS_MacOS::~OS_MacOS() { OS_MacOS::~OS_MacOS() {
CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); // NOP
CFRelease(pre_wait_observer);
} }