[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 void show_emoji_and_symbol_picker() const override;
void _process_events(bool p_pump);
virtual void process_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) {
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);
if ([wd.window_object isMiniaturized]) {
return;
@ -3204,20 +3209,26 @@ void DisplayServerMacOS::show_emoji_and_symbol_picker() const {
}
void DisplayServerMacOS::process_events() {
_process_events(true);
}
void DisplayServerMacOS::_process_events(bool p_pump) {
ERR_FAIL_COND(!Thread::is_main_thread());
while (true) {
NSEvent *event = [NSApp
nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (p_pump) {
while (true) {
NSEvent *event = [NSApp
nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (event == nil) {
break;
if (event == nil) {
break;
}
[NSApp sendEvent:event];
}
[NSApp sendEvent:event];
}
// Process "menu_callback"s.

View File

@ -36,8 +36,8 @@
#import <Foundation/Foundation.h>
@interface GodotApplicationDelegate : NSObject <NSUserInterfaceItemSearching, NSApplicationDelegate>
- (void)activate;
- (void)forceUnbundledWindowActivationHackStep1;
- (void)forceUnbundledWindowActivationHackStep2;
- (void)forceUnbundledWindowActivationHackStep3;
- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
@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"];
const char *bundled_id = getenv("__CFBundleIdentifier");
NSString *nsbundleid_env = [NSString stringWithUTF8String:(bundled_id != nullptr) ? bundled_id : ""];
@ -139,11 +145,6 @@
- (id)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;
}
@ -152,36 +153,45 @@
[[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();
if (!event || !os) {
if (!os) {
return;
}
List<String> args;
if (([event eventClass] == kInternetEventClass) && ([event eventID] == kAEGetURL)) {
// Opening URL scheme.
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]));
}
for (NSURL *url in urls) {
if ([url isFileURL]) {
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 (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 {
} 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.
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 {
DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton();
if (ds) {
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;
}

View File

@ -59,36 +59,12 @@ int main(int argc, char **argv) {
}
}
OS_MacOS os;
Error err;
OS_MacOS os(argv[0], argc - first_arg, &argv[first_arg]);
// We must override main when testing is enabled.
TEST_MAIN_OVERRIDE
@autoreleasepool {
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();
}
os.run(); // Note: This function will never return.
return os.get_exit_code();
}

View File

@ -40,6 +40,13 @@
#include "servers/audio_server.h"
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;
#ifdef COREAUDIO_ENABLED
@ -51,7 +58,7 @@ class OS_MacOS : public OS_Unix {
CrashHandler crash_handler;
CFRunLoopObserverRef pre_wait_observer;
CFRunLoopObserverRef pre_wait_observer = nil;
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 void pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context);
void terminate();
protected:
virtual void initialize_core() override;
virtual void initialize() override;
@ -131,8 +140,13 @@ public:
virtual String get_system_ca_certificates() 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();
};

View File

@ -47,14 +47,20 @@
#include <sys/sysctl.h>
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.
// Do not redraw when rendering is done from the separate thread, it will conflict with the OpenGL context updates.
OS_MacOS *os = static_cast<OS_MacOS *>(OS::get_singleton());
DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton();
if (get_singleton()->get_main_loop() && ds && !get_singleton()->is_separate_thread_rendering_enabled() && !ds->get_is_resizing()) {
Main::force_redraw();
if (!Main::is_iterating()) { // Avoid cyclic loop.
Main::iteration();
@autoreleasepool {
@try {
if (DisplayServer::get_singleton()) {
static_cast<DisplayServerMacOS *>(DisplayServer::get_singleton())->_process_events(false); // Get rid of pending events.
}
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() {
if (!main_loop) {
return;
}
@autoreleasepool {
main_loop->initialize();
}
bool quit = false;
while (!quit) {
@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));
}
}
}
main_loop->finalize();
[NSApp run];
}
OS_MacOS::OS_MacOS() {
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 {
main_loop->initialize();
}
pre_wait_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, &pre_wait_observer_cb, nullptr);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes);
return;
}
} 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);
}
terminate();
}
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()) {
// Load security-scoped bookmarks, request access, remove stale or invalid bookmarks.
NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
@ -886,7 +925,7 @@ OS_MacOS::OS_MacOS() {
[GodotApplication sharedApplication];
// 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
// finishLaunching below, in order to properly emulate the behavior
@ -894,35 +933,13 @@ OS_MacOS::OS_MacOS() {
NSMenu *main_menu = [[NSMenu alloc] initWithTitle:@""];
[NSApp setMainMenu:main_menu];
[NSApp finishLaunching];
id delegate = [[GodotApplicationDelegate alloc] init];
delegate = [[GodotApplicationDelegate alloc] init];
ERR_FAIL_NULL(delegate);
[NSApp setDelegate: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() {
CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes);
CFRelease(pre_wait_observer);
// NOP
}