Skip to content

macOS: Embedded window support.#105884

Merged
Repiteo merged 1 commit intogodotengine:masterfrom
stuartcarnie:macos_embedded
May 6, 2025
Merged

macOS: Embedded window support.#105884
Repiteo merged 1 commit intogodotengine:masterfrom
stuartcarnie:macos_embedded

Conversation

@stuartcarnie
Copy link
Copy Markdown
Contributor

@stuartcarnie stuartcarnie commented Apr 28, 2025

Embedded window support for macOS.

This feature is implemented using CARemoteLayer, which is similar to how we implemented it for OpenEmu. This works by creating a CALayer in the child process and passing the CAContextID back to the Godot Editor. The editor is then able to create an instance of CARemoteLayer and display the rendered output in real-time, as if it was hosted in the app. A major difference is that we serialise the events from the host app, the Godot Editor, and process them in the running game.

The following additional state / events are processed correctly:

  • Joystick add / remove events must be sent to the child process
Joystick.mp4
  • Joystick rumble timestamps (can't demonstrate that in a video)
  • DisplayDriver events
    • cursor changes within embedded window
Cursor.Shape.mp4
  • IME events
IME.mp4
  • OpenGL Support, selection, panning, and more
OpenGL.and.more.mp4
  • Mouse Capture and ⌥+ESC shortcut, always available to the editor, to release capture
Mouse.Capture.mp4

@stuartcarnie stuartcarnie requested review from a team as code owners April 28, 2025 21:07
@stuartcarnie stuartcarnie requested a review from a team as a code owner April 28, 2025 21:09
@akien-mga akien-mga added this to the 4.x milestone Apr 28, 2025
@bruvzg bruvzg self-requested a review April 29, 2025 05:23
@stuartcarnie stuartcarnie force-pushed the macos_embedded branch 3 times, most recently from 2b57725 to 44ec980 Compare April 29, 2025 07:40
@bruvzg
Copy link
Copy Markdown
Member

bruvzg commented Apr 29, 2025

DisplayDriver events
cursor changes within embedded window
others?

IME activation and position events (:window_set_ime_*) should be sent from embedded DS to the host and back (NOTIFICATION_OS_IME_UPDATE and preedit string) to properly work. Currenlty it is never actvated for the embedded process.

@bruvzg
Copy link
Copy Markdown
Member

bruvzg commented Apr 29, 2025

So far the only issues I have found, embedded view is not hiden when switching tabs from game tab (if floating window is diabled). And IME not workitng (expected since events are not passed).

Screen.Recording.2025-04-29.at.11.26.29.mov

@stuartcarnie
Copy link
Copy Markdown
Contributor Author

So far the only issues I have found, embedded view is not hiden when switching tabs from game tab (if floating window is diabled). And IME not workitng (expected since events are not passed).

Screen.Recording.2025-04-29.at.11.26.29.mov

That's great – I must have recently broken the tab switching, as I did fix that previously 🙄 Thanks for catching that, I'll push a fix!

@stuartcarnie
Copy link
Copy Markdown
Contributor Author

@bruvzg I've fixed the panel hiding issue

Copy link
Copy Markdown
Contributor Author

@stuartcarnie stuartcarnie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Various codecs for serialising and deserialising input events

Copy link
Copy Markdown
Member

@KoBeWi KoBeWi May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that debugger_marshalls.cpp have serialize_key_shortcut() method, which is something similar. The new codecs could replace that probably.

EDIT:
Also you are encoding things into binary data. You could just use var_to_bytes(), though it's probably less efficient (if that matters).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also you are encoding things into binary data. You could just use var_to_bytes(), though it's probably less efficient (if that matters).

Efficiency was a concern here, as we're sending a rapid stream of events to the child process, such as mouse movement. I did look at encoding variants but favoured a more efficient approach to keep latency to a minimum.

Note that debugger_marshalls.cpp have serialize_key_shortcut() method, which is something similar. The new codecs could replace that probably.

@KoBeWi I will move the encode / decode functions to DebuggerMarshalls. Wasn't aware of those, but it is an obvious place to put them. Thanks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small change to make polling a little more efficient.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the started signal to when the PID is set, as it was causing a race condition with the embedded process, which raised the started before it had received its PID, so it wasn't able to communicate with the child process

Comment on lines +7100 to +7107
#ifdef MACOS_ENABLED
extern "C" GameViewPluginBase *get_game_view_plugin();
#else
GameViewPluginBase *get_game_view_plugin() {
return memnew(GameViewPlugin);
}
#endif

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are recommendations for a better way to switch out the GameViewPlugin for macOS, I'm all ears.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can move this plugin to the DisplayServer entirely:

A plugin instance can be created and returned by the DisplayServer method. Windows and X11 overrides return base type. And macOS its special one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a look at that - would be a nice approach

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be used for future implementations that want to serialise events across processes.

Comment on lines -48 to -49
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
- (void)accessibilityDisplayOptionsChange:(NSNotification *)notification;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved these into the mm so they are hidden from the public interface

Comment on lines -45 to -47
- (void)forceUnbundledWindowActivationHackStep1;
- (void)forceUnbundledWindowActivationHackStep2;
- (void)forceUnbundledWindowActivationHackStep3;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved these to the GodotApplication class, as I was exploring removing NSApplication loop completely from embedded child process, as it may not be needed.

Comment on lines -39 to -42
bool high_contrast;
bool reduce_motion;
bool reduce_transparency;
bool voice_over;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved these to the .mm file, as they don't need to be in the public interface.

Comment on lines +67 to +72
if (strcmp("--os-debug", argv[i]) == 0) {
i++;
wait_for_debugger = 5000; // wait 5 seconds by default
if (i < argc && strncmp(argv[i], "--", 2) != 0) {
wait_for_debugger = atoi(argv[i]);
i++;
}
continue;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For macOS, we can added a --os-debug <ms> run argument, so you can have it wait for a debugger and attach to debug the entire startup process when the Godot editor launches the child process, which was very handy for this work (and many other things I've done in the past)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should be in ifdef DEBUG_ENABLED block.

String _get_default_fontname(const String &p_font_name) const;

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);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to a block.

@stuartcarnie stuartcarnie force-pushed the macos_embedded branch 2 times, most recently from b14b6df to affb92c Compare April 29, 2025 20:43
Comment on lines +7100 to +7107
#ifdef MACOS_ENABLED
extern "C" GameViewPluginBase *get_game_view_plugin();
#else
GameViewPluginBase *get_game_view_plugin() {
return memnew(GameViewPlugin);
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can move this plugin to the DisplayServer entirely:

A plugin instance can be created and returned by the DisplayServer method. Windows and X11 overrides return base type. And macOS its special one.

Comment on lines +67 to +72
if (strcmp("--os-debug", argv[i]) == 0) {
i++;
wait_for_debugger = 5000; // wait 5 seconds by default
if (i < argc && strncmp(argv[i], "--", 2) != 0) {
wait_for_debugger = atoi(argv[i]);
i++;
}
continue;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should be in ifdef DEBUG_ENABLED block.

Comment on lines +867 to +868
r_arguments.push_back("--rendering-driver");
r_arguments.push_back("metal");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this, maybe it's better to check renderer and disable embedding if it's GL for now. Silently switching a running project to a different renderer is not what most users will expect.

@nubels
Copy link
Copy Markdown
Contributor

nubels commented Apr 30, 2025

First of all, thanks a ton for implementing this feature and opening this pr.

I was very eager to test it and I think I've found some issues:

  • You can't resize the game window if you drag your mouse over it, you have to move the mouse around the window and then it works.
    CleanShot 2025-04-30 at 12 09 07

  • After clicking outside of the game window, it sometimes registers input that isn't there. In this clip, I stop pressing A around the 5s mark and my character still moves to the left. After pressing A again, it seems to stop. I'm using Input.get_vector

CleanShot 2025-04-30 at 12 07 26

I hope this helps :)

@stuartcarnie stuartcarnie force-pushed the macos_embedded branch 2 times, most recently from 4bf9a01 to 35cb8f7 Compare May 1, 2025 07:31
@stuartcarnie
Copy link
Copy Markdown
Contributor Author

@bruvzg OpenGL now works, so I've removed the explicit --rendering-device argument.

@stuartcarnie stuartcarnie force-pushed the macos_embedded branch 2 times, most recently from dd3bce0 to 604072c Compare May 1, 2025 07:41
@KoBeWi
Copy link
Copy Markdown
Member

KoBeWi commented May 5, 2025

You might've missed #105884 (comment)

@stuartcarnie
Copy link
Copy Markdown
Contributor Author

You might've missed #105884 (comment)

Thanks!

@stuartcarnie
Copy link
Copy Markdown
Contributor Author

@Calinou

I noticed the CLI arguments used by the embedded process are --display-driver embedded --embedded. Can --embedded imply --display-driver embedded so you don't need to use both?

💯 agree. I've made that change.

@Repiteo Repiteo merged commit aa24e3b into godotengine:master May 6, 2025
20 checks passed
@Repiteo
Copy link
Copy Markdown
Contributor

Repiteo commented May 6, 2025

Shoot, this got included in the merge batch erroneously. It should be fine as-is, but I meant to hold off for some more maintainer approvals & wait for the followup fixes.

@stuartcarnie Those can go into an immediate followup PR, my apologies

@akien-mga
Copy link
Copy Markdown
Member

akien-mga commented May 6, 2025

Heh, thankfully it had time to be well-reviewed and polished in the past couple of days, so it was 99% ready to merge already.

Let's celebrate this huge achievement and long-awaited feature and not just the merge process mishap :P 🎉

Thanks for the amazing work @stuartcarnie 🥇

@stuartcarnie
Copy link
Copy Markdown
Contributor Author

No stress, @Repiteo! I do have some follow up fixes that I'll prep today in a new PR!

Thanks @akien-mga and to all those that reviewed.

@Calinou my changes will include preventing embedding in single-window mode.

@stuartcarnie
Copy link
Copy Markdown
Contributor Author

@Repiteo see #106134 with some initial follow up work to resolve some of @Calinou's issues.

@stuartcarnie
Copy link
Copy Markdown
Contributor Author

stuartcarnie commented May 6, 2025

Remaining items to investigate (no included in #106134):

  • Clicking the X button to close the floating game window has no effect.
  • Closing the editor using Ctrl + C in the terminal won't always stop the running project
  • In Compatibility, using window embedding will break V-Sync (FPS will be uncapped).
  • When using Metal in Truck Town (Forward+/Mobile), the sky takes the background color of the game window

akien-mga added a commit to akien-mga/godot that referenced this pull request May 13, 2025
akien-mga added a commit that referenced this pull request May 13, 2025
lalitshankarch pushed a commit to lalitshankarch/godot that referenced this pull request May 13, 2025
PiCode9560 pushed a commit to PiCode9560/godot that referenced this pull request May 13, 2025
vbettaque pushed a commit to vbettaque/godot that referenced this pull request May 14, 2025
harrisyu pushed a commit to harrisyu/godot that referenced this pull request May 16, 2025
goatchurchprime pushed a commit to goatchurchprime/godot that referenced this pull request Jun 10, 2025
dsmtE pushed a commit to dsmtE/godot that referenced this pull request Jun 17, 2025
spanzeri pushed a commit to spanzeri/godot that referenced this pull request Jun 25, 2025
dawdle-deer pushed a commit to dawdle-deer/godot that referenced this pull request Jul 3, 2025
Haydoggo pushed a commit to Haydoggo/godot that referenced this pull request Jul 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for game window embedding on macOS

9 participants