-
-
Notifications
You must be signed in to change notification settings - Fork 49
Add opt-in support for SDL's main callbacks #550
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
base: master
Are you sure you want to change the base?
Changes from 8 commits
278e0a8
275b4b5
92e0396
ba79774
5d5f340
ae73d64
5f34811
b422fad
3309e77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| /* | ||
| Cute Framework | ||
| Copyright (C) 2024 Randy Gaul https://randygaul.github.io/ | ||
|
|
||
| This software is dual-licensed with zlib or Unlicense, check LICENSE.txt for more info | ||
| */ | ||
|
|
||
| #ifndef CF_MAIN_H | ||
| #define CF_MAIN_H | ||
|
|
||
| // Deliberately not included by cute.h -- the entry-point glue below has external linkage and must | ||
| // live in exactly one translation unit, so including this header is itself the opt-in. Include it | ||
| // alongside cute.h in the one file that owns your app's entry point, not anywhere else. | ||
|
|
||
| #if defined(CF_MAIN) | ||
| # error "cute_main.h: define either CF_MAIN or include cute_main.h, not both -- they are two different entry points." | ||
| #endif | ||
|
|
||
| // If something already pulled in <SDL3/SDL_main.h>, its include guard would silently swallow the | ||
| // callback machinery below and your program would link with no `main` at all. Catch it here | ||
| // instead: include cute_main.h before any other header that reaches SDL_main.h. | ||
| #ifdef SDL_main_h_ | ||
| # error "cute_main.h: <SDL3/SDL_main.h> was already included. Include cute_main.h first, before any header that reaches it." | ||
| #endif | ||
|
|
||
| #define SDL_MAIN_USE_CALLBACKS | ||
| #include "cute_app.h" | ||
| #include <SDL3/SDL_main.h> | ||
| #include <SDL3/SDL_log.h> | ||
|
|
||
| //-------------------------------------------------------------------------------------------------- | ||
| // C API | ||
|
|
||
| #ifdef __cplusplus | ||
| extern "C" { | ||
| #endif // __cplusplus | ||
|
|
||
| /** | ||
| * @function cf_main_init | ||
| * @category app | ||
| * @brief Your app's startup function -- implement it and call `cf_make_app` inside. | ||
| * @param argc The argument count, forwarded from the entry point. | ||
| * @param argv The argument vector, forwarded from the entry point. Pass `argv[0]` along to `cf_make_app`. | ||
| * @return Return the `CF_Result` from `cf_make_app` (or your own error). An error result aborts startup. | ||
| * @remarks Include `cute_main.h` in exactly one source file to let the host drive your main loop instead of | ||
| * writing your own `while` loop -- don't define `main` yourself, implement `cf_main_init`, | ||
| * `cf_main_update` and `cf_main_quit` instead. All three must live in this one file: in C++ they | ||
| * are only declared here, so defining one in a file that lacks this include gives it C++ linkage | ||
| * and you get an undefined-symbol error at link time. | ||
| * | ||
| * Called once before the first `cf_main_update`. Your `cf_make_app` call must include | ||
| * `CF_APP_OPTIONS_MAIN_CALLBACKS_BIT` -- startup fails with a message if it doesn't, rather than | ||
| * leaving you with a window that silently receives no input. | ||
| * | ||
| * The exact same code then runs on desktop, web, and mobile -- no platform-specific main-loop | ||
| * forks needed. The app quits once `cf_app_is_running` returns false (window close, or call | ||
| * `cf_app_signal_shutdown`). The platform decides the frame rate, so drive animation off | ||
| * `CF_DELTA_TIME` rather than assuming 60hz. | ||
| * | ||
| * Note for fixed-timestep games (`cf_set_fixed_timestep`): events are delivered between frames, | ||
| * so all sub-steps of one frame share the same input snapshot (the classic loop re-polls the OS | ||
| * queue per sub-step). | ||
| * | ||
| * Implemented on [SDL's main callbacks](https://wiki.libsdl.org/SDL3/README-main-functions): CF | ||
| * defines `SDL_AppInit`/`SDL_AppIterate`/`SDL_AppEvent`/`SDL_AppQuit` for you. If you want those | ||
| * four yourself, skip this header entirely: define `SDL_MAIN_USE_CALLBACKS`, write them, pass | ||
| * `CF_APP_OPTIONS_MAIN_CALLBACKS_BIT` to `cf_make_app`, and forward every event to | ||
| * `cf_app_process_event` (see cute_app.h). | ||
| * @related cf_main_update cf_main_quit cf_make_app cf_app_process_event CF_AppOptionFlagBits | ||
| */ | ||
| CF_Result cf_main_init(int argc, char* argv[]); | ||
|
|
||
| /** | ||
| * @function cf_main_update | ||
| * @category app | ||
| * @brief Your app's frame function -- one frame of the main loop. | ||
| * @remarks Call `cf_app_update` at the top and `cf_app_draw_onto_screen` at the bottom, with your game logic in | ||
| * between, exactly like the body of a classic `while (cf_app_is_running())` loop. The platform decides | ||
| * the call rate (e.g. the browser's requestAnimationFrame on web). The app quits once `cf_app_is_running` | ||
| * returns false -- call `cf_app_signal_shutdown` to request that. | ||
| * @related cf_main_init cf_main_quit cf_app_update cf_app_draw_onto_screen cf_app_signal_shutdown | ||
| */ | ||
| void cf_main_update(void); | ||
|
|
||
| /** | ||
| * @function cf_main_quit | ||
| * @category app | ||
| * @brief Your app's cleanup function -- called once at shutdown. | ||
| * @remarks Free your own resources here. `cf_destroy_app` is called for you right afterwards. Note this runs even | ||
| * when `cf_main_init` failed, so don't assume the app was created. | ||
| * @related cf_main_init cf_main_update cf_destroy_app | ||
| */ | ||
| void cf_main_quit(void); | ||
|
|
||
| SDL_AppResult SDLCALL SDL_AppInit(void** appstate, int argc, char* argv[]) | ||
| { | ||
| (void)appstate; | ||
| if (cf_is_error(cf_main_init(argc, argv))) return SDL_APP_FAILURE; | ||
| // Without the bit CF would keep polling an event queue the callback harness already drained, | ||
| // leaving a window with no keyboard, no mouse and no way to quit. Fail loudly instead. | ||
| if (!(cf_app_get_options() & CF_APP_OPTIONS_MAIN_CALLBACKS_BIT)) { | ||
| SDL_SetError("cf_make_app was called without CF_APP_OPTIONS_MAIN_CALLBACKS_BIT, which cute_main.h requires. Add it to the options passed to cf_make_app in cf_main_init."); | ||
| SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", SDL_GetError()); | ||
| return SDL_APP_FAILURE; | ||
| } | ||
| return SDL_APP_CONTINUE; | ||
| } | ||
|
|
||
| SDL_AppResult SDLCALL SDL_AppIterate(void* appstate) | ||
| { | ||
| (void)appstate; | ||
| cf_main_update(); | ||
| return cf_app_is_running() ? SDL_APP_CONTINUE : SDL_APP_SUCCESS; | ||
| } | ||
|
|
||
| SDL_AppResult SDLCALL SDL_AppEvent(void* appstate, SDL_Event* event) | ||
| { | ||
| (void)appstate; | ||
| cf_app_process_event(event); | ||
| return SDL_APP_CONTINUE; | ||
| } | ||
|
|
||
| void SDLCALL SDL_AppQuit(void* appstate, SDL_AppResult result) | ||
| { | ||
| (void)appstate; | ||
| (void)result; | ||
| cf_main_quit(); | ||
| cf_destroy_app(); | ||
| } | ||
|
|
||
| #ifdef __cplusplus | ||
| } | ||
| #endif // __cplusplus | ||
|
|
||
| #endif // CF_MAIN_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // Lets the host drive the main loop instead of writing your own `while` loop. The same three | ||
| // functions run on desktop, web and mobile -- no `#ifdef CF_EMSCRIPTEN` fork anywhere. | ||
| // | ||
| // Try dragging the window edge and holding it: the orbiting dot keeps moving and the clock keeps | ||
| // ticking, where a classic `while (cf_app_is_running())` loop freezes until you let go. | ||
| // | ||
| // Including cute_main.h is itself the opt-in -- do it in exactly one source file, the one that | ||
| // owns your app's entry point. | ||
| #include <cute.h> | ||
| #include <cute_main.h> | ||
|
|
||
| #include <stdio.h> | ||
|
|
||
| static float s_elapsed; | ||
| static int s_presses; | ||
|
|
||
| CF_Result cf_main_init(int argc, char* argv[]) | ||
| { | ||
| (void)argc; | ||
| // CF checks this result for you -- no `if (cf_is_error(result)) return -1;` needed. | ||
| // CF_APP_OPTIONS_MAIN_CALLBACKS_BIT is required here: it stops CF from polling an event | ||
| // queue it no longer owns. Leave it out and CF refuses to start rather than handing you a | ||
| // window that silently receives no input. | ||
| return cf_make_app("Main Callbacks", 0, 0, 0, 640, 480, | ||
| CF_APP_OPTIONS_WINDOW_POS_CENTERED_BIT | CF_APP_OPTIONS_RESIZABLE_BIT | CF_APP_OPTIONS_MAIN_CALLBACKS_BIT, argv[0]); | ||
| } | ||
|
|
||
| void cf_main_update(void) | ||
| { | ||
| // One frame of the main loop -- exactly the body of a classic `while` loop. | ||
| cf_app_update(NULL); | ||
|
|
||
| // The host picks the frame rate (requestAnimationFrame on web), so animate off delta time | ||
| // instead of assuming 60hz. | ||
| s_elapsed += CF_DELTA_TIME; | ||
| if (cf_key_just_pressed(CF_KEY_SPACE)) s_presses++; | ||
|
|
||
| cf_draw_circle_fill2(cf_v2(cosf(s_elapsed * 2.0f) * 120.0f, sinf(s_elapsed * 2.0f) * 120.0f), 16.0f); | ||
|
|
||
| char buf[64]; | ||
| snprintf(buf, sizeof(buf), "elapsed %.2fs", s_elapsed); | ||
| cf_draw_text(buf, cf_v2(-60, 30), -1); | ||
| snprintf(buf, sizeof(buf), "space pressed %d", s_presses); | ||
| cf_draw_text(buf, cf_v2(-60, 10), -1); | ||
|
|
||
| cf_app_draw_onto_screen(true); | ||
| } | ||
|
|
||
| void cf_main_quit(void) | ||
| { | ||
| // Free your own resources here -- cf_destroy_app runs for you right afterwards. | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -432,8 +432,11 @@ void cf_destroy_app() | |
| cf_destroy_custom_sprite_cache(); | ||
| cs_shutdown(); | ||
| destroy_mutex(&app->on_sound_finish_mutex); | ||
| cf_free_buffered_events(); | ||
| destroy_mutex(&app->buffered_events_mutex); | ||
|
Comment on lines
+435
to
+436
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the limitation the PR description already calls out: "cf_destroy_app frees the event buffer and destroys buffered_events_mutex while another thread could still be inside cf_app_process_event ... Narrow (mobile lifecycle events only) but real." Agreed it's real -- a proper fix needs a shutdown protocol (e.g. an in-flight counter the destroy path waits to drain, or moving the mutex/queue to a lifetime outside CF_App) that's a bigger design change than this PR's scope. Leaving it as a disclosed limitation for now rather than bolting on synchronization here; open to doing it as a follow-up if you'd rather it land before merge. |
||
| if (app->window) SDL_DestroyWindow(app->window); | ||
| SDL_Quit(); | ||
| // Under SDL's main callbacks, SDL itself calls SDL_Quit after SDL_AppQuit returns. | ||
| if (!(app->options & CF_APP_OPTIONS_MAIN_CALLBACKS_BIT)) SDL_Quit(); | ||
| CF_Image* easy_sprites = app->easy_sprites.items(); | ||
| for (int i = 0; i < app->easy_sprites.count(); ++i) { | ||
| cf_image_free(&easy_sprites[i]); | ||
|
|
@@ -446,17 +449,32 @@ void cf_destroy_app() | |
|
|
||
| bool cf_app_is_running() | ||
| { | ||
| return app->running; | ||
| // Null-safe so the cute_main.h glue (and classic loops) behave when the | ||
| // user already destroyed the app, e.g. from within their own update function. | ||
| return app ? app->running : false; | ||
| } | ||
|
|
||
| CF_AppOptionFlags cf_app_get_options() | ||
| { | ||
| // Null-safe, matching cf_app_is_running. The cute_main.h glue reads this to | ||
| // check the app actually opted into callback mode, and must not fault when cf_main_init | ||
| // returned success without ever calling cf_make_app. | ||
| return app ? app->options : 0; | ||
| } | ||
|
|
||
| void cf_app_signal_shutdown() | ||
| { | ||
| app->running = 0; | ||
| if (app) app->running = 0; | ||
| } | ||
|
|
||
| static void s_on_update(void* udata) | ||
| { | ||
| cf_pump_input_msgs(); | ||
| // Buffered events drain in every mode: a stray cf_app_process_event call in a classic | ||
| // loop must not lose events, and must never turn the internal pump off. | ||
| cf_drain_buffered_events(); | ||
| if (!(app->options & CF_APP_OPTIONS_MAIN_CALLBACKS_BIT)) { | ||
| cf_pump_input_msgs(); | ||
| } | ||
| cf_binding_update(); | ||
| if (app->audio_needs_updates) { | ||
| cs_update(CF_DELTA_TIME); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of
void* event, just forward declare SDL_Event:See
cf_app_get_windowThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 3309e77.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bullno1 Makes more sense, thanks!