Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cocoa Metal example (no SDL). #52

Merged
merged 5 commits into from
Sep 12, 2024
Merged

Cocoa Metal example (no SDL). #52

merged 5 commits into from
Sep 12, 2024

Conversation

yay
Copy link
Contributor

@yay yay commented Aug 20, 2024

  • Have you added the example to the CI at .github/workflows/check.yml?
Screenshot 2024-08-20 at 11 50 53 PM

@yay
Copy link
Contributor Author

yay commented Aug 20, 2024

PR to fix build from failing (vet tabs): odin-lang/Odin#4117

ols.json Outdated
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you remove this file? You may also add it to .gitignore

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, didn't mean to commit it.

@mbger
Copy link

mbger commented Aug 26, 2024

Just took the sample for a spin and here are a few observations. Hope you don't mind me chiming in.

The other samples all have a way of exiting the main loop (e.g. pressing escape to set a quit flag to true). This is missing here. Therefor the compiler does not complain that there's no return statement at the end of metal_main. When I implemented this 'press escape to quit' functionality the program would segfault when exiting metal_main. I think this happens because metal_main sets up a scoped autorelease pool? I.e. when exiting metal_main already-released objects get released again wich causes the segfault.

Also, the program currently fetches just one event per main-loop iteration instead of draining all events from the queue. But this might be intentional?

Those were essentially my modifications:

// `metal_main`
{
    // Setup... (but remove call to NS.scoped_autoreleasepool() to prevent segfault)

    // Main Loop
    for quit := false; !quit;
    {
	// Poll all events from the queue
	for
	{
	    e := app->nextEventMatchingMask(NS.EventMaskAny, nil, NS.DefaultRunLoopMode, true)
	    
	    if e == nil { break }
	    
	    #partial switch e->type()
	    {
		case .KeyDown, .KeyUp:
		code := NS.kVK(e->keyCode())
		#partial switch code {
		    case .Escape:
		    quit = true
		    case .LeftArrow:
		    angle -= 0.02
		    case .RightArrow:
		    angle += 0.02
		    case:
		    fmt.println(code)
		}
		
		case:
		fmt.println(e->locationInWindow(), e->modifierFlags())
		app->sendEvent(e)
	    }
	}

	// Render frame...
    }

    // Exit `metal_main`
    return nil
}

@yay
Copy link
Contributor Author

yay commented Aug 26, 2024

@mbger Thanks! I added the Escape key handling. The reason I used NS.Date.distantFuture() is to keep CPU usage at 0% on idle. If I poll all events and use nil instead, idle CPU usage is quite high.

I wonder if there's a memory leak somewhere? If I press left or right key and hold it for a minute, memory usage for the process will slowly keep increasing. With or without the NS.scoped_autoreleasepool() statement.

@mbger
Copy link

mbger commented Aug 27, 2024

I wonder if there's a memory leak somewhere? If I press left or right key and hold it for a minute, memory usage for the process will slowly keep increasing. With or without the NS.scoped_autoreleasepool() statement.

