From 4eca2d7b6a4545c0bca48dd0c97c3dd58db7daa5 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Wed, 26 Aug 2026 13:02:37 -0700 Subject: [PATCH] Make SDL3 the default runner frontend --- PLAN.md | 2 +- README.md | 6 +- docs/sdl3-default.md | 59 ++ docs/wiimmfi-runbook.md | 8 +- runner/CMakeLists.txt | 36 +- runner/src/frontend.cpp | 653 +++++++++++++++++---- runner/src/melonds_compute/ComputeHost.cpp | 83 ++- 7 files changed, 705 insertions(+), 142 deletions(-) create mode 100644 docs/sdl3-default.md diff --git a/PLAN.md b/PLAN.md index 2749d08..6940c46 100644 --- a/PLAN.md +++ b/PLAN.md @@ -29,7 +29,7 @@ game until that gate is met. interpreter (the correctness floor running the firmware's own copied bytes). The firmware menu runs interpreted at first, promoted to banks later. -- **Host:** SDL2, single window stacked 256×384 (engine A top / B +- **Host:** SDL3 by default, single window stacked 256×384 (engine A top / B bottom), software rendering; mouse → ARM7 TSC touch. ## Inputs (verified, in `bios/`, git-ignored) diff --git a/README.md b/README.md index d3ef413..bc39e77 100644 --- a/README.md +++ b/README.md @@ -181,8 +181,10 @@ cmake -G Ninja -B runner/build runner cmake --build runner/build ``` -SDL2 is optional at configure time; without it, the runner is headless and -interactive presentation is unavailable. +SDL3 is the default host presentation backend. Configure with +`-DNDS_SDL_BACKEND=SDL2` for the compatibility backend, or +`-DNDS_SDL_BACKEND=NONE` for a headless runner with no interactive +presentation. ### Game-owned static banks diff --git a/docs/sdl3-default.md b/docs/sdl3-default.md new file mode 100644 index 0000000..06d3d43 --- /dev/null +++ b/docs/sdl3-default.md @@ -0,0 +1,59 @@ +# SDL3 default frontend + +## Goal + +`nds_runner` uses SDL3 as the default interactive host frontend. SDL2 remains +available as an explicit compatibility backend, and headless builds remain +available for non-interactive tooling. + +## Build contract + +- `NDS_SDL_BACKEND=SDL3` is the default and requires `SDL3::SDL3`. +- `NDS_SDL_BACKEND=SDL2` selects the compatibility backend and requires + `SDL2::SDL2`. +- `NDS_SDL_BACKEND=NONE` builds without the SDL presentation layer. +- `NDS_ENABLE_COMPUTE_RENDERER=ON` requires `NDS_SDL_BACKEND=SDL3` or `SDL2` + because the compute renderer owns an SDL-created GL context. + +## Implementation notes + +The compatibility layer is local to `runner/src/frontend.cpp` and +`runner/src/melonds_compute/ComputeHost.cpp`. The public runner behavior stays +the same: one stacked DS display window, keyboard/gamepad input, mouse/touch +mapping, optional audio, and the existing software/compute renderer switch. + +SDL3 uses `SDL_AudioStream` through `SDL_OpenAudioDeviceStream`. SDL2 keeps the +existing callback-backed ring buffer. SDL3 exclusive fullscreen currently maps +to SDL3's fullscreen window state; display-mode selection can be added later if +a title needs true mode switching. + +## Verified commands + +```powershell +& C:\msys64\mingw64\bin\cmake.exe -G Ninja ` + -S F:\Projects\ndsrecomp\worktrees\ndsrecomp-sdl3-default\runner ` + -B F:\Projects\ndsrecomp\worktrees\ndsrecomp-sdl3-default\runner\build-sdl3-default ` + -DCMAKE_BUILD_TYPE=Release ` + -DNDS_ENABLE_COMPUTE_RENDERER=ON ` + -DNDS_BOOTSTRAP_FIRMWARE=ON ` + -DNDS_GENERATED_DIR=F:\Projects\ndsrecomp\ndsrecomp\generated ` + -DCMAKE_PREFIX_PATH=C:\msys64\mingw64\lib\cmake +& C:\msys64\mingw64\bin\cmake.exe --build ` + F:\Projects\ndsrecomp\worktrees\ndsrecomp-sdl3-default\runner\build-sdl3-default ` + --target nds_runner -j 12 +``` + +```powershell +& C:\msys64\mingw64\bin\cmake.exe -G Ninja ` + -S F:\Projects\ndsrecomp\worktrees\ndsrecomp-sdl3-default\runner ` + -B F:\Projects\ndsrecomp\worktrees\ndsrecomp-sdl3-default\runner\build-sdl2-fallback ` + -DCMAKE_BUILD_TYPE=Release ` + -DNDS_SDL_BACKEND=SDL2 ` + -DNDS_ENABLE_COMPUTE_RENDERER=ON ` + -DNDS_BOOTSTRAP_FIRMWARE=ON ` + -DNDS_GENERATED_DIR=F:\Projects\ndsrecomp\ndsrecomp\generated ` + -DCMAKE_PREFIX_PATH=C:\msys64\mingw64\lib\cmake +& C:\msys64\mingw64\bin\cmake.exe --build ` + F:\Projects\ndsrecomp\worktrees\ndsrecomp-sdl3-default\runner\build-sdl2-fallback ` + --target nds_runner -j 12 +``` diff --git a/docs/wiimmfi-runbook.md b/docs/wiimmfi-runbook.md index 344dd86..3dc319c 100644 --- a/docs/wiimmfi-runbook.md +++ b/docs/wiimmfi-runbook.md @@ -30,9 +30,9 @@ Verified present on this machine: the `-G Ninja` generator. - `g++.exe (Rev9, Built by MSYS2 project) 15.2.0` at `C:\msys64\mingw64\bin\g++.exe`. -- SDL2 CMake config at `C:\msys64\mingw64\lib\cmake\SDL2` (required because - the runner defaults `NDS_ENABLE_COMPUTE_RENDERER=ON`, which hard-requires - SDL2 at configure time). +- SDL3 CMake config at `C:\msys64\mingw64\lib\cmake\SDL3` (default host + presentation backend). SDL2 remains available with + `-DNDS_SDL_BACKEND=SDL2`. - `ccache` auto-detected and enabled by the framework's `NdsCompilerCache.cmake` (`AUTO` policy) — configure log prints `ndsrecomp: compiler cache enabled: C:/msys64/mingw64/bin/ccache.exe`. @@ -152,7 +152,7 @@ Configure (PowerShell): Result: configure succeeded in **1.6 seconds**. `CMakeCache.txt` confirms `NDS_TITLE_BANK_DIR=F:/Projects/ndsrecomp/mariokartdsrecomp-wiimmfi/generated/recomp`, -`NDS_BOOTSTRAP_FIRMWARE=ON`, `SDL2_DIR=C:/msys64/mingw64/lib/cmake/SDL2`, +`NDS_BOOTSTRAP_FIRMWARE=ON`, `SDL3_DIR=C:/msys64/mingw64/lib/cmake/SDL3`, `CMAKE_C_COMPILER=C:/msys64/mingw64/bin/cc.exe`. Build: diff --git a/runner/CMakeLists.txt b/runner/CMakeLists.txt index a303aeb..3538e75 100644 --- a/runner/CMakeLists.txt +++ b/runner/CMakeLists.txt @@ -26,6 +26,9 @@ option(NDS_BOOTSTRAP_FIRMWARE option(NDS_ENABLE_COMPUTE_RENDERER "Build the preferred melonDS OpenGL 4.3 compute renderer" ON) +set(NDS_SDL_BACKEND "SDL3" CACHE STRING + "Host presentation backend: SDL3, SDL2, or NONE") +set_property(CACHE NDS_SDL_BACKEND PROPERTY STRINGS SDL3 SDL2 NONE) option(NDS_ENABLE_PCAP_BACKEND "Build the vendored melonDS Net_PCap backend (dynamically loads Npcap/WinPcap at runtime; off by default per docs/adr-melonds-wifi-vendoring.md §2)" OFF) @@ -541,16 +544,33 @@ if(SM64DS_BANK_SOURCES) endif() endif() -# SDL is a host-only presentation layer. The runner remains buildable without -# it for headless deterministic/oracle work; --interactive reports that the -# configured binary has no frontend in that case. -find_package(SDL2 CONFIG QUIET) -if(TARGET SDL2::SDL2) - target_compile_definitions(nds_runner PRIVATE NDS_HAVE_SDL2=1) +# SDL is a host-only presentation layer. SDL3 is the default runtime frontend; +# SDL2 and a no-frontend headless build are explicit compatibility choices. +if(NDS_SDL_BACKEND STREQUAL "SDL3") + find_package(SDL3 CONFIG REQUIRED) + if(NOT TARGET SDL3::SDL3) + message(FATAL_ERROR "NDS_SDL_BACKEND=SDL3 requires target SDL3::SDL3") + endif() + target_compile_definitions(nds_runner PRIVATE + NDS_HAVE_SDL3=1 + NDS_SDL_BACKEND_NAME="SDL3") + target_link_libraries(nds_runner PRIVATE SDL3::SDL3) +elseif(NDS_SDL_BACKEND STREQUAL "SDL2") + find_package(SDL2 CONFIG REQUIRED) + if(NOT TARGET SDL2::SDL2) + message(FATAL_ERROR "NDS_SDL_BACKEND=SDL2 requires target SDL2::SDL2") + endif() + target_compile_definitions(nds_runner PRIVATE + NDS_HAVE_SDL2=1 + NDS_SDL_BACKEND_NAME="SDL2") target_link_libraries(nds_runner PRIVATE SDL2::SDL2) +elseif(NDS_SDL_BACKEND STREQUAL "NONE") + target_compile_definitions(nds_runner PRIVATE NDS_SDL_BACKEND_NAME="NONE") +else() + message(FATAL_ERROR "NDS_SDL_BACKEND must be SDL3, SDL2, or NONE") endif() -if(NDS_ENABLE_COMPUTE_RENDERER AND NOT TARGET SDL2::SDL2) - message(FATAL_ERROR "NDS_ENABLE_COMPUTE_RENDERER requires SDL2") +if(NDS_ENABLE_COMPUTE_RENDERER AND NDS_SDL_BACKEND STREQUAL "NONE") + message(FATAL_ERROR "NDS_ENABLE_COMPUTE_RENDERER requires NDS_SDL_BACKEND=SDL3 or SDL2") endif() if(NDS_ENABLE_COMPUTE_RENDERER AND CMAKE_DL_LIBS) target_link_libraries(nds_runner PRIVATE ${CMAKE_DL_LIBS}) diff --git a/runner/src/frontend.cpp b/runner/src/frontend.cpp index ef45583..2789c2c 100644 --- a/runner/src/frontend.cpp +++ b/runner/src/frontend.cpp @@ -78,12 +78,111 @@ void observe_top_black_bands(const uint32_t* pixels, uint64_t frame) { } } // namespace -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) #define SDL_MAIN_HANDLED +#if defined(NDS_HAVE_SDL3) +#include +#include +#else #include +#endif namespace { +#if defined(NDS_HAVE_SDL3) +#undef SDL_GameController +#undef SDL_GameControllerButton +#undef SDL_GameControllerClose +#undef SDL_GameControllerGetAxis +#undef SDL_GameControllerName +#undef SDL_GameControllerOpen +#undef SDL_CONTROLLER_AXIS_LEFTX +#undef SDL_CONTROLLER_AXIS_LEFTY +#undef SDL_CONTROLLER_AXIS_RIGHTX +#undef SDL_CONTROLLER_AXIS_RIGHTY +#undef SDL_CONTROLLER_AXIS_TRIGGERLEFT +#undef SDL_CONTROLLER_AXIS_TRIGGERRIGHT +#undef SDL_CONTROLLER_BUTTON_A +#undef SDL_CONTROLLER_BUTTON_B +#undef SDL_CONTROLLER_BUTTON_BACK +#undef SDL_CONTROLLER_BUTTON_DPAD_DOWN +#undef SDL_CONTROLLER_BUTTON_DPAD_LEFT +#undef SDL_CONTROLLER_BUTTON_DPAD_RIGHT +#undef SDL_CONTROLLER_BUTTON_DPAD_UP +#undef SDL_CONTROLLER_BUTTON_INVALID +#undef SDL_CONTROLLER_BUTTON_LEFTSHOULDER +#undef SDL_CONTROLLER_BUTTON_LEFTSTICK +#undef SDL_CONTROLLER_BUTTON_RIGHTSHOULDER +#undef SDL_CONTROLLER_BUTTON_RIGHTSTICK +#undef SDL_CONTROLLER_BUTTON_START +#undef SDL_CONTROLLER_BUTTON_X +#undef SDL_CONTROLLER_BUTTON_Y +#undef SDL_CONTROLLERBUTTONDOWN +#undef SDL_CONTROLLERBUTTONUP +#undef SDL_CONTROLLERDEVICEADDED +#undef SDL_CONTROLLERDEVICEREMOVED +#undef SDL_KEYDOWN +#undef SDL_KEYUP +#undef SDL_MOUSEBUTTONDOWN +#undef SDL_MOUSEBUTTONUP +#undef SDL_MOUSEMOTION +#undef SDL_QUIT +#undef SDL_FALSE +#undef SDL_TRUE +#undef SDL_INIT_GAMECONTROLLER +#undef SDL_ScaleModeNearest +#undef SDL_WINDOW_ALLOW_HIGHDPI +#undef AUDIO_S16SYS +#define SDL_GameController SDL_Gamepad +#define SDL_GameControllerButton SDL_GamepadButton +#define SDL_GameControllerClose SDL_CloseGamepad +#define SDL_GameControllerGetAxis SDL_GetGamepadAxis +#define SDL_GameControllerName SDL_GetGamepadName +#define SDL_GameControllerOpen SDL_OpenGamepad +#define SDL_CONTROLLER_AXIS_LEFTX SDL_GAMEPAD_AXIS_LEFTX +#define SDL_CONTROLLER_AXIS_LEFTY SDL_GAMEPAD_AXIS_LEFTY +#define SDL_CONTROLLER_AXIS_RIGHTX SDL_GAMEPAD_AXIS_RIGHTX +#define SDL_CONTROLLER_AXIS_RIGHTY SDL_GAMEPAD_AXIS_RIGHTY +#define SDL_CONTROLLER_AXIS_TRIGGERLEFT SDL_GAMEPAD_AXIS_LEFT_TRIGGER +#define SDL_CONTROLLER_AXIS_TRIGGERRIGHT SDL_GAMEPAD_AXIS_RIGHT_TRIGGER +#define SDL_CONTROLLER_BUTTON_A SDL_GAMEPAD_BUTTON_SOUTH +#define SDL_CONTROLLER_BUTTON_B SDL_GAMEPAD_BUTTON_EAST +#define SDL_CONTROLLER_BUTTON_BACK SDL_GAMEPAD_BUTTON_BACK +#define SDL_CONTROLLER_BUTTON_DPAD_DOWN SDL_GAMEPAD_BUTTON_DPAD_DOWN +#define SDL_CONTROLLER_BUTTON_DPAD_LEFT SDL_GAMEPAD_BUTTON_DPAD_LEFT +#define SDL_CONTROLLER_BUTTON_DPAD_RIGHT SDL_GAMEPAD_BUTTON_DPAD_RIGHT +#define SDL_CONTROLLER_BUTTON_DPAD_UP SDL_GAMEPAD_BUTTON_DPAD_UP +#define SDL_CONTROLLER_BUTTON_INVALID SDL_GAMEPAD_BUTTON_INVALID +#define SDL_CONTROLLER_BUTTON_LEFTSHOULDER SDL_GAMEPAD_BUTTON_LEFT_SHOULDER +#define SDL_CONTROLLER_BUTTON_LEFTSTICK SDL_GAMEPAD_BUTTON_LEFT_STICK +#define SDL_CONTROLLER_BUTTON_RIGHTSHOULDER SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER +#define SDL_CONTROLLER_BUTTON_RIGHTSTICK SDL_GAMEPAD_BUTTON_RIGHT_STICK +#define SDL_CONTROLLER_BUTTON_START SDL_GAMEPAD_BUTTON_START +#define SDL_CONTROLLER_BUTTON_X SDL_GAMEPAD_BUTTON_WEST +#define SDL_CONTROLLER_BUTTON_Y SDL_GAMEPAD_BUTTON_NORTH +#define SDL_CONTROLLERBUTTONDOWN SDL_EVENT_GAMEPAD_BUTTON_DOWN +#define SDL_CONTROLLERBUTTONUP SDL_EVENT_GAMEPAD_BUTTON_UP +#define SDL_CONTROLLERDEVICEADDED SDL_EVENT_GAMEPAD_ADDED +#define SDL_CONTROLLERDEVICEREMOVED SDL_EVENT_GAMEPAD_REMOVED +#define SDL_KEYDOWN SDL_EVENT_KEY_DOWN +#define SDL_KEYUP SDL_EVENT_KEY_UP +#define SDL_MOUSEBUTTONDOWN SDL_EVENT_MOUSE_BUTTON_DOWN +#define SDL_MOUSEBUTTONUP SDL_EVENT_MOUSE_BUTTON_UP +#define SDL_MOUSEMOTION SDL_EVENT_MOUSE_MOTION +#define SDL_QUIT SDL_EVENT_QUIT +#define SDL_FALSE false +#define SDL_TRUE true +#define SDL_INIT_GAMECONTROLLER SDL_INIT_GAMEPAD +#define SDL_ScaleModeNearest SDL_SCALEMODE_NEAREST +#define SDL_WINDOW_ALLOW_HIGHDPI SDL_WINDOW_HIGH_PIXEL_DENSITY +#define AUDIO_S16SYS SDL_AUDIO_S16 +#else +#define SDL_EVENT_WINDOW_CLOSE_REQUESTED SDL_WINDOWEVENT_CLOSE +#define SDL_EVENT_WINDOW_FOCUS_GAINED SDL_WINDOWEVENT_FOCUS_GAINED +#define SDL_EVENT_WINDOW_FOCUS_LOST SDL_WINDOWEVENT_FOCUS_LOST +#define SDL_EVENT_WINDOW_MOUSE_LEAVE SDL_WINDOWEVENT_LEAVE +#endif + constexpr int kScreenWidth = 256; constexpr int kScreenHeight = 192; constexpr uint32_t kMphUs10MorphState = 0x020DA818u; @@ -482,14 +581,24 @@ constexpr uint32_t kAudioFrameBytes = 2u * sizeof(int16_t); constexpr uint32_t kAudioCapacityFrames = 65536; struct AudioQueue { +#if !defined(NDS_HAVE_SDL3) std::array samples{}; uint32_t read = 0; uint32_t write = 0; uint32_t count = 0; +#endif std::atomic underruns{0}; std::atomic started{false}; }; +#if defined(NDS_HAVE_SDL3) +struct NdsAudioDevice { + SDL_AudioStream* stream = nullptr; + explicit operator bool() const { return stream != nullptr; } +}; +#else +using NdsAudioDevice = SDL_AudioDeviceID; + void SDLCALL audio_callback(void* userdata, Uint8* stream, int len) { auto* queue = static_cast(userdata); std::memset(stream, 0, static_cast(len)); @@ -508,6 +617,112 @@ void SDLCALL audio_callback(void* userdata, Uint8* stream, int len) { if (take < requested && queue->started.load(std::memory_order_relaxed)) queue->underruns.fetch_add(1, std::memory_order_relaxed); } +#endif + +bool sdl_push_event(SDL_Event& event) { +#if defined(NDS_HAVE_SDL3) + return SDL_PushEvent(&event); +#else + return SDL_PushEvent(&event) == 1; +#endif +} + +bool sdl_init_frontend() { +#if defined(NDS_HAVE_SDL3) + return SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS | + SDL_INIT_GAMECONTROLLER); +#else + return SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS | + SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) == 0; +#endif +} + +bool sdl_set_thread_priority_high() { +#if defined(NDS_HAVE_SDL3) + return SDL_SetCurrentThreadPriority(SDL_THREAD_PRIORITY_HIGH); +#else + return SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) == 0; +#endif +} + +bool sdl_set_relative_mouse_mode(SDL_Window* window, bool enabled) { +#if defined(NDS_HAVE_SDL3) + return SDL_SetWindowRelativeMouseMode(window, enabled); +#else + (void)window; + return SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) == 0; +#endif +} + +SDL_Scancode sdl_event_scancode(const SDL_Event& event) { +#if defined(NDS_HAVE_SDL3) + return event.key.scancode; +#else + return event.key.keysym.scancode; +#endif +} + +void sdl_set_event_scancode(SDL_Event& event, SDL_Scancode key) { +#if defined(NDS_HAVE_SDL3) + event.key.scancode = key; + event.key.key = SDL_GetKeyFromScancode(key, SDL_KMOD_NONE, true); + event.key.down = event.type == SDL_KEYDOWN; +#else + event.key.keysym.scancode = key; + event.key.keysym.sym = SDL_GetKeyFromScancode(key); +#endif +} + +void sdl_set_mouse_button_state(SDL_Event& event, bool down) { +#if defined(NDS_HAVE_SDL3) + event.button.down = down; +#else + event.button.state = down ? SDL_PRESSED : SDL_RELEASED; +#endif +} + +bool sdl_window_event_is(const SDL_Event& event, Uint32 window_event) { +#if defined(NDS_HAVE_SDL3) + return event.type == window_event; +#else + return event.type == SDL_WINDOWEVENT && event.window.event == window_event; +#endif +} + +void sdl_make_window_event(SDL_Event& event, Uint32 window_event, + uint32_t window_id) { +#if defined(NDS_HAVE_SDL3) + event.type = window_event; +#else + event.type = SDL_WINDOWEVENT; + event.window.event = static_cast(window_event); +#endif + event.window.windowID = window_id; +} + +SDL_JoystickID sdl_controller_device_id(const SDL_Event& event) { +#if defined(NDS_HAVE_SDL3) + return event.gdevice.which; +#else + return event.cdevice.which; +#endif +} + +uint8_t sdl_controller_button(const SDL_Event& event) { +#if defined(NDS_HAVE_SDL3) + return event.gbutton.button; +#else + return event.cbutton.button; +#endif +} + +SDL_JoystickID sdl_controller_id(SDL_GameController* controller) { +#if defined(NDS_HAVE_SDL3) + return SDL_GetGamepadID(controller); +#else + return SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)); +#endif +} uint16_t key_bit(SDL_Scancode key) { // KEYINPUT/EXTKEYIN are active-low. This layout follows the common DS @@ -548,6 +763,22 @@ uint16_t controller_bit(SDL_GameControllerButton button) { } SDL_GameController* open_first_controller() { +#if defined(NDS_HAVE_SDL3) + int count = 0; + SDL_JoystickID* gamepads = SDL_GetGamepads(&count); + if (!gamepads) return nullptr; + SDL_GameController* controller = nullptr; + for (int index = 0; index < count && !controller; ++index) { + if (!SDL_IsGamepad(gamepads[index])) continue; + controller = SDL_GameControllerOpen(gamepads[index]); + } + SDL_free(gamepads); + if (controller) { + std::fprintf(stderr, "[sdl] Player 1 controller: %s\n", + SDL_GameControllerName(controller)); + } + return controller; +#else for (int index = 0; index < SDL_NumJoysticks(); ++index) { if (!SDL_IsGameController(index)) continue; if (SDL_GameController* controller = SDL_GameControllerOpen(index)) { @@ -557,16 +788,12 @@ SDL_GameController* open_first_controller() { } } return nullptr; +#endif } -void set_touch_from_mouse(int window_x, int window_y, bool down, +void set_touch_from_mouse(float x, float y, bool down, NdsScreenLayout layout, int logical_width) { - // SDL_RenderSetLogicalSize also maps absolute mouse events into the - // renderer's logical coordinate system. Calling RenderWindowToLogical a - // second time halves coordinates at 2x scale (and turns bottom-screen - // clicks into top-screen clicks), so consume the event coordinates as-is. - const float x = static_cast(window_x); - const float y = static_cast(window_y); + // Coordinates are expected in the renderer's logical DS-space. const float bottom_origin = layout == NdsScreenLayout::Separate ? 0.0f : kScreenHeight; const float left = @@ -583,15 +810,22 @@ void set_touch_from_mouse(int window_x, int window_y, bool down, nds_set_touch(touch_x, touch_y, true); } -uint32_t audio_queue_count(SDL_AudioDeviceID device, AudioQueue& queue) { +uint32_t audio_queue_count(const NdsAudioDevice& device, AudioQueue& queue) { if (!device) return 0; +#if defined(NDS_HAVE_SDL3) + (void)queue; + const int queued_bytes = SDL_GetAudioStreamQueued(device.stream); + return queued_bytes > 0 + ? static_cast(queued_bytes) / kAudioFrameBytes : 0; +#else SDL_LockAudioDevice(device); const uint32_t count = queue.count; SDL_UnlockAudioDevice(device); return count; +#endif } -uint32_t drain_audio(SDL_AudioDeviceID device, AudioQueue& queue, +uint32_t drain_audio(const NdsAudioDevice& device, AudioQueue& queue, bool throttle, uint32_t pace_floor, bool& queue_error) { if (!device) return 0; std::array samples{}; @@ -600,6 +834,19 @@ uint32_t drain_audio(SDL_AudioDeviceID device, AudioQueue& queue, if (!frames) break; bool pushed = false; while (!pushed) { +#if defined(NDS_HAVE_SDL3) + const uint32_t queued = audio_queue_count(device, queue); + if (kAudioCapacityFrames - queued >= frames) { + if (SDL_PutAudioStreamData(device.stream, samples.data(), + static_cast( + frames * kAudioFrameBytes))) { + pushed = true; + } else { + queue_error = true; + return audio_queue_count(device, queue); + } + } +#else SDL_LockAudioDevice(device); if (kAudioCapacityFrames - queue.count >= frames) { const uint32_t first = std::min( @@ -615,6 +862,7 @@ uint32_t drain_audio(SDL_AudioDeviceID device, AudioQueue& queue, pushed = true; } SDL_UnlockAudioDevice(device); +#endif if (!pushed) { if (!throttle) { queue_error = true; @@ -640,13 +888,63 @@ uint32_t drain_audio(SDL_AudioDeviceID device, AudioQueue& queue, return queued; } -void clear_audio_queue(SDL_AudioDeviceID device, AudioQueue& queue) { +void clear_audio_queue(const NdsAudioDevice& device, AudioQueue& queue) { if (!device) return; +#if defined(NDS_HAVE_SDL3) + SDL_ClearAudioStream(device.stream); + (void)queue; +#else SDL_LockAudioDevice(device); queue.read = 0; queue.write = 0; queue.count = 0; SDL_UnlockAudioDevice(device); +#endif +} + +void pause_audio(const NdsAudioDevice& device, bool paused) { + if (!device) return; +#if defined(NDS_HAVE_SDL3) + if (paused) SDL_PauseAudioStreamDevice(device.stream); + else SDL_ResumeAudioStreamDevice(device.stream); +#else + SDL_PauseAudioDevice(device, paused ? 1 : 0); +#endif +} + +void close_audio(NdsAudioDevice& device) { + if (!device) return; +#if defined(NDS_HAVE_SDL3) + SDL_DestroyAudioStream(device.stream); + device.stream = nullptr; +#else + SDL_CloseAudioDevice(device); + device = 0; +#endif +} + +NdsAudioDevice open_audio_device(AudioQueue& queue, + const SDL_AudioSpec& want) { +#if defined(NDS_HAVE_SDL3) + (void)queue; + NdsAudioDevice device{}; + device.stream = SDL_OpenAudioDeviceStream( + SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &want, nullptr, nullptr); + return device; +#else + SDL_AudioSpec got{}; + NdsAudioDevice device = SDL_OpenAudioDevice(nullptr, 0, &want, &got, 0); + if (device && (got.freq != want.freq || got.format != want.format || + got.channels != want.channels)) { + std::fprintf(stderr, + "[sdl] refusing mismatched audio format: want=%d/%u/%u " + "got=%d/%u/%u\n", + want.freq, want.format, want.channels, + got.freq, got.format, got.channels); + close_audio(device); + } + return device; +#endif } void discard_spu_output() { @@ -693,16 +991,18 @@ struct FrontendPresentation { int sample_scale = 1; }; +#if !defined(NDS_HAVE_SDL3) uint32_t fullscreen_flags(NdsFullscreenMode mode) { switch (mode) { case NdsFullscreenMode::Borderless: return SDL_WINDOW_FULLSCREEN_DESKTOP; case NdsFullscreenMode::Exclusive: - return SDL_WINDOW_FULLSCREEN; + return static_cast(SDL_WINDOW_FULLSCREEN); default: return 0; } } +#endif void destroy_presentation(FrontendPresentation& presentation) { for (SDL_Texture*& texture : presentation.sample_targets) { @@ -728,13 +1028,124 @@ void destroy_presentation(FrontendPresentation& presentation) { } SDL_Renderer* create_renderer(SDL_Window* window) { +#if defined(NDS_HAVE_SDL3) + SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr); + if (!renderer) + renderer = SDL_CreateRenderer(window, "software"); +#else SDL_Renderer* renderer = SDL_CreateRenderer( window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE); if (!renderer) renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE); +#endif return renderer; } +SDL_Window* create_window(const char* title, int x, int y, int w, int h, + uint64_t flags) { +#if defined(NDS_HAVE_SDL3) + SDL_Window* window = SDL_CreateWindow( + title, w, h, static_cast(flags)); + if (window) + SDL_SetWindowPosition(window, x, y); + return window; +#else + return SDL_CreateWindow(title, x, y, w, h, static_cast(flags)); +#endif +} + +bool set_window_fullscreen(SDL_Window* window, NdsFullscreenMode mode) { +#if defined(NDS_HAVE_SDL3) + return SDL_SetWindowFullscreen(window, mode != NdsFullscreenMode::Off); +#else + return SDL_SetWindowFullscreen(window, fullscreen_flags(mode)) == 0; +#endif +} + +bool set_render_logical_size(SDL_Renderer* renderer, int width, int height) { +#if defined(NDS_HAVE_SDL3) + return SDL_SetRenderLogicalPresentation( + renderer, width, height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); +#else + return SDL_RenderSetLogicalSize(renderer, width, height) == 0; +#endif +} + +void convert_mouse_event_to_logical_coordinates( + SDL_Event& event, const FrontendPresentation& presentation) { +#if defined(NDS_HAVE_SDL3) + auto renderer_for_window = [&](uint32_t window_id) -> SDL_Renderer* { + for (int screen = 0; screen < 2; ++screen) { + if (window_id == presentation.window_ids[screen]) + return presentation.renderers[screen]; + } + return nullptr; + }; + if (event.type == SDL_MOUSEBUTTONDOWN || + event.type == SDL_MOUSEBUTTONUP) { + if (SDL_Renderer* renderer = + renderer_for_window(event.button.windowID)) { + float x = event.button.x; + float y = event.button.y; + if (SDL_RenderCoordinatesFromWindow( + renderer, event.button.x, event.button.y, &x, &y)) { + event.button.x = x; + event.button.y = y; + } + } + } else if (event.type == SDL_MOUSEMOTION) { + if (SDL_Renderer* renderer = + renderer_for_window(event.motion.windowID)) { + float x = event.motion.x; + float y = event.motion.y; + if (SDL_RenderCoordinatesFromWindow( + renderer, event.motion.x, event.motion.y, &x, &y)) { + event.motion.x = x; + event.motion.y = y; + } + } + } +#else + (void)event; + (void)presentation; +#endif +} + +bool set_texture_scale_nearest(SDL_Texture* texture) { +#if defined(NDS_HAVE_SDL3) + return SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); +#else + return SDL_SetTextureScaleMode(texture, SDL_ScaleModeNearest) == 0; +#endif +} + +void render_texture(SDL_Renderer* renderer, SDL_Texture* texture, + const SDL_Rect* destination) { +#if defined(NDS_HAVE_SDL3) + SDL_FRect dst{}; + const SDL_FRect* dst_ptr = nullptr; + if (destination) { + dst.x = static_cast(destination->x); + dst.y = static_cast(destination->y); + dst.w = static_cast(destination->w); + dst.h = static_cast(destination->h); + dst_ptr = &dst; + } + SDL_RenderTexture(renderer, texture, nullptr, dst_ptr); +#else + SDL_RenderCopy(renderer, texture, nullptr, destination); +#endif +} + +void render_line(SDL_Renderer* renderer, int x1, int y1, int x2, int y2) { +#if defined(NDS_HAVE_SDL3) + SDL_RenderLine(renderer, static_cast(x1), static_cast(y1), + static_cast(x2), static_cast(y2)); +#else + SDL_RenderDrawLine(renderer, x1, y1, x2, y2); +#endif +} + bool create_presentation(const NdsFrontendOptions& options, FrontendPresentation& presentation, bool allow_gl_top = true) { @@ -774,13 +1185,13 @@ bool create_presentation(const NdsFrontendOptions& options, const int first_height = presentation.separate ? kScreenHeight * kWindowScale : kScreenHeight * 2 * kWindowScale; - const uint32_t top_window_flags = + const uint64_t top_window_flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | (presentation.gl_top - ? static_cast(SDL_WINDOW_OPENGL) : 0u); - presentation.windows[0] = SDL_CreateWindow( + ? static_cast(SDL_WINDOW_OPENGL) : 0u); + presentation.windows[0] = create_window( presentation.separate ? "ndsrecomp - Top Screen" - : "ndsrecomp firmware preview", + : "ndsrecomp firmware preview", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, (presentation.separate ? presentation.screen_widths[0] : presentation.canvas_width) * kWindowScale, @@ -803,7 +1214,7 @@ bool create_presentation(const NdsFrontendOptions& options, int top_x = 0; int top_y = 0; SDL_GetWindowPosition(presentation.windows[0], &top_x, &top_y); - presentation.windows[1] = SDL_CreateWindow( + presentation.windows[1] = create_window( "ndsrecomp - Bottom Screen", top_x + presentation.screen_widths[0] * kWindowScale + 32, top_y, @@ -832,8 +1243,7 @@ bool create_presentation(const NdsFrontendOptions& options, // Fullscreen is applied only after both separate-layout windows have their // final placement. The primary combined/top window is deliberately the // sole target so the separate touch window remains usable. - if (SDL_SetWindowFullscreen(presentation.windows[0], - fullscreen_flags(options.fullscreen)) != 0) { + if (!set_window_fullscreen(presentation.windows[0], options.fullscreen)) { std::fprintf(stderr, "[sdl] fullscreen (%s) failed: %s\n", nds_fullscreen_mode_name(options.fullscreen), SDL_GetError()); @@ -851,13 +1261,16 @@ bool create_presentation(const NdsFrontendOptions& options, !presentation.separate && screen == 0 ? kScreenHeight * 2 : kScreenHeight; if (screen == 0 || presentation.separate) { - SDL_RenderSetLogicalSize(presentation.renderers[screen], + set_render_logical_size( + presentation.renderers[screen], presentation.separate ? presentation.screen_widths[screen] : presentation.canvas_width, logical_height); +#if !defined(NDS_HAVE_SDL3) SDL_RenderSetIntegerScale(presentation.renderers[screen], SDL_TRUE); +#endif } presentation.textures[screen] = SDL_CreateTexture( presentation.renderers[screen], SDL_PIXELFORMAT_ARGB8888, @@ -869,8 +1282,7 @@ bool create_presentation(const NdsFrontendOptions& options, destroy_presentation(presentation); return false; } - if (SDL_SetTextureScaleMode(presentation.textures[screen], - SDL_ScaleModeNearest) != 0) { + if (!set_texture_scale_nearest(presentation.textures[screen])) { std::fprintf(stderr, "[sdl] texture scale mode failed: %s\n", SDL_GetError()); destroy_presentation(presentation); @@ -890,8 +1302,8 @@ bool create_presentation(const NdsFrontendOptions& options, destroy_presentation(presentation); return false; } - if (SDL_SetTextureScaleMode(presentation.sample_targets[screen], - SDL_ScaleModeNearest) != 0) { + if (!set_texture_scale_nearest( + presentation.sample_targets[screen])) { std::fprintf( stderr, "[sdl] supersample target scale mode failed: %s\n", @@ -914,11 +1326,11 @@ void render_screen(FrontendPresentation& presentation, int screen, SDL_SetRenderTarget(renderer, presentation.sample_targets[screen]); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); - SDL_RenderCopy(renderer, source, nullptr, nullptr); + render_texture(renderer, source, nullptr); SDL_SetRenderTarget(renderer, nullptr); source = presentation.sample_targets[screen]; } - SDL_RenderCopy(renderer, source, nullptr, &destination); + render_texture(renderer, source, &destination); } struct PresentationTicks { @@ -936,10 +1348,10 @@ void draw_virtual_stylus(SDL_Renderer* renderer, const SDL_Rect& destination, std::lround(stylus_y * destination.h / 192.0f)); SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); - SDL_RenderDrawLine(renderer, x - 5, y, x - 2, y); - SDL_RenderDrawLine(renderer, x + 2, y, x + 5, y); - SDL_RenderDrawLine(renderer, x, y - 5, x, y - 2); - SDL_RenderDrawLine(renderer, x, y + 2, x, y + 5); + render_line(renderer, x - 5, y, x - 2, y); + render_line(renderer, x + 2, y, x + 5, y); + render_line(renderer, x, y - 5, x, y - 2); + render_line(renderer, x, y + 2, x, y + 5); } PresentationTicks present_screens(FrontendPresentation& presentation, @@ -1046,20 +1458,21 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { // sleeps a full ~15.6 ms scheduler quantum, so the audio-queue throttle // overshoots every frame, pinning the loop at ~57 FPS and cyclically // starving the audio queue (the audible boot crackle). - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS | - SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0) { + if (!sdl_init_frontend()) { std::fprintf(stderr, "[sdl] init failed: %s\n", SDL_GetError()); return 1; } - if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) != 0) + if (!sdl_set_thread_priority_high()) std::fprintf(stderr, "[sdl] thread priority unchanged: %s\n", SDL_GetError()); +#if !defined(NDS_HAVE_SDL3) if (!SDL_SetHintWithPriority(SDL_HINT_RENDER_SCALE_QUALITY, "0", SDL_HINT_OVERRIDE)) { std::fprintf(stderr, "[sdl] render scale quality hint was not applied\n"); } +#endif FrontendPresentation presentation{}; if (!create_presentation(options, presentation)) { SDL_Quit(); @@ -1164,22 +1577,12 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { want.freq = kAudioFrequency; want.format = AUDIO_S16SYS; want.channels = 2; +#if !defined(NDS_HAVE_SDL3) want.samples = 1024; want.callback = audio_callback; want.userdata = &audio_queue; - SDL_AudioSpec got{}; - SDL_AudioDeviceID audio = SDL_OpenAudioDevice( - nullptr, 0, &want, &got, 0); - if (audio && (got.freq != want.freq || got.format != want.format || - got.channels != want.channels)) { - std::fprintf(stderr, - "[sdl] refusing mismatched audio format: want=%d/%u/%u " - "got=%d/%u/%u\n", - want.freq, want.format, want.channels, - got.freq, got.format, got.channels); - SDL_CloseAudioDevice(audio); - audio = 0; - } +#endif + NdsAudioDevice audio = open_audio_device(audio_queue, want); if (!audio) std::fprintf(stderr, "[sdl] audio unavailable: %s\n", SDL_GetError()); @@ -1222,7 +1625,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { SDL_GameController* controller = open_first_controller(); SDL_JoystickID controller_id = controller - ? SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)) + ? sdl_controller_id(controller) : -1; uint16_t keyboard_pressed = 0; uint16_t controller_pressed = 0; @@ -1382,7 +1785,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { }; auto release_relative_mouse = [&]() { if (relative_mouse.captured()) { - SDL_SetRelativeMouseMode(SDL_FALSE); + sdl_set_relative_mouse_mode(presentation.windows[0], false); SDL_CaptureMouse(SDL_FALSE); relative_mouse.release(); nds_set_touch(0, 0, false); @@ -1400,7 +1803,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { auto capture_relative_mouse = [&]() { if (!options.relative_mouse_touch || relative_mouse.captured()) return; - if (SDL_SetRelativeMouseMode(SDL_TRUE) != 0) { + if (!sdl_set_relative_mouse_mode(presentation.windows[0], true)) { std::fprintf(stderr, "[sdl] relative mouse capture failed: %s\n", SDL_GetError()); @@ -1537,16 +1940,16 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { SDL_Event injected{}; if (!selftest_key_down && counts.vblank9 >= 10) { injected.type = SDL_KEYDOWN; - injected.key.keysym.scancode = SDL_SCANCODE_Q; + sdl_set_event_scancode(injected, SDL_SCANCODE_Q); injected.key.repeat = 0; - selftest_event_error |= SDL_PushEvent(&injected) < 0; + selftest_event_error |= !sdl_push_event(injected); selftest_key_down = true; } else if (selftest_key_down && !selftest_key_up && counts.vblank9 >= 12) { injected.type = SDL_KEYUP; - injected.key.keysym.scancode = SDL_SCANCODE_Q; + sdl_set_event_scancode(injected, SDL_SCANCODE_Q); injected.key.repeat = 0; - selftest_event_error |= SDL_PushEvent(&injected) < 0; + selftest_event_error |= !sdl_push_event(injected); selftest_key_up = true; } if (!selftest_touch_down && g_insn_count[0] >= 42300000) { @@ -1554,13 +1957,14 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.type = SDL_MOUSEBUTTONDOWN; injected.button.windowID = presentation.window_ids[1]; injected.button.button = SDL_BUTTON_LEFT; - // SDL transforms window-tagged mouse events from physical - // window pixels into the renderer's logical coordinates. + sdl_set_mouse_button_state(injected, true); + // Synthetic events use window pixels; the SDL3 frontend path + // converts absolute mouse positions into logical DS-space. injected.button.x = (bottom_content_left + 127) * kWindowScale; injected.button.y = (presentation.separate ? 180 : 192 + 180) * kWindowScale; - selftest_event_error |= SDL_PushEvent(&injected) < 0; + selftest_event_error |= !sdl_push_event(injected); selftest_touch_down = true; } else if (selftest_touch_down && !selftest_touch_up && counts.vblank9 >= 116) { @@ -1568,11 +1972,12 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.type = SDL_MOUSEBUTTONUP; injected.button.windowID = presentation.window_ids[1]; injected.button.button = SDL_BUTTON_LEFT; + sdl_set_mouse_button_state(injected, false); injected.button.x = (bottom_content_left + 127) * kWindowScale; injected.button.y = (presentation.separate ? 180 : 192 + 180) * kWindowScale; - selftest_event_error |= SDL_PushEvent(&injected) < 0; + selftest_event_error |= !sdl_push_event(injected); selftest_touch_up = true; } } @@ -1593,6 +1998,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.type = SDL_MOUSEBUTTONDOWN; injected.button.windowID = presentation.window_ids[0]; injected.button.button = SDL_BUTTON_LEFT; + sdl_set_mouse_button_state(injected, true); // Stacked first verifies that the bottom logical screen // remains touch-only. Separate verifies the traditional // top-window capture path. @@ -1602,7 +2008,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.button.y = presentation.separate ? 96 * kWindowScale : (kScreenHeight + 96) * kWindowScale; - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + relative_mouse_selftest_error |= !sdl_push_event(injected); relative_mouse_selftest_stage = 1; } else if (relative_mouse_selftest_stage == 1 && shown_frames >= 3) { @@ -1619,23 +2025,25 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.type = SDL_MOUSEBUTTONUP; injected.button.windowID = presentation.window_ids[0]; injected.button.button = SDL_BUTTON_LEFT; + sdl_set_mouse_button_state(injected, false); injected.button.x = (bottom_content_left + 127) * kWindowScale; injected.button.y = (kScreenHeight + 96) * kWindowScale; relative_mouse_selftest_stage = 2; } - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + relative_mouse_selftest_error |= !sdl_push_event(injected); } else if (relative_mouse_selftest_stage == 2 && shown_frames >= 4) { relative_mouse_selftest_error |= mouse_down; injected.type = SDL_MOUSEBUTTONDOWN; injected.button.windowID = presentation.window_ids[0]; injected.button.button = SDL_BUTTON_LEFT; + sdl_set_mouse_button_state(injected, true); injected.button.x = (top_content_left + 127) * kWindowScale; injected.button.y = 96 * kWindowScale; - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + relative_mouse_selftest_error |= !sdl_push_event(injected); relative_mouse_selftest_stage = 3; } else if (relative_mouse_selftest_stage == 3 && shown_frames >= 5) { @@ -1644,7 +2052,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.motion.windowID = presentation.window_ids[0]; injected.motion.xrel = 20; injected.motion.yrel = -10; - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + relative_mouse_selftest_error |= !sdl_push_event(injected); relative_mouse_selftest_stage = 4; } else if (relative_mouse_selftest_stage == 4 && shown_frames >= 6) { @@ -1656,7 +2064,8 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.type = SDL_MOUSEBUTTONDOWN; injected.button.windowID = presentation.window_ids[0]; injected.button.button = SDL_BUTTON_LEFT; - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + sdl_set_mouse_button_state(injected, true); + relative_mouse_selftest_error |= !sdl_push_event(injected); relative_mouse_selftest_stage = 5; } else if (relative_mouse_selftest_stage == 5 && shown_frames >= 7) { @@ -1669,14 +2078,15 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.type = SDL_MOUSEBUTTONUP; injected.button.windowID = presentation.window_ids[0]; injected.button.button = SDL_BUTTON_LEFT; - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + sdl_set_mouse_button_state(injected, false); + relative_mouse_selftest_error |= !sdl_push_event(injected); relative_mouse_selftest_stage = 6; } else if (relative_mouse_selftest_stage == 6 && shown_frames >= 8) { relative_mouse_selftest_error |= mouse_pressed != 0; injected.type = SDL_KEYDOWN; - injected.key.keysym.scancode = SDL_SCANCODE_ESCAPE; - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + sdl_set_event_scancode(injected, SDL_SCANCODE_ESCAPE); + relative_mouse_selftest_error |= !sdl_push_event(injected); relative_mouse_selftest_stage = 7; } else if (relative_mouse_selftest_stage == 7 && shown_frames >= 9) { @@ -1685,18 +2095,19 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { injected.type = SDL_MOUSEBUTTONDOWN; injected.button.windowID = presentation.window_ids[0]; injected.button.button = SDL_BUTTON_LEFT; + sdl_set_mouse_button_state(injected, true); injected.button.x = (top_content_left + 127) * kWindowScale; injected.button.y = 96 * kWindowScale; - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + relative_mouse_selftest_error |= !sdl_push_event(injected); relative_mouse_selftest_stage = 8; } else if (relative_mouse_selftest_stage == 8 && shown_frames >= 10) { relative_mouse_selftest_error |= !relative_mouse.captured(); - injected.type = SDL_WINDOWEVENT; - injected.window.windowID = presentation.window_ids[0]; - injected.window.event = SDL_WINDOWEVENT_FOCUS_LOST; - relative_mouse_selftest_error |= SDL_PushEvent(&injected) < 0; + sdl_make_window_event( + injected, SDL_EVENT_WINDOW_FOCUS_LOST, + presentation.window_ids[0]); + relative_mouse_selftest_error |= !sdl_push_event(injected); relative_mouse_selftest_stage = 9; } else if (relative_mouse_selftest_stage == 9 && shown_frames >= 11) { @@ -1707,17 +2118,16 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { } SDL_Event event{}; while (SDL_PollEvent(&event)) { + convert_mouse_event_to_logical_coordinates(event, presentation); if (event.type == SDL_QUIT) { release_relative_mouse(); running = false; } - if (event.type == SDL_WINDOWEVENT && - event.window.event == SDL_WINDOWEVENT_CLOSE) { + if (sdl_window_event_is(event, SDL_EVENT_WINDOW_CLOSE_REQUESTED)) { release_relative_mouse(); running = false; } - if (event.type == SDL_WINDOWEVENT && - event.window.event == SDL_WINDOWEVENT_FOCUS_LOST && + if (sdl_window_event_is(event, SDL_EVENT_WINDOW_FOCUS_LOST) && is_presentation_window(event.window.windowID)) { const int index = presentation_window_focus_index(event.window.windowID); @@ -1730,8 +2140,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { clear_tab_turbo(); } } - if (event.type == SDL_WINDOWEVENT && - event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED && + if (sdl_window_event_is(event, SDL_EVENT_WINDOW_FOCUS_GAINED) && is_presentation_window(event.window.windowID)) { const int index = presentation_window_focus_index(event.window.windowID); @@ -1740,54 +2149,55 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { focus_release_pending = false; } if (event.type == SDL_KEYDOWN && !event.key.repeat) { - if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { + const SDL_Scancode scancode = sdl_event_scancode(event); + if (scancode == SDL_SCANCODE_ESCAPE) { if (relative_mouse.captured()) release_relative_mouse(); else running = false; } else if (process_mph_prime_key( - event.key.keysym.scancode, true, false)) { + scancode, true, false)) { // Consumed by the MPH-specific keyboard/mouse layer. } else if (options.tab_turbo && - event.key.keysym.scancode == SDL_SCANCODE_TAB) { + scancode == SDL_SCANCODE_TAB) { turbo_pressed = true; } else if (mph_prime_active()) { // Prime Controls replaces the normal keyboard keypad map; // unbound keys must not leak through as DS buttons. - } else if (const uint16_t bit = key_bit(event.key.keysym.scancode)) { + } else if (const uint16_t bit = key_bit(scancode)) { ++host_key_presses; keyboard_pressed |= bit; publish_keys(); } } if (event.type == SDL_KEYUP && !event.key.repeat) { + const SDL_Scancode scancode = sdl_event_scancode(event); if (process_mph_prime_key( - event.key.keysym.scancode, false, false)) { + scancode, false, false)) { // Consumed by the MPH-specific keyboard/mouse layer. } else if (options.tab_turbo && - event.key.keysym.scancode == SDL_SCANCODE_TAB) { + scancode == SDL_SCANCODE_TAB) { turbo_pressed = false; } else if (mph_prime_active()) { // See keydown path: ignore generic keyboard bindings // while the Prime Controls capture owns the keyboard. - } else if (const uint16_t bit = key_bit( - event.key.keysym.scancode)) { + } else if (const uint16_t bit = key_bit(scancode)) { keyboard_pressed &= static_cast(~bit); publish_keys(); } } if (event.type == SDL_CONTROLLERDEVICEADDED && !controller) { - controller = SDL_GameControllerOpen(event.cdevice.which); + controller = + SDL_GameControllerOpen(sdl_controller_device_id(event)); if (controller) { - controller_id = SDL_JoystickInstanceID( - SDL_GameControllerGetJoystick(controller)); + controller_id = sdl_controller_id(controller); std::fprintf(stderr, "[sdl] Player 1 controller: %s\n", SDL_GameControllerName(controller)); } } if (event.type == SDL_CONTROLLERDEVICEREMOVED && - controller && event.cdevice.which == controller_id) { + controller && sdl_controller_device_id(event) == controller_id) { SDL_GameControllerClose(controller); controller = nullptr; controller_id = -1; @@ -1799,7 +2209,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { } if (event.type == SDL_CONTROLLERBUTTONDOWN) { const auto button = static_cast( - event.cbutton.button); + sdl_controller_button(event)); if (process_mph_prime_pad(MphPadInputKind::Button, button, true)) { // Consumed by Prime Controls. @@ -1810,7 +2220,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { } if (event.type == SDL_CONTROLLERBUTTONUP) { const auto button = static_cast( - event.cbutton.button); + sdl_controller_button(event)); if (process_mph_prime_pad(MphPadInputKind::Button, button, false)) { // Consumed by Prime Controls. @@ -1946,8 +2356,8 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { nds_set_touch(relative_mouse.x(), relative_mouse.y(), true); } } - if (event.type == SDL_WINDOWEVENT && - event.window.event == SDL_WINDOWEVENT_LEAVE && mouse_down && + if (sdl_window_event_is(event, SDL_EVENT_WINDOW_MOUSE_LEAVE) && + mouse_down && event.window.windowID == presentation.window_ids[1]) { mouse_down = false; if (touch_frames_held < 2) @@ -2076,7 +2486,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { if (turbo_want != turbo_active) { turbo_active = turbo_want; if (audio) { - SDL_PauseAudioDevice(audio, 1); + pause_audio(audio, true); clear_audio_queue(audio, audio_queue); audio_queue.started.store(false, std::memory_order_relaxed); } @@ -2255,7 +2665,7 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { // Opening paused and prebuffering avoids the guaranteed startup // underrun produced by unpausing an empty SDL queue. audio_queue.started.store(true, std::memory_order_relaxed); - SDL_PauseAudioDevice(audio, 0); + pause_audio(audio, false); audio_started = true; audio_min_queue = queued; audio_pace_floor = audio_start_threshold; @@ -2323,10 +2733,10 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { static_cast(frequency); const uint64_t top_hash = framebuffer_rgb_fnv(0); const uint64_t bottom_hash = framebuffer_rgb_fnv(1); - if (audio) SDL_PauseAudioDevice(audio, 1); + if (audio) pause_audio(audio, true); const uint64_t audio_underruns = audio_queue.underruns.load(std::memory_order_relaxed); - if (audio) SDL_CloseAudioDevice(audio); + close_audio(audio); #if defined(NDS_HAVE_COMPUTE_RENDERER) nds_gpu2d_set_direct_present(false); nds_compute_host_stop(); @@ -2404,25 +2814,26 @@ int nds_run_interactive_frontend(const NdsFrontendOptions& options) { int nds_run_interactive_frontend(const NdsFrontendOptions&) { std::fprintf(stderr, - "[sdl] this runner was built without SDL2; install SDL2 and reconfigure\n"); + "[sdl] this runner was built without SDL; configure NDS_SDL_BACKEND=SDL3 " + "or SDL2 for interactive presentation\n"); return 1; } #endif bool nds_frontend_request_exit() { -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) if (!g_live_stats.active) return false; SDL_Event event{}; event.type = SDL_QUIT; - return SDL_PushEvent(&event) == 1; + return sdl_push_event(event); #else return false; #endif } bool nds_frontend_debug_key(const char* key_name, bool down) { -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) if (!g_input_debug.active || !key_name || key_name[0] == '\0') return false; const SDL_Scancode key = scancode_from_binding_name(key_name); @@ -2432,11 +2843,14 @@ bool nds_frontend_debug_key(const char* key_name, bool down) { SDL_Event event{}; event.type = down ? SDL_KEYDOWN : SDL_KEYUP; event.key.windowID = g_input_debug.top_window_id; +#if !defined(NDS_HAVE_SDL3) event.key.state = down ? SDL_PRESSED : SDL_RELEASED; +#else + event.key.down = down; +#endif event.key.repeat = 0; - event.key.keysym.scancode = key; - event.key.keysym.sym = SDL_GetKeyFromScancode(key); - const bool pushed = SDL_PushEvent(&event) == 1; + sdl_set_event_scancode(event, key); + const bool pushed = sdl_push_event(event); if (pushed) ++g_input_debug.debug_key_events; else ++g_input_debug.debug_event_errors; return pushed; @@ -2448,18 +2862,22 @@ bool nds_frontend_debug_key(const char* key_name, bool down) { } bool nds_frontend_debug_mouse_button(uint8_t button, bool down) { -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) if (!g_input_debug.active || button == 0) return false; SDL_Event event{}; event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; event.button.windowID = g_input_debug.top_window_id; event.button.button = button; +#if !defined(NDS_HAVE_SDL3) event.button.state = down ? SDL_PRESSED : SDL_RELEASED; +#else + event.button.down = down; +#endif event.button.clicks = 1; event.button.x = 128 * kWindowScale; event.button.y = 96 * kWindowScale; - const bool pushed = SDL_PushEvent(&event) == 1; + const bool pushed = sdl_push_event(event); if (pushed) ++g_input_debug.debug_mouse_button_events; else ++g_input_debug.debug_event_errors; return pushed; @@ -2471,7 +2889,7 @@ bool nds_frontend_debug_mouse_button(uint8_t button, bool down) { } bool nds_frontend_debug_mouse_motion(int dx, int dy) { -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) if (!g_input_debug.active) return false; SDL_Event event{}; @@ -2481,7 +2899,7 @@ bool nds_frontend_debug_mouse_motion(int dx, int dy) { event.motion.y = 96 * kWindowScale; event.motion.xrel = dx; event.motion.yrel = dy; - const bool pushed = SDL_PushEvent(&event) == 1; + const bool pushed = sdl_push_event(event); if (pushed) ++g_input_debug.debug_mouse_motion_events; else ++g_input_debug.debug_event_errors; return pushed; @@ -2493,14 +2911,18 @@ bool nds_frontend_debug_mouse_motion(int dx, int dy) { } bool nds_frontend_debug_touch(uint16_t x, uint16_t y, bool down) { -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) if (!g_input_debug.active || x >= kScreenWidth || y >= kScreenHeight) return false; SDL_Event event{}; event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; event.button.windowID = g_input_debug.bottom_window_id; event.button.button = SDL_BUTTON_LEFT; +#if !defined(NDS_HAVE_SDL3) event.button.state = down ? SDL_PRESSED : SDL_RELEASED; +#else + event.button.down = down; +#endif event.button.clicks = 1; event.button.x = (g_input_debug.bottom_content_left + static_cast(x)) * kWindowScale; @@ -2508,7 +2930,7 @@ bool nds_frontend_debug_touch(uint16_t x, uint16_t y, bool down) { : kScreenHeight + static_cast(y)) * kWindowScale; - const bool pushed = SDL_PushEvent(&event) == 1; + const bool pushed = sdl_push_event(event); if (pushed) ++g_input_debug.debug_touch_events; else ++g_input_debug.debug_event_errors; return pushed; @@ -2521,7 +2943,7 @@ bool nds_frontend_debug_touch(uint16_t x, uint16_t y, bool down) { } bool nds_frontend_debug_capture_mouse() { -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) if (!g_input_debug.active) return false; if (g_input_debug.relative_mouse_captured) @@ -2535,16 +2957,15 @@ bool nds_frontend_debug_capture_mouse() { } bool nds_frontend_debug_release_mouse() { -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) if (!g_input_debug.active) return false; if (!g_input_debug.relative_mouse_captured) return true; SDL_Event event{}; - event.type = SDL_WINDOWEVENT; - event.window.windowID = g_input_debug.top_window_id; - event.window.event = SDL_WINDOWEVENT_FOCUS_LOST; - const bool pushed = SDL_PushEvent(&event) == 1; + sdl_make_window_event(event, SDL_EVENT_WINDOW_FOCUS_LOST, + g_input_debug.top_window_id); + const bool pushed = sdl_push_event(event); if (pushed) ++g_input_debug.debug_release_events; else ++g_input_debug.debug_event_errors; return pushed; @@ -2556,7 +2977,7 @@ bool nds_frontend_debug_release_mouse() { void nds_frontend_live_stats(NdsFrontendLiveStats* out) { if (!out) return; *out = g_live_stats; -#if defined(NDS_HAVE_SDL2) +#if defined(NDS_HAVE_SDL3) || defined(NDS_HAVE_SDL2) if (g_live_stats.active) out->now_ticks = SDL_GetPerformanceCounter(); #endif diff --git a/runner/src/melonds_compute/ComputeHost.cpp b/runner/src/melonds_compute/ComputeHost.cpp index 1a4a8ee..66b976f 100644 --- a/runner/src/melonds_compute/ComputeHost.cpp +++ b/runner/src/melonds_compute/ComputeHost.cpp @@ -6,7 +6,12 @@ #include #define SDL_MAIN_HANDLED +#if defined(NDS_HAVE_SDL3) +#include +#include +#else #include +#endif #include "glad/glad.h" #include "gpu2d.h" @@ -30,6 +35,63 @@ GLuint g_hd_below_texture[2] = {}; int g_texture_width = 0; unsigned g_present_buffer = 0; +#if defined(NDS_HAVE_SDL3) +bool sdl_ok(bool result) +{ + return result; +} +#else +bool sdl_ok(int result) +{ + return result == 0; +} +#endif + +SDL_Window* create_gl_window(const char* title, int width, int height, + uint64_t flags) +{ +#if defined(NDS_HAVE_SDL3) + return SDL_CreateWindow(title, width, height, + static_cast(flags)); +#else + return SDL_CreateWindow(title, SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, width, height, + static_cast(flags)); +#endif +} + +bool sdl_init_video() +{ +#if defined(NDS_HAVE_SDL3) + return SDL_InitSubSystem(SDL_INIT_VIDEO); +#else + return SDL_InitSubSystem(SDL_INIT_VIDEO) == 0; +#endif +} + +bool sdl_make_current(SDL_Window* window, SDL_GLContext context) +{ + return sdl_ok(SDL_GL_MakeCurrent(window, context)); +} + +void sdl_destroy_context(SDL_GLContext context) +{ +#if defined(NDS_HAVE_SDL3) + SDL_GL_DestroyContext(context); +#else + SDL_GL_DeleteContext(context); +#endif +} + +void sdl_get_drawable_size(SDL_Window* window, int* width, int* height) +{ +#if defined(NDS_HAVE_SDL3) + SDL_GetWindowSizeInPixels(window, width, height); +#else + SDL_GL_GetDrawableSize(window, width, height); +#endif +} + struct PresentViewport { int x = 0; int y = 0; @@ -444,11 +506,11 @@ void configure_layer_texture(GLuint texture, int width) void release_host_objects() { if (g_context && g_window) { - SDL_GL_MakeCurrent(g_window, g_context); + sdl_make_current(g_window, g_context); stop_presenter(); } if (g_context) nds_texture_upscale_shutdown(); - if (g_context) SDL_GL_DeleteContext(g_context); + if (g_context) sdl_destroy_context(g_context); if (g_window && g_owns_window) SDL_DestroyWindow(g_window); if (g_owns_video) SDL_QuitSubSystem(SDL_INIT_VIDEO); g_window = nullptr; @@ -478,7 +540,7 @@ bool nds_compute_host_start(SDL_Window* presentation_window) SDL_SetMainReady(); g_owns_video = (SDL_WasInit(SDL_INIT_VIDEO) == 0); - if (g_owns_video && SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { + if (g_owns_video && !sdl_init_video()) { std::fprintf(stderr, "[gpu3d] SDL video init failed: %s\n", SDL_GetError()); return fail_or_fallback("SDL video initialization"); @@ -492,9 +554,8 @@ bool nds_compute_host_start(SDL_Window* presentation_window) g_window = presentation_window; g_visible = presentation_window != nullptr; if (!g_window) { - g_window = SDL_CreateWindow( - "ndsrecomp compute context", SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, 1, 1, + g_window = create_gl_window( + "ndsrecomp compute context", 1, 1, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); g_owns_window = true; if (!g_window) { @@ -506,7 +567,7 @@ bool nds_compute_host_start(SDL_Window* presentation_window) } g_context = SDL_GL_CreateContext(g_window); SDL_GL_ResetAttributes(); - if (!g_context || SDL_GL_MakeCurrent(g_window, g_context) != 0) { + if (!g_context || !sdl_make_current(g_window, g_context)) { std::fprintf(stderr, "[gpu3d] compute GL 4.3 context failed: %s\n", SDL_GetError()); return fail_or_fallback("OpenGL 4.3 context creation"); @@ -541,7 +602,7 @@ bool nds_compute_host_start(SDL_Window* presentation_window) bool nds_compute_host_make_current() { - return !g_active || SDL_GL_MakeCurrent(g_window, g_context) == 0; + return !g_active || sdl_make_current(g_window, g_context); } bool nds_compute_host_has_visible_context() @@ -556,7 +617,7 @@ bool nds_compute_host_present_top(const unsigned int* fallback_pixels, { if (!g_active || !g_visible || !g_present_program || !ticks) return false; - if (SDL_GL_MakeCurrent(g_window, g_context) != 0) return false; + if (!sdl_make_current(g_window, g_context)) return false; const int width = direct_frame ? direct_frame->width : fallback_width; if (width <= 0 || width > 448) return false; if (g_texture_width != width) { @@ -611,7 +672,7 @@ bool nds_compute_host_present_top(const unsigned int* fallback_pixels, start = SDL_GetPerformanceCounter(); int drawable_width = 0; int drawable_height = 0; - SDL_GL_GetDrawableSize(g_window, &drawable_width, &drawable_height); + sdl_get_drawable_size(g_window, &drawable_width, &drawable_height); const PresentViewport viewport = fit_present_viewport(drawable_width, drawable_height, width, 192); glClearColor(0.f, 0.f, 0.f, 1.f); @@ -683,7 +744,7 @@ void nds_compute_host_stop() release_host_objects(); return; } - SDL_GL_MakeCurrent(g_window, g_context); + sdl_make_current(g_window, g_context); nds_gpu3d_restore_soft_renderer(); release_host_objects(); }