From 40da0446a7faf7148cbf82b0701c305336b2683b Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 5 Jun 2026 13:24:53 +0100 Subject: [PATCH 1/7] SDL3 frontend --- CMakeLists.txt | 8 + Makefile | 6 + src/desktop/backends/sdl3.c | 330 ++++++++++++++++++++++++++++++++++++ src/desktop/main.c | 4 +- 4 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 src/desktop/backends/sdl3.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 7af57aae..2f1d341b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -267,6 +267,14 @@ if(PLATFORM STREQUAL "desktop") target_include_directories(butterscotch PRIVATE ${SDL2_INCLUDE_DIRS}) target_link_directories(butterscotch PRIVATE ${SDL2_LIBRARY_DIRS}) set(BACKEND_LIBRARIES ${SDL2_LIBRARIES}) + elseif(DESKTOP_BACKEND STREQUAL "sdl3") + add_compile_definitions(USE_SDL3) + + find_package(PkgConfig REQUIRED) + pkg_check_modules(SDL3 REQUIRED sdl3) + target_include_directories(butterscotch PRIVATE ${SDL3_INCLUDE_DIRS}) + target_link_directories(butterscotch PRIVATE ${SDL3_LIBRARY_DIRS}) + set(BACKEND_LIBRARIES ${SDL3_LIBRARIES}) endif() target_link_libraries(butterscotch ${BACKEND_LIBRARIES} ${AUDIO_LIBRARIES} ${PLATFORM_LIBRARIES}) target_sources(butterscotch PRIVATE src/desktop/backends/${DESKTOP_BACKEND}.c) diff --git a/Makefile b/Makefile index d729b27d..5aca81d2 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,12 @@ SDL2_LIBS += $(shell pkg-config --libs sdl2) LIBS += $(SDL2_LIBS) DEFINES += -DUSE_SDL2 endif +ifeq ($(DESKTOP_BACKEND),sdl3) +SDL3_LIBS += $(shell pkg-config --libs sdl3) +LIBS += $(SDL3_LIBS) +DEFINES += -DUSE_SDL3 +endif + # GNU make doesn't have a way to do OR in conditionals, stupid language for clowns ifndef DISABLE_LEGACY_GL diff --git a/src/desktop/backends/sdl3.c b/src/desktop/backends/sdl3.c new file mode 100644 index 00000000..4a54dcfc --- /dev/null +++ b/src/desktop/backends/sdl3.c @@ -0,0 +1,330 @@ +#include + +#include +#include +#include + +#include "common.h" +#include "input_recording.h" +#include "desktop/platformdefs.h" +#include + +static Runner *g_runner; +static int32_t fbWidth, fbHeight; +static SDL_Surface* scr; +static SDL_Window *window; + +void platformSetWindowTitle(const char* title) { + char windowTitle[256]; + snprintf(windowTitle, sizeof(windowTitle), "Butterscotch - %s", title); + SDL_SetWindowTitle(window, windowTitle); +} + +bool platformGetWindowSize(int32_t* outW, int32_t* outH) { + if (!outW || !outH) return false; + *outW = fbWidth; + *outH = fbHeight; + return true; +} + +bool platformGetScaledWindowSize(int32_t* outW, int32_t* outH) { + return platformGetWindowSize(outW, outH); +} + +void platformSetWindowSize(int32_t width, int32_t height) { + if (width <= 0 || height <= 0) return; + fbWidth = width; + fbHeight = height; + SDL_SetWindowSize(window, width, height); + if (gfx == SOFTWARE) + scr = SDL_GetWindowSurface(window); +} + +void platformGetMousePos(double *xPos, double *yPos) { + if (!xPos || !yPos) return; + float mx = 0, my = 0; + SDL_GetMouseState(&mx, &my); + *xPos = (double)mx; + *yPos = (double)my; +} + +static bool platformGetWindowFocus(void) { + return SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS; +} + +bool platformInit(int reqW, int reqH, const char *title, bool headless) { + // Init SDL + if (!SDL_Init(SDL_INIT_VIDEO)) { + fprintf(stderr, "Failed to initialize SDL\n"); + return false; + } + + if (gfx == LEGACY_GL) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); + } else if (gfx == MODERN_GL) { +#ifdef ENABLE_GLES +#ifdef SDL_GL_CONTEXT_PROFILE_MASK + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); +#endif + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); +#else + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG | SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); +#endif + } + + Uint32 flags; + if (headless) + flags = (gfx == SOFTWARE ? 0 : SDL_WINDOW_OPENGL) | SDL_WINDOW_HIDDEN; + else + flags = (gfx == SOFTWARE ? 0 : SDL_WINDOW_OPENGL) | SDL_WINDOW_RESIZABLE; + + fbWidth = reqW; + fbHeight = reqH; + window = SDL_CreateWindow( + title, + fbWidth, + fbHeight, + flags + ); + if (!window && gfx == SOFTWARE) { + SDL_DisplayID display_id = SDL_GetPrimaryDisplay(); + + const SDL_DisplayMode *mode = SDL_GetCurrentDisplayMode(display_id); + + if (mode != NULL) { + fprintf(stderr, "Warning: %dx%d unavailable, falling back to %dx%d: %s\n", + reqW, reqH, mode->w, mode->h, SDL_GetError()); + fbWidth = mode->w; + fbHeight = mode->h; + + window = SDL_CreateWindow( + title, + fbWidth, + fbHeight, + flags + ); + } + } + if (!window) { + fprintf(stderr, "Fatal: Could not set any video mode: %s\n", SDL_GetError()); + return false; + } + if (gfx != SOFTWARE) { + if (!SDL_GL_CreateContext(window)) { + fprintf(stderr, "Fatal: Could not create GL context: %s\n", SDL_GetError()); + return false; + } + SDL_GL_SetSwapInterval(0); // disable vsync + } else + scr = SDL_GetWindowSurface(window); + + return true; +} + +void platformExit(void) { + SDL_Quit(); +} + +void platformInitFunctions(Runner *runner) { + g_runner = runner; + runner->windowHasFocus = platformGetWindowFocus; +} + +#ifdef ENABLE_SW_RENDERER + +static SDL_Surface* nextFb = NULL; + +void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { + if (nextFb) { + SDL_FreeSurface(nextFb); + nextFb = NULL; + } + + nextFb = SDL_CreateRGBSurfaceFrom( + framebuffer, + width, + height, + 32, + width * 4, + 0x00ff0000, // Rmask + 0x0000ff00, // Gmask + 0x000000ff, // Bmask + 0x00000000 // Amask + ); +} + +#endif + +void platformSwapBuffers(void) { +#ifdef ENABLE_SW_RENDERER + if(gfx == SOFTWARE) { + SDL_BlitSurface(nextFb, NULL, scr, NULL); + SDL_UpdateWindowSurface(window); + } +#endif +#if defined(ENABLE_LEGACY_GL) || defined(ENABLE_MODERN_GL) + if (gfx == LEGACY_GL || gfx == MODERN_GL) + SDL_GL_SwapWindow(window); +#endif +} + +#if defined(ENABLE_MODERN_GL) || defined(ENABLE_LEGACY_GL) + +void *platformGetProcAddress(const char *name) { + return SDL_GL_GetProcAddress(name); +} + +#endif + +double platformGetTime(void) { + return (double)SDL_GetTicks() / 1000.0; +} + +static int32_t SDLKeyToGml(int sdlkey) { + // Letters and numbers are the same as GML + if (sdlkey >= 'a' && sdlkey <= 'z') return toupper(sdlkey); + if (sdlkey >= '0' && sdlkey <= '9') return sdlkey; + // Special keys need mapping + switch (sdlkey) { + case SDLK_ESCAPE: return VK_ESCAPE; + case SDLK_RETURN: return VK_ENTER; + case SDLK_TAB: return VK_TAB; + case SDLK_BACKSPACE: return VK_BACKSPACE; + case SDLK_SPACE: return VK_SPACE; + case SDLK_LSHIFT: + case SDLK_RSHIFT: return VK_SHIFT; + case SDLK_LCTRL: + case SDLK_RCTRL: return VK_CONTROL; + case SDLK_LALT: + case SDLK_RALT: return VK_ALT; + case SDLK_UP: return VK_UP; + case SDLK_DOWN: return VK_DOWN; + case SDLK_LEFT: return VK_LEFT; + case SDLK_RIGHT: return VK_RIGHT; + case SDLK_F1: return VK_F1; + case SDLK_F2: return VK_F2; + case SDLK_F3: return VK_F3; + case SDLK_F4: return VK_F4; + case SDLK_F5: return VK_F5; + case SDLK_F6: return VK_F6; + case SDLK_F7: return VK_F7; + case SDLK_F8: return VK_F8; + case SDLK_F9: return VK_F9; + case SDLK_F10: return VK_F10; + case SDLK_F11: return VK_F11; + case SDLK_F12: return VK_F12; + case SDLK_INSERT: return VK_INSERT; + case SDLK_DELETE: return VK_DELETE; + case SDLK_HOME: return VK_HOME; + case SDLK_END: return VK_END; + case SDLK_PAGEUP: return VK_PAGEUP; + case SDLK_PAGEDOWN: return VK_PAGEDOWN; + default: return -1; // Unknown + } +} + +static uint32_t utf8_to_codepoint(const char *s) { + const unsigned char *p = (const unsigned char *)s; + + if (p[0] < 0x80) + return p[0]; + + if ((p[0] & 0xE0) == 0xC0) + return ((p[0] & 0x1F) << 6) | + (p[1] & 0x3F); + + if ((p[0] & 0xF0) == 0xE0) + return ((p[0] & 0x0F) << 12) | + ((p[1] & 0x3F) << 6) | + (p[2] & 0x3F); + + if ((p[0] & 0xF8) == 0xF0) + return ((p[0] & 0x07) << 18) | + ((p[1] & 0x3F) << 12) | + ((p[2] & 0x3F) << 6) | + (p[3] & 0x3F); + + return 0xFFFD; // replacement character +} + +static int32_t SDLMouseButtonToGml(int sdlButton) { + switch (sdlButton) { + case SDL_BUTTON_LEFT: return GML_MB_LEFT; + case SDL_BUTTON_RIGHT: return GML_MB_RIGHT; + case SDL_BUTTON_MIDDLE: return GML_MB_MIDDLE; + default: return -1; + } +} + +bool platformHandleEvents(void) { + bool should_exit = false; + SDL_Event e; + while (SDL_PollEvent(&e)) { + switch(e.type) { + case SDL_EVENT_KEY_DOWN: + // During playback, suppress real keyboard input + if (InputRecording_isPlaybackActive(globalInputRecording)) break; + if (e.key.repeat != 0) + break; + RunnerKeyboard_onKeyDown(g_runner->keyboard, SDLKeyToGml(e.key.key)); + break; + case SDL_EVENT_KEY_UP: + // During playback, suppress real keyboard input + if (InputRecording_isPlaybackActive(globalInputRecording)) break; + RunnerKeyboard_onKeyUp(g_runner->keyboard, SDLKeyToGml(e.key.key)); + break; + case SDL_EVENT_TEXT_INPUT: + // During playback, suppress real keyboard input + if (InputRecording_isPlaybackActive(globalInputRecording)) break; + RunnerKeyboard_onCharacter(g_runner->keyboard, utf8_to_codepoint(e.text.text)); + break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: { + if (InputRecording_isPlaybackActive(globalInputRecording)) break; + int32_t gmlBtn = SDLMouseButtonToGml(e.button.button); + if (gmlBtn >= 0) RunnerMouse_onButtonDown(g_runner->mouse, gmlBtn); + } break; + case SDL_EVENT_MOUSE_BUTTON_UP: { + if (InputRecording_isPlaybackActive(globalInputRecording)) break; + int32_t gmlBtn = SDLMouseButtonToGml(e.button.button); + if (gmlBtn >= 0) RunnerMouse_onButtonUp(g_runner->mouse, gmlBtn); + } break; + case SDL_EVENT_MOUSE_WHEEL: + if (InputRecording_isPlaybackActive(globalInputRecording)) break; + if (e.wheel.y != 0) + RunnerMouse_onWheel(g_runner->mouse, (float)e.wheel.y); + break; + case SDL_EVENT_WINDOW_RESIZED: + fbWidth = e.window.data1; + fbHeight = e.window.data2; + if (gfx == SOFTWARE) + scr = SDL_GetWindowSurface(window); + break; + case SDL_EVENT_QUIT: + should_exit = true; + break; + default: + break; + } + } + + return should_exit; +} + +void platformSleepUntil(double time) { + double remaining = time - platformGetTime(); + if (remaining > 0.002) + SDL_Delay((Uint32)((remaining - 0.001) * 1000)); + + while (platformGetTime() < time) { + // Spin-wait for the remaining sub-millisecond + } +} + +void platformGamepad_poll(RunnerGamepadState* gp) { + (void)gp; +} diff --git a/src/desktop/main.c b/src/desktop/main.c index 007ff79a..69b7a8be 100644 --- a/src/desktop/main.c +++ b/src/desktop/main.c @@ -55,6 +55,8 @@ #include #elif defined(USE_SDL2) #include +#elif defined(USE_SDL3) +#include #endif enum GraphicsAPI gfx; @@ -351,7 +353,7 @@ static void parseCommandLineArgs(CommandLineArgs* args, int argc, char* argv[]) args->loadType = DATAWINLOADTYPE_LOAD_IN_MEMORY_AHEAD_OF_TIME; // TODO: detect available driver features // at runtime to improve defaults. -#if defined(ENABLE_MODERN_GL) && (defined(USE_GLFW3) || defined(USE_SDL2)) +#if defined(ENABLE_MODERN_GL) && (defined(USE_GLFW3) || defined(USE_SDL2) || defined(USE_SDL3)) args->renderer = "modern-gl"; #elif defined(ENABLE_LEGACY_GL) args->renderer = "legacy-gl"; From 922c75405d4c4eab9aadf8de3e52517544a7f611 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 5 Jun 2026 14:10:54 +0100 Subject: [PATCH 2/7] sdl3: use SDL_DelayPrecise for platform sleep --- src/desktop/backends/sdl3.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/desktop/backends/sdl3.c b/src/desktop/backends/sdl3.c index 4a54dcfc..9433b971 100644 --- a/src/desktop/backends/sdl3.c +++ b/src/desktop/backends/sdl3.c @@ -1,3 +1,4 @@ +#include #include #include @@ -182,7 +183,8 @@ void *platformGetProcAddress(const char *name) { #endif double platformGetTime(void) { - return (double)SDL_GetTicks() / 1000.0; + // SDL_GetTicksNS() returns Uint64 nanoseconds + return (double)SDL_GetTicksNS() / 1000000000.0; } static int32_t SDLKeyToGml(int sdlkey) { @@ -317,11 +319,10 @@ bool platformHandleEvents(void) { void platformSleepUntil(double time) { double remaining = time - platformGetTime(); - if (remaining > 0.002) - SDL_Delay((Uint32)((remaining - 0.001) * 1000)); - while (platformGetTime() < time) { - // Spin-wait for the remaining sub-millisecond + if (remaining > 0.0) { + Uint64 remainingNS = (Uint64)(remaining * 1000000000.0); + SDL_DelayPrecise(remainingNS); } } From 2390019f845915985a23539b2d925b9ea1b9e610 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 5 Jun 2026 14:37:19 +0100 Subject: [PATCH 3/7] sdl3: cleanup indentation in platformInit --- src/desktop/backends/sdl3.c | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/desktop/backends/sdl3.c b/src/desktop/backends/sdl3.c index 9433b971..1a8e57dc 100644 --- a/src/desktop/backends/sdl3.c +++ b/src/desktop/backends/sdl3.c @@ -79,10 +79,7 @@ bool platformInit(int reqW, int reqH, const char *title, bool headless) { } Uint32 flags; - if (headless) - flags = (gfx == SOFTWARE ? 0 : SDL_WINDOW_OPENGL) | SDL_WINDOW_HIDDEN; - else - flags = (gfx == SOFTWARE ? 0 : SDL_WINDOW_OPENGL) | SDL_WINDOW_RESIZABLE; + flags = (gfx == SOFTWARE ? 0 : SDL_WINDOW_OPENGL) | (headless ? SDL_WINDOW_HIDDEN : SDL_WINDOW_RESIZABLE); fbWidth = reqW; fbHeight = reqH; @@ -94,20 +91,17 @@ bool platformInit(int reqW, int reqH, const char *title, bool headless) { ); if (!window && gfx == SOFTWARE) { SDL_DisplayID display_id = SDL_GetPrimaryDisplay(); - const SDL_DisplayMode *mode = SDL_GetCurrentDisplayMode(display_id); - if (mode != NULL) { fprintf(stderr, "Warning: %dx%d unavailable, falling back to %dx%d: %s\n", reqW, reqH, mode->w, mode->h, SDL_GetError()); fbWidth = mode->w; fbHeight = mode->h; - window = SDL_CreateWindow( - title, - fbWidth, - fbHeight, - flags + title, + fbWidth, + fbHeight, + flags ); } } From 937c09fd99a0a0ad1a1ebe26c493ea28d24ae72c Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 5 Jun 2026 14:39:15 +0100 Subject: [PATCH 4/7] sdl3: platformInit: condense flags initialisation --- src/desktop/backends/sdl3.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/desktop/backends/sdl3.c b/src/desktop/backends/sdl3.c index 1a8e57dc..19355437 100644 --- a/src/desktop/backends/sdl3.c +++ b/src/desktop/backends/sdl3.c @@ -78,16 +78,14 @@ bool platformInit(int reqW, int reqH, const char *title, bool headless) { #endif } - Uint32 flags; - flags = (gfx == SOFTWARE ? 0 : SDL_WINDOW_OPENGL) | (headless ? SDL_WINDOW_HIDDEN : SDL_WINDOW_RESIZABLE); - + Uint32 flags = (gfx == SOFTWARE ? 0 : SDL_WINDOW_OPENGL) | (headless ? SDL_WINDOW_HIDDEN : SDL_WINDOW_RESIZABLE); fbWidth = reqW; fbHeight = reqH; window = SDL_CreateWindow( - title, - fbWidth, - fbHeight, - flags + title, + fbWidth, + fbHeight, + flags ); if (!window && gfx == SOFTWARE) { SDL_DisplayID display_id = SDL_GetPrimaryDisplay(); From 6aa60f826129c4fbe7b5a4d09258a8b4877669e4 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 5 Jun 2026 14:55:29 +0100 Subject: [PATCH 5/7] sdl3: fix Runner_setNextFrame override for software renderer in anticipation --- src/desktop/backends/sdl3.c | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/desktop/backends/sdl3.c b/src/desktop/backends/sdl3.c index 19355437..90a20ed7 100644 --- a/src/desktop/backends/sdl3.c +++ b/src/desktop/backends/sdl3.c @@ -134,20 +134,16 @@ static SDL_Surface* nextFb = NULL; void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { if (nextFb) { - SDL_FreeSurface(nextFb); + SDL_DestroySurface(nextFb); nextFb = NULL; } - nextFb = SDL_CreateRGBSurfaceFrom( - framebuffer, + nextFb = SDL_CreateSurfaceFrom( width, height, - 32, + SDL_PIXELFORMAT_XRGB8888, + framebuffer width * 4, - 0x00ff0000, // Rmask - 0x0000ff00, // Gmask - 0x000000ff, // Bmask - 0x00000000 // Amask ); } From 4a0c7549c554bcb36b529e3ad6b9d2957e8cb777 Mon Sep 17 00:00:00 2001 From: Cobalt <65132371+cobaltgit@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:03:29 +0100 Subject: [PATCH 6/7] look a comma --- src/desktop/backends/sdl3.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop/backends/sdl3.c b/src/desktop/backends/sdl3.c index 90a20ed7..b0266687 100644 --- a/src/desktop/backends/sdl3.c +++ b/src/desktop/backends/sdl3.c @@ -142,7 +142,7 @@ void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { width, height, SDL_PIXELFORMAT_XRGB8888, - framebuffer + framebuffer, width * 4, ); } From 55becb4a4b187ab41edcc8f400f502157edf0408 Mon Sep 17 00:00:00 2001 From: Cobalt <65132371+cobaltgit@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:04:01 +0100 Subject: [PATCH 7/7] look no comma --- src/desktop/backends/sdl3.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop/backends/sdl3.c b/src/desktop/backends/sdl3.c index b0266687..34f63dd2 100644 --- a/src/desktop/backends/sdl3.c +++ b/src/desktop/backends/sdl3.c @@ -143,7 +143,7 @@ void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { height, SDL_PIXELFORMAT_XRGB8888, framebuffer, - width * 4, + width * 4 ); }