In your original example, the scoped autorelease pool never has a chance to release anything because the relevant scope never exits. I think what might help is to set up a scoped autorelease pool specifically for the event-polling section of our code (though I can't test now):

    // Main Loop
    for quit := false; !quit;
    {
	// Poll all events from the queue
	{   
	    NS.scoped_autoreleasepool()
	    
	    for
	    {
		e := app->nextEventMatchingMask(NS.EventMaskAny, nil, NS.DefaultRunLoopMode, true)
		
		if e == nil { break }
		
		#partial switch e->type()
		{
		    case .KeyDown, .KeyUp:
		    code := NS.kVK(e->keyCode())
		    #partial switch code {
			case .Escape:
			quit = true
			case .LeftArrow:
			angle -= 0.02
			case .RightArrow:
			angle += 0.02
			case:
			fmt.println(code)
		    }
		    
		    case:
		    fmt.println(e->locationInWindow(), e->modifierFlags())
		}
		app->sendEvent(e)
	    }
	}

	// Render frame

@yay
Copy link
Contributor Author

yay commented Aug 27, 2024

I think what might help is to set up a scoped autorelease pool specifically for the event-polling section of our code

Yes, that's definitely an improvement. Just made that change. It still looks like it might be leaking a little though.

@laytan
Copy link
Collaborator

laytan commented Aug 27, 2024

One thing I've noticed is that pressing the x to close the app just closes the window but doesn't quit the program.

@yay
Copy link
Contributor Author

yay commented Aug 27, 2024

Yes, one needs to create an AppDelegate and implement one of its methods to handle x button behavior. But IDK how to combine that and explicit event loop handling.

@laytan
Copy link
Collaborator

laytan commented Aug 27, 2024

Yes, one needs to create an AppDelegate and implement one of its methods to handle x button behavior. But IDK how to combine that and explicit event loop handling.

That seems like a blocker to me, any application should support properly closing like that

@yay
Copy link
Contributor Author

yay commented Aug 27, 2024

No problem. Should I keep this open or close and reopen when I have an idea how to do that?

I don't have any immediate plans to explore further, the AppDelegate stuff is way over my head and I'm making more progress elsewhere.

@laytan
Copy link
Collaborator

laytan commented Aug 27, 2024

You don't have to close it, maybe somebody else sees it and is able to implement the missing things.

@colrdavidson
Copy link
Member

I don't know how useful it is, but I did something like this a while back
https://gist.github.com/colrdavidson/aecf689196fcae49122749f866780854#file-osx_platform-odin-L190

@mbger
Copy link

mbger commented Aug 28, 2024

I don't have any immediate plans to explore further, the AppDelegate stuff is way over my head and I'm making more progress elsewhere.

I think you're in good company. This stuff is tricky to get right and after doing a bit of research I think every approach will have its drawbacks. See for example this discussion in the sokol project. (AFAICT they don't set up their own main loop but start the NSApp run loop and hook int the main window's drawRect: to render each frame)

As for the immediate PR-blocking issue here (exiting the program when the window is closed): I think we could register a NSWindowDelegate and set our quit flag in windowWillClose: like this:

  1. Make the quit flag global and add the windowWillClose: procedure
// Global scope
quit := false

windowWillClose :: proc (^NS.Notification)
{
    quit = true
}
  1. Register the NSWindowDelegate in our setup code:
    //  metal_main
    window->setDelegate(NS.window_delegate_register_and_alloc({
	windowWillClose = windowWillClose
    }, "MainWindowDelegate", context))

@yay
Copy link
Contributor Author

yay commented Aug 28, 2024

@mbger Thank you! Just pushed those changes. Now there are 4 different ways to close the app. From the menu, via Cmd+Q shortcut, by pressing Escape, and by clicking the x button.

@colrdavidson Thanks! But I don't see where the get_next_event proc is being called?

@yay
Copy link
Contributor Author

yay commented Aug 28, 2024

On a separate note, when pressing keyboard keys, the app makes those beep sounds, as if the action is invalid.

If I don't call app->sendEvent(event) on case .KeyDown, .KeyUp, no beep sounds are made, but then if I press Cmd+Q the app also doesn't exit.

I guess I can handle Cmd+Q separately, there is enough information for me do that, but I wonder if there's a better way to suppress those beep sounds? EDIT: actually, that wouldn't really work, because that would require special handling for all shortcuts, not just Cmd+Q.

@mbger
Copy link

mbger commented Aug 28, 2024

Don't know if that's the way to do it, but what worked for me is to create a custom subclass of NSWindow which overrides keyDown:. Not sure if that approach breaks some other stuff down the road though... (I don't have a ton of experience with macOS dev)

USE_CUSTOM_WINDOW :: true

    // ...

    window: ^NS.Window

    when USE_CUSTOM_WINDOW
    {	
	customWindowClass := NS.objc_allocateClassPair(intrinsics.objc_find_class("NSWindow"), strings.clone_to_cstring("CustomWindow", context.temp_allocator), 0)

	keyDown :: proc "c" (event: ^NS.Event) { /* ignore key down events */ }

	assert(customWindowClass != nil)
	
	NS.class_addMethod(customWindowClass, intrinsics.objc_find_selector("keyDown:"), auto_cast keyDown, "v@:@")
	NS.objc_registerClassPair(customWindowClass)
	keyIgnoringWindow := NS.class_createInstance(customWindowClass, size_of(NS.Window))
	window = cast(^NS.Window)keyIgnoringWindow
    }
    else
    {
	window = NS.Window.alloc()
    }

    defer window->release()

@yay
Copy link
Contributor Author

yay commented Aug 31, 2024

Apparently, one can override the acceptsFirstResponder property to return true: https://stackoverflow.com/questions/13519647/silence-cocoa-error-beep

But I haven't figured out how to do that yet. Is that the same as overriding a method?

There are these ObjC Runtime functions:

class_addProperty           :: proc(cls: Class, name: cstring, attributes: [^]objc_property_attribute_t, attributeCount: uint) -> BOOL ---
class_replaceProperty       :: proc(cls: Class, name: cstring, attributes: [^]objc_property_attribute_t, attributeCount: uint) ---

But I'm reading that you can't actually add properties at runtime: https://stackoverflow.com/questions/7819092/how-can-i-add-properties-to-an-object-at-runtime

So I'm confused. This doesn't make it stop beeping on key press:

CustomWindowClass := NS.objc_allocateClassPair(intrinsics.objc_find_class("NSWindow"), strings.clone_to_cstring("CustomWindow", context.temp_allocator), 0)
assert(CustomWindowClass != nil)
assert(NS.class_getProperty(CustomWindowClass, "acceptsFirstResponder") != nil)
acceptsFirstResponder :: proc "c" () -> NS.BOOL { return NS.YES }
sel := intrinsics.objc_find_selector("acceptsFirstResponder")
assert(NS.class_respondsToSelector(CustomWindowClass, sel) == true)
NS.class_addMethod(CustomWindowClass, sel, auto_cast acceptsFirstResponder, "B@:")
NS.objc_registerClassPair(CustomWindowClass)
window := cast(^NS.Window)(NS.class_createInstance(CustomWindowClass, size_of(NS.Window)))
defer window->release()

In the meantime, I've submitted this as I keep researching: odin-lang/Odin#4171

@mbger
Copy link

mbger commented Sep 2, 2024

Apparently, one can override the acceptsFirstResponder property to return true: https://stackoverflow.com/questions/13519647/silence-cocoa-error-beep

[...]
So I'm confused. This doesn't make it stop beeping on key press:

Maybe the accepted answer is just incorrect and there's nothing wrong with how your Odin code? Have you tried to confirm the accepted answer with Xcode > File > New Project?

@yay
Copy link
Contributor Author

yay commented Sep 2, 2024

Maybe the accepted answer is just incorrect and there's nothing wrong with how your Odin code? Have you tried to confirm the accepted answer with Xcode > File > New Project?

Well, acceptsFirstResponder indeed doesn't work in both Objective-C and Odin.
But performKeyEquivalent does! And key presses are silent.

performKeyEquivalent :: proc "c" (event: ^NS.Event) -> NS.BOOL { return NS.YES }
NS.class_addMethod(CustomWindowClass, intrinsics.objc_find_selector("performKeyEquivalent:"), auto_cast performKeyEquivalent, "B@:@")

However, Cmd+Q stops working.

@mbger
Copy link

mbger commented Sep 3, 2024

Just curious: What are the downsides with the approach I outlined in a previous comment? At least on my end everything seems to work just fine – no beeps and CMD+Q quits the program.

@yay
Copy link
Contributor Author

yay commented Sep 3, 2024

I would prefer not to ignore the key down event, if possible. My intent was to show how to create a macOS GUI that idles with zero CPU usage in the absence of events. And when typing, characters show up on screen on key down event. Though it's probably equally as important for games.

Of course, I can just use your solution (which I'm grateful for btw) just to get this PR to merge. But I wonder if there's something else we can try? This has been a very productive conversation so far.

@yay yay force-pushed the cocoa-metal branch 3 times, most recently from 3e74173 to 2b21d06 Compare September 3, 2024 13:21
@laytan
Copy link
Collaborator

laytan commented Sep 4, 2024

Yep, your gist also doesn't work:

focus.mov

@mbger
Copy link

mbger commented Sep 4, 2024

Thanks! Are you running your browser & terminal/editor in some special windowing mode? Looks a bit like the macOS native fullscreen split mode but w/o the divider in the middle. For me it looks like this:

CleanShot.2024-09-04.at.16.13.12.mp4

@laytan
Copy link
Collaborator

laytan commented Sep 4, 2024

Thanks! Are you running your browser & terminal/editor in some special windowing mode? Looks a bit like the macOS native fullscreen split mode but w/o the divider in the middle. For me it looks like this:

Nothing special going on afaict, I do have Yabai but disabling that doesn't help, my terminal is Alacritty.
It all works as expected with the SDL version and any other app I have tried.

I think we should just put in the deprecated app->activateIgnoringOtherApps(true) since that is what GLFW and Sokol do (I haven't looked at what SDL does).

@mbger
Copy link

mbger commented Sep 4, 2024

I think we should just put in the deprecated app->activateIgnoringOtherApps(true) since that is what GLFW and Sokol do (I haven't looked at what SDL does).

Well... I guess this explains why in your case the app is also half hidden behind alacritty at first and then ordered in front... 😄

CleanShot 2024-09-04 at 16 45 42@2x
See: https://github.com/libsdl-org/SDL/blob/f317581c91961ee628446886dd4df87e9c16ac79/src/video/cocoa/SDL_cocoaevents.m#L328

@yay
Copy link
Contributor Author

yay commented Sep 4, 2024

Weird,

window->makeKeyAndOrderFront(nil)
app->finishLaunching()

is what I tried and it didn't work for me.

But activate did the trick.

I'm on macOS 15.0 Beta (24A5331b), if that matters.

@yay
Copy link
Contributor Author

yay commented Sep 4, 2024

@laytan I replaced app->activate() with app->activateIgnoringOtherApps(true).

@yay
Copy link
Contributor Author

yay commented Sep 4, 2024

Hmm... Screenshot 2024-09-04 at 11 40 21 PM

So what do we do to merge this PR?

@laytan
Copy link
Collaborator

laytan commented Sep 4, 2024

odin-lang/Odin@ddf5ca7 👿

@yay
Copy link
Contributor Author

yay commented Sep 4, 2024

odin-lang/Odin@ddf5ca7 👿

Cool. I think we're good to go now.

@yay
Copy link
Contributor Author

yay commented Sep 5, 2024

This is amazing actually. This has been annoying me for years! On any platform.

In macOS Sonoma, we have introduced Cooperative App Activation. Cooperative app activation reduces unexpected application switches, for example, an app switch while you are in the middle of typing.

There are two parts to cooperative app activation. Activate is now a request, as opposed to a command. The system considers the broader context of what the user is doing to decide if the activate request is appropriate. The new yield API allows an application to influence the context of a future activation request.

Now that activate is a request, the ignoringOtherApps parameter and option are ignored. As such, in macOS Sonoma, the activate(ignoringOtherApps:) function and the activateIgnoringOtherApps option are both deprecated. Replace them with the new activate APIs for NSApplication and NSRunningApplication.

More here: https://developer.apple.com/videos/play/wwdc2023/10054/

So I think we should change back to app->activate().

@laytan
Copy link
Collaborator

laytan commented Sep 5, 2024

Well as we have seen here, it clearly doesn't work well enough yet. I am guessing apps that open others have to implement something (that yield concept) and until they do just an ->activate doesn't work.

@yay
Copy link
Contributor Author

yay commented Sep 5, 2024

Not sure how activateIgnoringOtherApps would work though, if Now that activate is a request, the ignoringOtherApps parameter and option are ignored.

Either way, I'm happy for this to be merged as is too.

@laytan
Copy link
Collaborator

laytan commented Sep 5, 2024

Now that activate is a request, the ignoringOtherApps parameter and option are ignored.

It looks like this is not yet the case. I think it's good to leave like this so it behaves like glfw, SDL, and Sokol does, we can always revisit later.

I will look over the entire pr once more in a bit and merge if there is nothing else, great work here any way!

@laytan
Copy link
Collaborator

laytan commented Sep 5, 2024

Ah man it is not quite there yet, sorry :(

If you minimise the app you can't get it back into focus by clicking it in the dock.

You also can't quit the app by right clicking the dock entry and clicking on quit.

@mbger
Copy link

mbger commented Sep 6, 2024

Ah man it is not quite there yet, sorry :(

If you minimise the app you can't get it back into focus by clicking it in the dock.

You also can't quit the app by right clicking the dock entry and clicking on quit.

Weird that we're experiencing such different behavior.

If I minimize it's still active and receives events. And right-click -> quit from the dock icon quits the program.

@laytan
Copy link
Collaborator

laytan commented Sep 6, 2024

Weird that we're experiencing such different behavior.

Oh that is weird, I assumed this would reproduce for you too. I did confirm that the sdl version worked as expected, but let me investigate some more.

@yay
Copy link
Contributor Author

yay commented Sep 6, 2024

If I minimize it's still active and receives events.

Same here. And I can restore it by clicking the dock icon.

You also can't quit the app by right clicking the dock entry and clicking on quit.

But also same here. Selecting "Quit" from dock icon's context menu does nothing.

Weird that we're experiencing such different behavior.

Indeed.

@yay
Copy link
Contributor Author

yay commented Sep 11, 2024

I managed to find an example in pure C:
https://github.com/EimaMei/Silicon/blob/main/examples/general/events.c
Build with: gcc -g -o0 -Wall examples/general/events.c -I./ -framework Cocoa -o events

(funny enough, it's bases on the Obj-C example I was already using)

But after literal translation to Odin, the Odin app simply doesn't quit from the dock icon menu, whereas the C app does.

Here's my Odin code:

package main

import NS "core:sys/darwin/Foundation"

main :: proc() {
	app := NS.Application.sharedApplication()

	window1 := NS.Window.alloc()->initWithContentRect({{100, 100}, {300, 300}}, { .Titled, .Closable, .Miniaturizable, .Resizable }, .Buffered, false)
	window1->setIsVisible(true)

	app->setActivationPolicy(.Regular)
	app->finishLaunching()

	for {
		NS.scoped_autoreleasepool()
		event := app->nextEventMatchingMask(NS.EventMaskAny, NS.Date.distantFuture(), NS.DefaultRunLoopMode, true)

		if event->type() == .KeyDown && NS.kVK(event->keyCode()) == .ANSI_Q { break }

		app->sendEvent(event)
		app->updateWindows()
	}
}

@laytan
Copy link
Collaborator

laytan commented Sep 11, 2024

That small example and comparison allowed me to debug it and I found the issue, working on a fix.

@yay
Copy link
Contributor Author

yay commented Sep 11, 2024

Is it something to do with objc_msgSend? Or Application not extending NSResponder? I've been trying to figure it out all evening.

@laytan
Copy link
Collaborator

laytan commented Sep 11, 2024

It was because our EventMaskAny was wrong, now fixed through odin-lang/Odin@27ed107 and odin-lang/Odin@201a7b9

@laytan
Copy link
Collaborator

laytan commented Sep 11, 2024

I believe the last thing you need here is a call to app->finishLaunching()

@yay
Copy link
Contributor Author

yay commented Sep 12, 2024

Ah, so the right events were not coming through.

I believe the last thing you need here is a call to app->finishLaunching()

Done, thanks. Indeed, the dock menu Quit didn't work without this one even with the new mask.

@laytan
Copy link
Collaborator

laytan commented Sep 12, 2024

Thanks for sticking with me and all the work here, this is a very nice example to accompany the native win32 example!

@laytan laytan merged commit b88eef8 into odin-lang:master Sep 12, 2024
1 check passed
@mbger
Copy link

mbger commented Sep 13, 2024

Nice, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants