Skip to content

Migration to SDL3.#2017

Open
nicolasnoble wants to merge 11 commits into
grumpycoders:mainfrom
nicolasnoble:sdl3
Open

Migration to SDL3.#2017
nicolasnoble wants to merge 11 commits into
grumpycoders:mainfrom
nicolasnoble:sdl3

Conversation

@nicolasnoble
Copy link
Copy Markdown
Member

Blocked on waiting for Ubuntu 26.04.1 so we get SDL3 in LTS.

nicolasnoble and others added 8 commits May 6, 2026 23:01
Drops in SDL_AudioStream (pull callback model) as the SPU output
backend, preserving the public API surface (feedStreamData,
getCurrentFrames, waitForGoal, getBackends, getDevices, reinit) so
the SPU and XA paths are unchanged.

NullSync is preserved via a dedicated timer thread, since SDL3 only
allows one audio driver loaded per process and we can't run a real
device alongside a null one the way MiniAudio did.

Backend and Device settings are read but the names won't round-trip
from prior MiniAudio values (case differences, WinMM->wmme, SDL's
device-name format). Settings silently reset to Default on first
launch; users re-pick from SPU configuration as needed.

Linux/macOS pull SDL3 from system packages (libsdl3-dev / brew);
Windows builds it from the third_party/SDL submodule already wired
in 2e945f6. The vsprojects side of this change is still pending.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
Swaps the gamepad polling backend in core/pad.cc to SDL_Gamepad,
inheriting SDL's gamecontrollerdb.txt mapping pipeline so controllers
GLFW didn't recognize (PS5, recent Xbox revs, etc.) work out of the
box. Hot-plug detection moves to draining SDL_EVENT_GAMEPAD_ADDED/
REMOVED on each poll, replacing GLFW's joystick callback.

Settings defaults migrated from GLFW_GAMEPAD_BUTTON_* to
SDL_GAMEPAD_BUTTON_*. Synthetic LEFT_TRIGGER/RIGHT_TRIGGER/INVALID
sentinels move to PCSX_GAMEPAD_BUTTON_* values that don't collide
with SDL's enum. The in-tree UI doesn't expose per-button rebinding,
so the only users affected by the enum-format change are those who
hand-edited their config JSON; their saved Controller_Pad* values
will not round-trip and will need to be re-set to defaults.

Keyboard input still flows through imgui_impl_glfw -> ImGui::IsKeyDown,
so Keyboard_* settings continue to use GLFW_KEY_* constants. Keyboard
moves with the window/ImGui-backend swap in Phase 3.

Rumble plumbing deferred per directive.

SDL event polling for gamepad hotplug currently lives inline in
Pad::getButtons(); will move to the central event pump when Phase 3
takes ownership of the event loop.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
keyboard input, and high-DPI plumbing.

Window/GL: SDL_CreateWindow + SDL_GL_CreateContext replace GLFW's
combined glfwCreateWindow. The 3.2-core / 3.0-fallback retry now
covers context creation since SDL splits window and context, and
destroys+recreates both on first failure rather than just the window.
SDL_WINDOW_HIGH_PIXEL_DENSITY makes the window/pixel split meaningful
on macOS Retina and Windows per-monitor DPI.

Event loop: GLFW callback installation (install_callbacks=true on the
imgui_impl_glfw side) replaced with an explicit SDL_PollEvent loop in
startFrame() that hands every event to ImGui_ImplSDL3_ProcessEvent
first, then dispatches close/resize/drop/key locally. Fixes the
recurring high-DPI bug: GLFW didn't fire when crossing monitors with
different DPI on Windows; SDL3 fires SDL_EVENT_WINDOW_DISPLAY_CHANGED,
which we now route to changeScale + a font rebuild.

ImGui backend: imgui_impl_glfw -> imgui_impl_sdl3, viewport callback
hooks (Platform_CreateWindow / OnChangedViewport / DestroyWindow)
preserved; the per-window key callback that the GLFW backend needed
is gone since the SDL3 backend dispatches via ProcessEvent.

Input mode: SDL_SetWindowRelativeMouseMode collapses GLFW's two-step
(CURSOR_DISABLED + RAW_MOUSE_MOTION) into one call. SDL3 fullscreen
with no mode set produces borderless desktop, matching GLFW's
glfwSetWindowMonitor(..., GLFW_DONT_CARE).

Keyboard: pad bindings now store SDL_Scancode values; SdlScancodeToImGuiKey
replaces GlfwKeyToImGuiKey, sdlScancodeToString replaces glfwKeyToString
using SDL_GetScancodeName for the layout-aware label. The keyboard
event signal carries SDL_Scancode in Events::Keyboard.scancode and
the rebinding UI reads .scancode (not .key) so writes and reads agree
on the integer's meaning.

NanoVG: cleanup ordering corrected so sub-contexts and the main
context are deleted while their owning GL context is still live; the
GLFW-era code freed them after glfwDestroyWindow, technically leaking
GPU resources.

Migration impact - keyboard bindings: settings serialize every field
including defaults, so all existing pcsx.json files carry seventeen
Keyboard_* keys with GLFW_KEY_* integers saved. After this change
those values get interpreted as SDL_Scancode integers, which produces
nonsense bindings on first launch with the new build. Per directive,
no in-place migration is provided; users will need to either delete
the pad config block from their pcsx.json or rebind in the SPU/Pad
configuration UI once. This is a one-time cost on the upgrade path.

Stale GLFW includes removed from src/gui/widgets/assembly.cc and
src/gui/widgets/zep.cc (no API calls were ever made through them,
just stray includes).

Makefile: glfw3 dropped from PACKAGES, imgui_impl_glfw.cpp swapped
for imgui_impl_sdl3.cpp in SRCS. Phase 4 (cleaning up the unused
miniaudio submodule and any final references) lands separately.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
This is the largest ImGui breaking-change set since 2015. The bump
is motivated by a multi-viewport regression that surfaced in Phase 3:
the main window stayed underneath all other viewports when focused.
v1.92 includes fixes for several multi-viewport ordering issues and
also reworks the font system in ways our pre-1.92 dynamic-DPI scaffold
no longer needs.

ImGui v1.91.5-docking -> v1.92.7-docking.

Project-side adjustments:

- forced-includes/imgui.h: define IM_ASSERT_USER_ERROR_RET and
  IM_ASSERT_USER_ERROR_RETV alongside the existing IM_ASSERT_USER_ERROR
  override. v1.92 added the new variants under the same #ifndef guard,
  so once we redirect the base macro we have to provide all three.

- gui.cc / gui.h: PushFont takes a required size argument in v1.92.
  useMainFont/useMonoFont now pass the active font's LegacySize so
  the per-scale font map continues to drive rendering size, matching
  pre-1.92 PushFont(font) semantics. Dropped ImGui_ImplOpenGL3_Create
  /DestroyFontsTexture and ImFontAtlas::Build() which v1.92 retired
  in favor of the dynamic-texture protocol. The reload pass now also
  sets Style.FontSizeBase to the active main font's LegacySize so
  default-context frames render at the right size on DPI flip.
  changeScale refreshes io.FontDefault and Style.FontSizeBase on every
  scale change, not only the first one.

- ram-viewer: ImFont::FindGlyph moved onto a per-size ImFontBaked,
  queried via font->GetFontBaked(GetFontSize()). Pre-fetch hex-glyph
  UVs and the font atlas TexRef in drawRAM() before ImGui::Render()
  rather than inside the AddCallback render closure, so the custom
  shader doesn't sample with stale UVs after a same-frame atlas grow.
  ImFont::ContainerAtlas was removed; we go through ImGui::GetIO()
  ->Fonts->TexRef.GetTexID() instead.

- vram-viewer, ram-viewer: IM_OFFSETOF -> offsetof.

- shader-editor: ImDrawCmd::TextureId -> TexRef.GetTexID().

Third-party touch-ups:

- ImFileDialog: ImFont::FontSize -> LegacySize at the loaded-size
  call sites; ImGuiContext::FontSize (a different field, still extant)
  left alone.

- imgui_lua_bindings: extend the perl generator's argument-type regex
  to accept ImTextureRef (constructs implicitly from ImTextureID, so
  the existing IM_TEXTURE_ID_ARG path still works at the call site).
  Regenerated imgui_iterator.inl from imgui.h via the project's
  documented procedure; restores Image / ImageWithBg / ImageButton
  bindings the prior generator dropped on v1.92's signatures.

- third_party/imgui_md (forked to grumpycoders/imgui_md): TexID ->
  TexRef.GetTexID() on the font-atlas demo image; .gitmodules URL
  updated.

- third_party/zep (already a grumpycoders fork): m_pFont->FontSize
  -> LegacySize.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Review Change Stack

Caution

Review failed

Failed to post review comments

📝 Walkthrough

Walkthrough

This PR migrates windowing/input from GLFW to SDL3, replaces the MiniAudio backend with an SDL-based audio implementation, updates ImGui integration for v1.92, and adjusts build/project files and widget rendering to match the new APIs.

Changes

SDL3 and Audio Migration

Layer / File(s) Summary
Build system and submodules
.gitmodules, Makefile, third_party/SDL_build_config.h, third_party/SDL, third_party/imgui
Add third_party/SDL, update imgui/imgui_md pins, swap Makefile package from glfw3sdl3, and add SDL build config header.
Visual Studio SDL project and wiring
vsprojects/SDL/*, vsprojects/*/*.vcxproj, vsprojects/pcsx-redux.sln
Add SDL.vcxproj and filters, update solution/project references to include SDL, remove GLFW NuGet target imports across projects, and swap ImGui project references to SDL3 backend.
GUI window/context and lifecycle (SDL3)
src/gui/gui.h, src/gui/gui.cc, src/gui/widgets/assembly.cc
Replace GLFW window/context with SDL_Window/SDL_GLContext, implement GL core-context attempt+fallback, manage multi-viewport windows and context switching with SDL, snapshot/restore fullscreen geometry, and reorder teardown.
Event loop and ImGui SDL3 backend
src/gui/gui.cc
Poll SDL events, forward to ImGui_ImplSDL3_ProcessEvent, translate SDL events to emulator signals, and switch per-frame backend calls to SDL3.
Input: gamepad and keyboard
src/core/pad.cc
Migrate input to SDL_Gamepad with handle array, parse controller DB via SDL IO, implement virtual trigger mapping, hotplug scanning via SDL events, and move keyboard bindings to SDL scancodes with scancode→ImGui mapping and UI names.
Audio backend (MiniAudio → SDLAudio)
src/spu/interface.h, src/spu/sdlaudio.h/.cc, src/spu/spu.cc, src/spu/xa.cc, removal of src/spu/miniaudio.cc
Add SDLAudio implementation with device enumeration, SDL_AudioStream mixing, frame timing and null-sync thread; update SPU code and XA codec to feed SDLAudio::Frame; remove miniaudio implementation.
ImGui v1.92 compatibility and bindings
src/forced-includes/imgui.h, third_party/imgui_lua_bindings/*, third_party/ImFileDialog/ImFileDialog.cpp
Redefine assertion macros with recoverable variants, update font usage to LegacySize, update Lua bindings to accept ImTextureRef and many new/renamed ImGui APIs, and adjust ICON_SIZE to LegacySize.
Widget rendering and texture/font handling
src/gui/widgets/ram-viewer.{h,cc}, src/gui/widgets/shader-editor.cc, src/gui/widgets/vram-viewer.cc, src/gui/widgets/zep.cc
Prefetch glyph UVs and font texture ID for RAM viewer render callbacks, use ImDrawCmd::TexRef.GetTexID(), and switch to offsetof() for ImDrawVert attributes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • grumpycoders/pcsx-redux#2003: RAM viewer glyph-caching implementation that this PR further adapts for ImGui v1.92 font texture handling and UV prefetching.

Poem

🐰 From GLFW fields to SDL's bright glade,
I hopped through code where contexts were made.
MiniAudio's hum now sings SDL's tune,
ImGui fonts align beneath the moon.
A tiny hop — and builds compile soon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Migration to SDL3.' accurately and clearly describes the primary change—migrating from GLFW/MiniAudio to SDL3 across windowing, input, and audio.
Description check ✅ Passed The description directly relates to the changeset by noting the migration is blocked on Ubuntu 26.04.1 for SDL3 in LTS, which contextualizes the PR's objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/core/pad.cc (3)

924-933: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't save scancodes that the runtime path can't resolve.

keyboardEvent() now stores any event.scancode, but getKeyboardButtons() silently ignores bindings whose SdlScancodeToImGuiKey() result is ImGuiKey_None. That lets users save dead bindings for keys outside the manual map.

💡 Minimal guard
 void PadsImpl::Pad::keyboardEvent(const PCSX::Events::Keyboard& event) {
     if (m_buttonToWait == -1) {
         return;
     }
+    if (SdlScancodeToImGuiKey(event.scancode) == ImGuiKey_None) {
+        return;
+    }
     // Bindings are stored as SDL_Scancode values to match the runtime lookup
     // path (SdlScancodeToImGuiKey) and the SDL_SCANCODE_* setting defaults.
     // event.key is an SDL_Keycode (layout-aware codepoint); event.scancode is
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/pad.cc` around lines 924 - 933, keyboardEvent currently writes
event.scancode into the binding unconditionally, which allows saving scancodes
that SdlScancodeToImGuiKey can't map (ImGuiKey_None). Before assigning to
getButtonFromGUIIndex(m_buttonToWait) in PadsImpl::Pad::keyboardEvent, call
SdlScancodeToImGuiKey(event.scancode) and only store the scancode if the result
!= ImGuiKey_None; otherwise clear m_buttonToWait without saving (or ignore the
input) so dead bindings are not persisted. Ensure you reference keyboardEvent,
getButtonFromGUIIndex, SdlScancodeToImGuiKey, ImGuiKey_None, and m_buttonToWait
when implementing the guard.

1031-1094: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Factor the mapping UI out of configure().

configure() is the third hotspot listed in the failing CodeScene gate. The repeated mapping-row rendering and the gamepad combo builder are good candidates for small helpers or a descriptor-driven loop so this stops blocking the PR on complexity alone.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/pad.cc` around lines 1031 - 1094, The configure() function is too
large; extract the repeated mapping-row code and the gamepad combo building into
small helpers to reduce complexity. Create a member/helper like
renderMappingRow(int guiIndex, int absIndex) that encapsulates the ImGui row
creation (uses m_buttonToWait, getButtonFromGUIIndex, sdlScancodeToString and
Push/PopStyleColor logic) and call it from configure() for both the button rows
and D-pad rows, and create a helper like buildAndRenderGamepadCombo(int& id,
const char*& preview) or renderGamepadCombo() that builds the gamepads vector
(reuse Entry, SDL_GetGamepadName, s_pads->m_gamepads) and performs the
ImGui::BeginCombo/Selectable logic while setting changed and id; keep all
existing state uses (m_buttonToWait,
m_settings.get<SettingControllerID>().value, selected) and ensure behavior and
preview selection remain identical.

500-621: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Split getButtons() before the hotspot gate keeps blocking the PR.

This function is also called out by the current CodeScene failure. Extracting the SDL hotplug drain, gamepad snapshot, and digital/analog post-processing into small helpers should get the complexity back under control without changing behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/pad.cc` around lines 500 - 621, The getButtons() routine is too
large and tripping the CodeScene hotspot check; extract the SDL hotplug/drain
and gamepad snapshot logic (the SDL_PumpEvents/SDL_PeepEvents block and the
m_gamepad capture/SDL_GetGamepad* reads) into a helper like drainHotplugEvents()
/ sampleGamepadState(GamepadState&), and extract the Digital/Analog
post-processing blocks (the PadType::Digital dpad-angle math and the
PadType::Analog axisToUint8 mapping and assignments) into helpers like
processDigitalDpad(bool buttons[16], const GamepadState&) and
processAnalogAxes(Pad& pad, const GamepadState&). Leave getKeyboardButtons()
as-is, and modify getButtons() to call these new helpers in sequence
(drainHotplugEvents(), sampleGamepadState(), then either processDigitalDpad() or
processAnalogAxes()) so behavior is unchanged but complexity is reduced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/pad.cc`:
- Around line 222-329: The SdlScancodeToImGuiKey function has excessive
cyclomatic complexity due to the large switch; replace it with a data-driven
lookup (e.g., a constexpr std::array/std::pair table or two small grouped helper
maps) that maps SDL scancodes to ImGuiKey values, and have SdlScancodeToImGuiKey
perform a O(1) lookup (with a default ImGuiKey_None fallback). Locate the
mapping inside the src/core/pad.cc file (symbols: SdlScancodeToImGuiKey) and
refactor the switch into a compile-time table (or grouped tables for contiguous
ranges like letters, numbers, keypad, function keys) to preserve current
behavior and return values, then update the function to consult that table and
return ImGuiKey_None when not found.
- Around line 71-74: scanGamepads() currently rebuilds m_gamepads[] densely
which shifts indices and breaks persisted SettingControllerID; instead store a
stable device identity (e.g. device instance GUID/ID from the SDL handle) in
settings and change scanGamepads() to populate m_gamepads by matching scanned
devices to those stored identities (leave slots empty if a saved device isn't
present, and place newly seen devices into the first free slot) so indices
remain stable; update map() to resolve the current SDL_Gamepad* handle for a
given SettingControllerID by looking up the saved device identity and finding
the matching live device handle during rescans.

In `@src/gui/gui.cc`:
- Around line 210-224: PCSX::GUI::setFullscreen currently always restores
position/size when leaving fullscreen but ignores whether the window was
maximized; update the else branch to restore maximized state by checking
m_windowMaximized and calling SDL_MaximizeWindow(m_window) when true (otherwise
leave as-is or call SDL_RestoreWindow if you need to ensure unmaximized state).
Modify PCSX::GUI::setFullscreen to perform this check after
SDL_SetWindowSize/Position so the window returns to its prior maximized state
using the existing m_windowMaximized flag.
- Around line 956-967: On window move/resize/maximize/restore events refresh the
cached windowed geometry (m_windowPosX, m_windowPosY, m_windowSizeX,
m_windowSizeY, m_windowMaximized) so saveCfg() later writes the current geometry
instead of stale values; inside the SDL event handling cases that affect
geometry (e.g., SDL_EVENT_WINDOW_MOVED, SDL_EVENT_WINDOW_RESIZED,
SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED, SDL_EVENT_WINDOW_MAXIMIZED,
SDL_EVENT_WINDOW_RESTORED, and similar where event.window.windowID ==
SDL_GetWindowID(m_window)) call SDL_GetWindowPosition(m_window, &x, &y),
SDL_GetWindowSize(m_window, &w, &h) and update m_windowPosX/Y = x/y,
m_windowSizeX/Y = w/h and set m_windowMaximized = (SDL_GetWindowFlags(m_window)
& SDL_WINDOW_MAXIMIZED) != 0; avoid overwriting when the window is currently
minimized or fullscreen if that behavior must be preserved.
- Around line 969-973: The SDL drop-file handler must explicitly free SDL's
dynamically-allocated drop payload rather than assuming it is freed on event
consumption: in the SDL_EVENT_DROP_FILE case, keep the existing null check and
call magicOpen(event.drop.data) when non-null, then always call
SDL_CleanupEvent(&event) afterwards to release event.drop.data; ensure
SDL_CleanupEvent is invoked whether or not magicOpen succeeds so there is no
per-drop leak (refer to the SDL_EVENT_DROP_FILE case, event.drop.data,
magicOpen, and SDL_CleanupEvent).

In `@src/gui/widgets/ram-viewer.cc`:
- Around line 392-423: The drawRAM() function has grown past the complexity
threshold due to the glyph prefetch block; extract that block into a new private
helper method (e.g., PCSX::Widgets::RAMViewer::refreshGlyphCache()) that
performs the ImGui font lookup, fills m_glyphUVs, sets m_glyphAspect and
m_fontTexID, and then call refreshGlyphCache() from drawRAM() in the same
location the block was removed from; ensure the helper uses ImGui::GetFont(),
ImGui::GetFontSize(), ImFontBaked::FindGlyph(), and updates the same member
variables (m_glyphUVs, m_glyphAspect, m_fontTexID) so behavior is unchanged.

In `@src/gui/widgets/shader-editor.cc`:
- Around line 755-759: The current cast at the texture extraction (GLuint
textureID = static_cast<GLuint>(cmd->TexRef.GetTexID())) can break on platforms
where ImTextureID is a pointer-sized type; change the conversion to use the
intptr_t bridge used elsewhere by casting via intptr_t before GLuint (i.e. use
(GLuint)(intptr_t)cmd->TexRef.GetTexID()); update the assignment at the
textureID creation in the shader-editor render loop (the line using
cmd->TexRef.GetTexID()) to match the ram-viewer pattern to ensure correct
cross-backend behavior.

In `@src/spu/sdlaudio.cc`:
- Around line 148-164: Currently failures from SDL_SetAudioStreamGetCallback or
SDL_BindAudioStream abort startup by throwing; instead follow the same
degraded-safe fallback used earlier: on failure of SDL_SetAudioStreamGetCallback
or SDL_BindAudioStream capture SDL_GetError(), clean up
(SDL_DestroyAudioStream(m_stream); m_stream=nullptr;
SDL_CloseAudioDevice(m_device); m_device=0), then switch to the dummy backend by
invoking init(true) or the existing degraded initialization path rather than
throwing; ensure you reference trampoline, m_stream and m_device when locating
the code so the error path mirrors the earlier init(true) retry behavior.
- Around line 43-49: The resume-recovery path in the
m_listener.listen<Events::ExecutionFlow::Run> handler uses uninit() and
init(true) when SDL_ResumeAudioDevice(m_device) fails but does not resume the
new device, so the replacement backend stays paused; change the recovery to call
SDL_ResumeAudioDevice(m_device) after init(true) (or call init(false) if init
has a parameter to leave device running) and only startNullThread() when
m_settings.get<NullSync>() is true, ensuring that when NullSync is off the newly
initialized device is resumed; apply the identical fix to the other recovery
block referenced (the one around lines 187-195) so both failure paths resume the
replacement device.
- Around line 59-169: The init() method is too large and failing CodeScene;
split its logic into clear helpers: extract the backend selection block (the
loop over m_backends, SDL_SetHint calls and m_settings.get<Backend>() handling)
into a helper like selectAudioBackend(), extract the device enumeration and open
logic (SDL_GetAudioPlaybackDevices, filling m_devices, chosen/deviceFound,
m_settings.get<Device>(), SDL_OpenAudioDevice) into enumerateAndOpenDevice(),
and extract stream creation and recovery (SDL_CreateAudioStream,
SDL_SetAudioStreamGetCallback, SDL_BindAudioStream and their error cleanup
paths) into setupStreamAndBind(); have init() call these helpers in order and
preserve existing error handling and state mutations (m_audioInitialized,
m_device, m_stream), referencing symbols PCSX::SPU::SDLAudio::init, m_backends,
m_settings.get<Backend>(), m_settings.get<Device>(), SDL_OpenAudioDevice,
SDL_CreateAudioStream, SDL_SetAudioStreamGetCallback, and SDL_BindAudioStream so
the behavior remains identical but with smaller, testable functions.
- Around line 254-276: In PCSX::SPU::SDLAudio::advanceFrames the code uses auto
total = m_frames.fetch_add(frameCount) which returns the pre-increment value, so
the goal comparison uses the old total and can miss an exact hit; change the
comparison to use the post-increment total (compute newTotal = total +
frameCount or call fetch_add and then add frameCount) when comparing against
m_goalpost and deciding to notify (affecting the branches that update
m_previousGoalpost, m_triggered and call
m_triggered.notify_one()/m_cv.notify_one()) so the wake happens immediately when
the increment reaches the goal.
- Around line 223-239: The mixed float samples from m_mixBuffers (loop over
STREAMS) can exceed [-1.0f,1.0f] and must be clamped before writing to
m_outputBuffer to avoid distortion; modify the mixing block in sdlaudio.cc (the
loop that computes l and r and writes to m_outputBuffer) to clamp lr/l/r to the
range -1.0f..1.0f (e.g. with std::clamp or a simple min/max) right after
computing the summed float(s) and before assigning into m_outputBuffer[f * 2 +
0/1].

In `@third_party/imgui_lua_bindings/imgui_iterator.inl`:
- Around line 1799-1800: The iterator marks ImGuiItemFlags as unsupported
because the bindings generator doesn't recognize that type; update
third_party/imgui_lua_bindings/generate_imgui_bindings.pl to treat
ImGuiItemFlags as a supported "flags" type: add "ImGuiItemFlags" to the
return-type handling so IMGUI_API ImGuiItemFlags GetItemFlags() is emitted (not
"Unsupported return type"), and add "ImGuiItemFlags" to the argument-type
whitelist/parsing used for function signatures so IMGUI_API void
PushItemFlag(ImGuiItemFlags option, bool enabled) is generated (not "Unsupported
arg type"); ensure the generator emits the corresponding Lua push/convert logic
for flags consistent with other flag types already supported in the script.

In `@third_party/SDL_build_config.h`:
- Line 32: The include in third_party/SDL_build_config.h references
<SDL3/SDL_platform_defines.h> which is missing; either restore the
SDL3/SDL_platform_defines.h file under third_party/SDL/include/SDL3 or change
the include to point to the actual header that exists in the repo (e.g., the
non-SDL3 path used by the vendor copy), and update the VS project include
settings in vsprojects/SDL/SDL.vcxproj to add third_party/SDL/include (in
addition to third_party/SDL/src) so the compiler can find the vendored headers;
modify third_party/SDL_build_config.h and vsprojects/SDL/SDL.vcxproj
accordingly.

---

Outside diff comments:
In `@src/core/pad.cc`:
- Around line 924-933: keyboardEvent currently writes event.scancode into the
binding unconditionally, which allows saving scancodes that
SdlScancodeToImGuiKey can't map (ImGuiKey_None). Before assigning to
getButtonFromGUIIndex(m_buttonToWait) in PadsImpl::Pad::keyboardEvent, call
SdlScancodeToImGuiKey(event.scancode) and only store the scancode if the result
!= ImGuiKey_None; otherwise clear m_buttonToWait without saving (or ignore the
input) so dead bindings are not persisted. Ensure you reference keyboardEvent,
getButtonFromGUIIndex, SdlScancodeToImGuiKey, ImGuiKey_None, and m_buttonToWait
when implementing the guard.
- Around line 1031-1094: The configure() function is too large; extract the
repeated mapping-row code and the gamepad combo building into small helpers to
reduce complexity. Create a member/helper like renderMappingRow(int guiIndex,
int absIndex) that encapsulates the ImGui row creation (uses m_buttonToWait,
getButtonFromGUIIndex, sdlScancodeToString and Push/PopStyleColor logic) and
call it from configure() for both the button rows and D-pad rows, and create a
helper like buildAndRenderGamepadCombo(int& id, const char*& preview) or
renderGamepadCombo() that builds the gamepads vector (reuse Entry,
SDL_GetGamepadName, s_pads->m_gamepads) and performs the
ImGui::BeginCombo/Selectable logic while setting changed and id; keep all
existing state uses (m_buttonToWait,
m_settings.get<SettingControllerID>().value, selected) and ensure behavior and
preview selection remain identical.
- Around line 500-621: The getButtons() routine is too large and tripping the
CodeScene hotspot check; extract the SDL hotplug/drain and gamepad snapshot
logic (the SDL_PumpEvents/SDL_PeepEvents block and the m_gamepad
capture/SDL_GetGamepad* reads) into a helper like drainHotplugEvents() /
sampleGamepadState(GamepadState&), and extract the Digital/Analog
post-processing blocks (the PadType::Digital dpad-angle math and the
PadType::Analog axisToUint8 mapping and assignments) into helpers like
processDigitalDpad(bool buttons[16], const GamepadState&) and
processAnalogAxes(Pad& pad, const GamepadState&). Leave getKeyboardButtons()
as-is, and modify getButtons() to call these new helpers in sequence
(drainHotplugEvents(), sampleGamepadState(), then either processDigitalDpad() or
processAnalogAxes()) so behavior is unchanged but complexity is reduced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4b68c645-4c3d-4705-ae06-4a4e76a6c644

📥 Commits

Reviewing files that changed from the base of the PR and between 96fa33c and 7aeee11.

📒 Files selected for processing (42)
  • .gitmodules
  • Makefile
  • src/core/pad.cc
  • src/forced-includes/imgui.h
  • src/gui/gui.cc
  • src/gui/gui.h
  • src/gui/widgets/assembly.cc
  • src/gui/widgets/ram-viewer.cc
  • src/gui/widgets/ram-viewer.h
  • src/gui/widgets/shader-editor.cc
  • src/gui/widgets/vram-viewer.cc
  • src/gui/widgets/zep.cc
  • src/spu/interface.h
  • src/spu/miniaudio.cc
  • src/spu/sdlaudio.cc
  • src/spu/sdlaudio.h
  • src/spu/spu.cc
  • src/spu/xa.cc
  • third_party/ImFileDialog/ImFileDialog.cpp
  • third_party/SDL
  • third_party/SDL_build_config.h
  • third_party/imgui
  • third_party/imgui_lua_bindings/generate_imgui_bindings.pl
  • third_party/imgui_lua_bindings/imgui_iterator.inl
  • third_party/imgui_md
  • third_party/zep
  • vsprojects/SDL/SDL.vcxproj
  • vsprojects/SDL/SDL.vcxproj.filters
  • vsprojects/SPU/SPU.vcxproj
  • vsprojects/SPU/SPU.vcxproj.filters
  • vsprojects/core/core.vcxproj
  • vsprojects/core/packages.config
  • vsprojects/gui/gui.vcxproj
  • vsprojects/gui/packages.config
  • vsprojects/imgui-glfw-ogl3/packages.config
  • vsprojects/imgui-sdl3-ogl3/imgui-sdl3-ogl3.vcxproj
  • vsprojects/imgui-sdl3-ogl3/imgui-sdl3-ogl3.vcxproj.filters
  • vsprojects/mainthunk/mainthunk.vcxproj
  • vsprojects/mainthunk/packages.config
  • vsprojects/pcsx-redux.sln
  • vsprojects/tests/pcsxrunner/packages.config
  • vsprojects/tests/pcsxrunner/pcsxrunner.vcxproj
💤 Files with no reviewable changes (10)
  • src/gui/widgets/assembly.cc
  • vsprojects/gui/packages.config
  • vsprojects/imgui-glfw-ogl3/packages.config
  • src/gui/widgets/zep.cc
  • vsprojects/core/packages.config
  • src/spu/miniaudio.cc
  • vsprojects/tests/pcsxrunner/packages.config
  • vsprojects/mainthunk/packages.config
  • vsprojects/core/core.vcxproj
  • vsprojects/gui/gui.vcxproj

Comment thread src/core/pad.cc
Comment on lines +71 to +74
// Open SDL gamepad handles, one per slot we've discovered. nullptr means the
// slot is unused. The slot index doubles as the user-visible "controller ID"
// saved in SettingControllerID.
SDL_Gamepad* m_gamepads[16] = {nullptr};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Persisting the dense scan index will reshuffle pads after hotplug.

SettingControllerID now represents the m_gamepads[] index, but scanGamepads() rebuilds that array from zero on every rescan. If pad 0 disappears, every higher pad shifts left on the next scan, so saved assignments can silently jump to a different physical controller. This needs a stable device identity for persistence, with the current handle resolved from that identity during map().

Also applies to: 380-397

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/pad.cc` around lines 71 - 74, scanGamepads() currently rebuilds
m_gamepads[] densely which shifts indices and breaks persisted
SettingControllerID; instead store a stable device identity (e.g. device
instance GUID/ID from the SDL handle) in settings and change scanGamepads() to
populate m_gamepads by matching scanned devices to those stored identities
(leave slots empty if a saved device isn't present, and place newly seen devices
into the first free slot) so indices remain stable; update map() to resolve the
current SDL_Gamepad* handle for a given SettingControllerID by looking up the
saved device identity and finding the matching live device handle during
rescans.

Comment thread src/core/pad.cc
Comment on lines +222 to +329
static ImGuiKey SdlScancodeToImGuiKey(int scancode) {
switch (scancode) {
case SDL_SCANCODE_TAB: return ImGuiKey_Tab;
case SDL_SCANCODE_LEFT: return ImGuiKey_LeftArrow;
case SDL_SCANCODE_RIGHT: return ImGuiKey_RightArrow;
case SDL_SCANCODE_UP: return ImGuiKey_UpArrow;
case SDL_SCANCODE_DOWN: return ImGuiKey_DownArrow;
case SDL_SCANCODE_PAGEUP: return ImGuiKey_PageUp;
case SDL_SCANCODE_PAGEDOWN: return ImGuiKey_PageDown;
case SDL_SCANCODE_HOME: return ImGuiKey_Home;
case SDL_SCANCODE_END: return ImGuiKey_End;
case SDL_SCANCODE_INSERT: return ImGuiKey_Insert;
case SDL_SCANCODE_DELETE: return ImGuiKey_Delete;
case SDL_SCANCODE_BACKSPACE: return ImGuiKey_Backspace;
case SDL_SCANCODE_SPACE: return ImGuiKey_Space;
case SDL_SCANCODE_RETURN: return ImGuiKey_Enter;
case SDL_SCANCODE_ESCAPE: return ImGuiKey_Escape;
case SDL_SCANCODE_APOSTROPHE: return ImGuiKey_Apostrophe;
case SDL_SCANCODE_COMMA: return ImGuiKey_Comma;
case SDL_SCANCODE_MINUS: return ImGuiKey_Minus;
case SDL_SCANCODE_PERIOD: return ImGuiKey_Period;
case SDL_SCANCODE_SLASH: return ImGuiKey_Slash;
case SDL_SCANCODE_SEMICOLON: return ImGuiKey_Semicolon;
case SDL_SCANCODE_EQUALS: return ImGuiKey_Equal;
case SDL_SCANCODE_LEFTBRACKET: return ImGuiKey_LeftBracket;
case SDL_SCANCODE_BACKSLASH: return ImGuiKey_Backslash;
case SDL_SCANCODE_RIGHTBRACKET: return ImGuiKey_RightBracket;
case SDL_SCANCODE_GRAVE: return ImGuiKey_GraveAccent;
case SDL_SCANCODE_CAPSLOCK: return ImGuiKey_CapsLock;
case SDL_SCANCODE_SCROLLLOCK: return ImGuiKey_ScrollLock;
case SDL_SCANCODE_NUMLOCKCLEAR: return ImGuiKey_NumLock;
case SDL_SCANCODE_PRINTSCREEN: return ImGuiKey_PrintScreen;
case SDL_SCANCODE_PAUSE: return ImGuiKey_Pause;
case SDL_SCANCODE_KP_0: return ImGuiKey_Keypad0;
case SDL_SCANCODE_KP_1: return ImGuiKey_Keypad1;
case SDL_SCANCODE_KP_2: return ImGuiKey_Keypad2;
case SDL_SCANCODE_KP_3: return ImGuiKey_Keypad3;
case SDL_SCANCODE_KP_4: return ImGuiKey_Keypad4;
case SDL_SCANCODE_KP_5: return ImGuiKey_Keypad5;
case SDL_SCANCODE_KP_6: return ImGuiKey_Keypad6;
case SDL_SCANCODE_KP_7: return ImGuiKey_Keypad7;
case SDL_SCANCODE_KP_8: return ImGuiKey_Keypad8;
case SDL_SCANCODE_KP_9: return ImGuiKey_Keypad9;
case SDL_SCANCODE_KP_PERIOD: return ImGuiKey_KeypadDecimal;
case SDL_SCANCODE_KP_DIVIDE: return ImGuiKey_KeypadDivide;
case SDL_SCANCODE_KP_MULTIPLY: return ImGuiKey_KeypadMultiply;
case SDL_SCANCODE_KP_MINUS: return ImGuiKey_KeypadSubtract;
case SDL_SCANCODE_KP_PLUS: return ImGuiKey_KeypadAdd;
case SDL_SCANCODE_KP_ENTER: return ImGuiKey_KeypadEnter;
case SDL_SCANCODE_KP_EQUALS: return ImGuiKey_KeypadEqual;
case SDL_SCANCODE_LSHIFT: return ImGuiKey_LeftShift;
case SDL_SCANCODE_LCTRL: return ImGuiKey_LeftCtrl;
case SDL_SCANCODE_LALT: return ImGuiKey_LeftAlt;
case SDL_SCANCODE_LGUI: return ImGuiKey_LeftSuper;
case SDL_SCANCODE_RSHIFT: return ImGuiKey_RightShift;
case SDL_SCANCODE_RCTRL: return ImGuiKey_RightCtrl;
case SDL_SCANCODE_RALT: return ImGuiKey_RightAlt;
case SDL_SCANCODE_RGUI: return ImGuiKey_RightSuper;
case SDL_SCANCODE_MENU: return ImGuiKey_Menu;
case SDL_SCANCODE_0: return ImGuiKey_0;
case SDL_SCANCODE_1: return ImGuiKey_1;
case SDL_SCANCODE_2: return ImGuiKey_2;
case SDL_SCANCODE_3: return ImGuiKey_3;
case SDL_SCANCODE_4: return ImGuiKey_4;
case SDL_SCANCODE_5: return ImGuiKey_5;
case SDL_SCANCODE_6: return ImGuiKey_6;
case SDL_SCANCODE_7: return ImGuiKey_7;
case SDL_SCANCODE_8: return ImGuiKey_8;
case SDL_SCANCODE_9: return ImGuiKey_9;
case SDL_SCANCODE_A: return ImGuiKey_A;
case SDL_SCANCODE_B: return ImGuiKey_B;
case SDL_SCANCODE_C: return ImGuiKey_C;
case SDL_SCANCODE_D: return ImGuiKey_D;
case SDL_SCANCODE_E: return ImGuiKey_E;
case SDL_SCANCODE_F: return ImGuiKey_F;
case SDL_SCANCODE_G: return ImGuiKey_G;
case SDL_SCANCODE_H: return ImGuiKey_H;
case SDL_SCANCODE_I: return ImGuiKey_I;
case SDL_SCANCODE_J: return ImGuiKey_J;
case SDL_SCANCODE_K: return ImGuiKey_K;
case SDL_SCANCODE_L: return ImGuiKey_L;
case SDL_SCANCODE_M: return ImGuiKey_M;
case SDL_SCANCODE_N: return ImGuiKey_N;
case SDL_SCANCODE_O: return ImGuiKey_O;
case SDL_SCANCODE_P: return ImGuiKey_P;
case SDL_SCANCODE_Q: return ImGuiKey_Q;
case SDL_SCANCODE_R: return ImGuiKey_R;
case SDL_SCANCODE_S: return ImGuiKey_S;
case SDL_SCANCODE_T: return ImGuiKey_T;
case SDL_SCANCODE_U: return ImGuiKey_U;
case SDL_SCANCODE_V: return ImGuiKey_V;
case SDL_SCANCODE_W: return ImGuiKey_W;
case SDL_SCANCODE_X: return ImGuiKey_X;
case SDL_SCANCODE_Y: return ImGuiKey_Y;
case SDL_SCANCODE_Z: return ImGuiKey_Z;
case SDL_SCANCODE_F1: return ImGuiKey_F1;
case SDL_SCANCODE_F2: return ImGuiKey_F2;
case SDL_SCANCODE_F3: return ImGuiKey_F3;
case SDL_SCANCODE_F4: return ImGuiKey_F4;
case SDL_SCANCODE_F5: return ImGuiKey_F5;
case SDL_SCANCODE_F6: return ImGuiKey_F6;
case SDL_SCANCODE_F7: return ImGuiKey_F7;
case SDL_SCANCODE_F8: return ImGuiKey_F8;
case SDL_SCANCODE_F9: return ImGuiKey_F9;
case SDL_SCANCODE_F10: return ImGuiKey_F10;
case SDL_SCANCODE_F11: return ImGuiKey_F11;
case SDL_SCANCODE_F12: return ImGuiKey_F12;
default: return ImGuiKey_None;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Break this mapping into data instead of a 100+ branch switch.

This helper is one of the functions named in the current CodeScene quality-gate failure. A constexpr lookup table or a couple of grouped helpers would preserve behavior while dropping the cyclomatic complexity back under the hotspot threshold.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/pad.cc` around lines 222 - 329, The SdlScancodeToImGuiKey function
has excessive cyclomatic complexity due to the large switch; replace it with a
data-driven lookup (e.g., a constexpr std::array/std::pair table or two small
grouped helper maps) that maps SDL scancodes to ImGuiKey values, and have
SdlScancodeToImGuiKey perform a O(1) lookup (with a default ImGuiKey_None
fallback). Locate the mapping inside the src/core/pad.cc file (symbols:
SdlScancodeToImGuiKey) and refactor the switch into a compile-time table (or
grouped tables for contiguous ranges like letters, numbers, keypad, function
keys) to preserve current behavior and return values, then update the function
to consult that table and return ImGuiKey_None when not found.

Comment thread src/gui/gui.cc
Comment on lines 210 to 224
void PCSX::GUI::setFullscreen(bool fullscreen) {
m_fullscreen = fullscreen;
if (fullscreen) {
glfwGetWindowPos(m_window, &m_glfwPosX, &m_glfwPosY);
glfwGetWindowSize(m_window, &m_glfwSizeX, &m_glfwSizeY);
const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor());
glfwSetWindowMonitor(m_window, glfwGetPrimaryMonitor(), 0, 0, mode->width, mode->height, GLFW_DONT_CARE);
// Snapshot windowed pos/size before the transition so we can restore them.
SDL_GetWindowPosition(m_window, &m_windowPosX, &m_windowPosY);
SDL_GetWindowSize(m_window, &m_windowSizeX, &m_windowSizeY);
// No fullscreen mode set -> SDL gives us borderless desktop fullscreen,
// which is the shape glfwSetWindowMonitor(..., GLFW_DONT_CARE) produced.
SDL_SetWindowFullscreenMode(m_window, nullptr);
SDL_SetWindowFullscreen(m_window, true);
} else {
glfwSetWindowMonitor(m_window, nullptr, m_glfwPosX, m_glfwPosY, m_glfwSizeX, m_glfwSizeY, GLFW_DONT_CARE);
SDL_SetWindowFullscreen(m_window, false);
SDL_SetWindowPosition(m_window, m_windowPosX, m_windowPosY);
SDL_SetWindowSize(m_window, m_windowSizeX, m_windowSizeY);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore maximized state when leaving fullscreen.

If the window was maximized before entering fullscreen, this path always comes back as a normal window because it only restores position/size. m_windowMaximized is already persisted elsewhere, so the fullscreen toggle should preserve it too.

🪟 Suggested fix
 void PCSX::GUI::setFullscreen(bool fullscreen) {
     m_fullscreen = fullscreen;
     if (fullscreen) {
+        m_windowMaximized = (SDL_GetWindowFlags(m_window) & SDL_WINDOW_MAXIMIZED) != 0;
         // Snapshot windowed pos/size before the transition so we can restore them.
         SDL_GetWindowPosition(m_window, &m_windowPosX, &m_windowPosY);
         SDL_GetWindowSize(m_window, &m_windowSizeX, &m_windowSizeY);
         // No fullscreen mode set -> SDL gives us borderless desktop fullscreen,
         // which is the shape glfwSetWindowMonitor(..., GLFW_DONT_CARE) produced.
         SDL_SetWindowFullscreenMode(m_window, nullptr);
         SDL_SetWindowFullscreen(m_window, true);
     } else {
         SDL_SetWindowFullscreen(m_window, false);
         SDL_SetWindowPosition(m_window, m_windowPosX, m_windowPosY);
         SDL_SetWindowSize(m_window, m_windowSizeX, m_windowSizeY);
+        if (m_windowMaximized) {
+            SDL_MaximizeWindow(m_window);
+        }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void PCSX::GUI::setFullscreen(bool fullscreen) {
m_fullscreen = fullscreen;
if (fullscreen) {
glfwGetWindowPos(m_window, &m_glfwPosX, &m_glfwPosY);
glfwGetWindowSize(m_window, &m_glfwSizeX, &m_glfwSizeY);
const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor());
glfwSetWindowMonitor(m_window, glfwGetPrimaryMonitor(), 0, 0, mode->width, mode->height, GLFW_DONT_CARE);
// Snapshot windowed pos/size before the transition so we can restore them.
SDL_GetWindowPosition(m_window, &m_windowPosX, &m_windowPosY);
SDL_GetWindowSize(m_window, &m_windowSizeX, &m_windowSizeY);
// No fullscreen mode set -> SDL gives us borderless desktop fullscreen,
// which is the shape glfwSetWindowMonitor(..., GLFW_DONT_CARE) produced.
SDL_SetWindowFullscreenMode(m_window, nullptr);
SDL_SetWindowFullscreen(m_window, true);
} else {
glfwSetWindowMonitor(m_window, nullptr, m_glfwPosX, m_glfwPosY, m_glfwSizeX, m_glfwSizeY, GLFW_DONT_CARE);
SDL_SetWindowFullscreen(m_window, false);
SDL_SetWindowPosition(m_window, m_windowPosX, m_windowPosY);
SDL_SetWindowSize(m_window, m_windowSizeX, m_windowSizeY);
}
void PCSX::GUI::setFullscreen(bool fullscreen) {
m_fullscreen = fullscreen;
if (fullscreen) {
m_windowMaximized = (SDL_GetWindowFlags(m_window) & SDL_WINDOW_MAXIMIZED) != 0;
// Snapshot windowed pos/size before the transition so we can restore them.
SDL_GetWindowPosition(m_window, &m_windowPosX, &m_windowPosY);
SDL_GetWindowSize(m_window, &m_windowSizeX, &m_windowSizeY);
// No fullscreen mode set -> SDL gives us borderless desktop fullscreen,
// which is the shape glfwSetWindowMonitor(..., GLFW_DONT_CARE) produced.
SDL_SetWindowFullscreenMode(m_window, nullptr);
SDL_SetWindowFullscreen(m_window, true);
} else {
SDL_SetWindowFullscreen(m_window, false);
SDL_SetWindowPosition(m_window, m_windowPosX, m_windowPosY);
SDL_SetWindowSize(m_window, m_windowSizeX, m_windowSizeY);
if (m_windowMaximized) {
SDL_MaximizeWindow(m_window);
}
}
}
🧰 Tools
🪛 Cppcheck (2.20.0)

[error] 210-210: failed to expand 'TYPESTRING', Invalid ## usage when expanding 'TYPESTRING1024'

(syntaxError)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/gui.cc` around lines 210 - 224, PCSX::GUI::setFullscreen currently
always restores position/size when leaving fullscreen but ignores whether the
window was maximized; update the else branch to restore maximized state by
checking m_windowMaximized and calling SDL_MaximizeWindow(m_window) when true
(otherwise leave as-is or call SDL_RestoreWindow if you need to ensure
unmaximized state). Modify PCSX::GUI::setFullscreen to perform this check after
SDL_SetWindowSize/Position so the window returns to its prior maximized state
using the existing m_windowMaximized flag.

Comment thread src/gui/gui.cc
Comment on lines +956 to +967
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (event.window.windowID == SDL_GetWindowID(m_window)) m_setupScreenSize = true;
break;
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
// Force a font/scale rebuild when the window crosses to a display
// with different DPI. This is the case GLFW didn't fire and is the
// recurring high-DPI bug we're fixing in this phase.
if (event.window.windowID == SDL_GetWindowID(m_window)) {
changeScale(SDL_GetWindowDisplayScale(m_window));
m_setupScreenSize = true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Refresh the cached windowed geometry on window-state events.

saveCfg() preserves m_windowPosX/Y, m_windowSizeX/Y, and m_windowMaximized whenever the window is minimized or fullscreen, but this switch never updates that cache during normal move/resize/maximize/restore activity. If the user changes the window geometry and quits while minimized/fullscreen, the old geometry is what gets written back.

🧭 Suggested fix
         switch (event.type) {
+            case SDL_EVENT_WINDOW_MOVED:
             case SDL_EVENT_WINDOW_RESIZED:
             case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
+            case SDL_EVENT_WINDOW_MAXIMIZED:
+            case SDL_EVENT_WINDOW_RESTORED:
                 if (event.window.windowID == SDL_GetWindowID(m_window)) m_setupScreenSize = true;
+                if (event.window.windowID == SDL_GetWindowID(m_window) &&
+                    (SDL_GetWindowFlags(m_window) & SDL_WINDOW_FULLSCREEN) == 0) {
+                    SDL_GetWindowPosition(m_window, &m_windowPosX, &m_windowPosY);
+                    SDL_GetWindowSize(m_window, &m_windowSizeX, &m_windowSizeY);
+                    m_windowMaximized = (SDL_GetWindowFlags(m_window) & SDL_WINDOW_MAXIMIZED) != 0;
+                }
                 break;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (event.window.windowID == SDL_GetWindowID(m_window)) m_setupScreenSize = true;
break;
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
// Force a font/scale rebuild when the window crosses to a display
// with different DPI. This is the case GLFW didn't fire and is the
// recurring high-DPI bug we're fixing in this phase.
if (event.window.windowID == SDL_GetWindowID(m_window)) {
changeScale(SDL_GetWindowDisplayScale(m_window));
m_setupScreenSize = true;
}
case SDL_EVENT_WINDOW_MOVED:
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
case SDL_EVENT_WINDOW_MAXIMIZED:
case SDL_EVENT_WINDOW_RESTORED:
if (event.window.windowID == SDL_GetWindowID(m_window)) m_setupScreenSize = true;
if (event.window.windowID == SDL_GetWindowID(m_window) &&
(SDL_GetWindowFlags(m_window) & SDL_WINDOW_FULLSCREEN) == 0) {
SDL_GetWindowPosition(m_window, &m_windowPosX, &m_windowPosY);
SDL_GetWindowSize(m_window, &m_windowSizeX, &m_windowSizeY);
m_windowMaximized = (SDL_GetWindowFlags(m_window) & SDL_WINDOW_MAXIMIZED) != 0;
}
break;
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
// Force a font/scale rebuild when the window crosses to a display
// with different DPI. This is the case GLFW didn't fire and is the
// recurring high-DPI bug we're fixing in this phase.
if (event.window.windowID == SDL_GetWindowID(m_window)) {
changeScale(SDL_GetWindowDisplayScale(m_window));
m_setupScreenSize = true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/gui.cc` around lines 956 - 967, On window
move/resize/maximize/restore events refresh the cached windowed geometry
(m_windowPosX, m_windowPosY, m_windowSizeX, m_windowSizeY, m_windowMaximized) so
saveCfg() later writes the current geometry instead of stale values; inside the
SDL event handling cases that affect geometry (e.g., SDL_EVENT_WINDOW_MOVED,
SDL_EVENT_WINDOW_RESIZED, SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED,
SDL_EVENT_WINDOW_MAXIMIZED, SDL_EVENT_WINDOW_RESTORED, and similar where
event.window.windowID == SDL_GetWindowID(m_window)) call
SDL_GetWindowPosition(m_window, &x, &y), SDL_GetWindowSize(m_window, &w, &h) and
update m_windowPosX/Y = x/y, m_windowSizeX/Y = w/h and set m_windowMaximized =
(SDL_GetWindowFlags(m_window) & SDL_WINDOW_MAXIMIZED) != 0; avoid overwriting
when the window is currently minimized or fullscreen if that behavior must be
preserved.

Comment thread src/gui/gui.cc
Comment on lines +969 to +973
case SDL_EVENT_DROP_FILE:
// SDL hands us a UTF-8 path that's owned by SDL and freed when
// the event is consumed; magicOpen copies what it needs.
if (event.drop.data) magicOpen(event.drop.data);
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In SDL3, after handling SDL_EVENT_DROP_FILE / SDL_DropEvent.data, does the application need to call SDL_free on the path buffer, or is it freed automatically when the event is consumed?

💡 Result:

In SDL3, after you handle SDL_EVENT_DROP_FILE / SDL_DropEvent.data (the file path string), your application must call SDL_free on that path buffer; it is not freed automatically just because the event has been consumed. SDL3’s SDL_DropEvent documents that the drop data is a pointer to a C string (const char *data) for SDL_EVENT_DROP_FILE, and SDL’s event lifetime is managed via an explicit cleanup function rather than by event “consumption” alone [1][2]. SDL also provides (and discusses) an event cleanup API (SDL_CleanupEvent) specifically because the library otherwise won’t know when an application is done with an event’s dynamic memory [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant region around the reported lines
sed -n '920,1020p' src/gui/gui.cc | nl -ba | sed -n '40,120p'

# Find definition/usages of magicOpen in the repo
rg -n "magicOpen\\b" -S src/gui/gui.cc src/gui -g'*.cc' -g'*.h' || true

# Print surrounding definition if found
# (Use rg to get file:line)
MAGIC_DEF=$(rg -n "magicOpen\\b" -S src/gui -g'*.h' -g'*.cc' | head -n 20 | cut -d: -f1 | head -n 1 || true)
if [ -n "${MAGIC_DEF:-}" ]; then
  echo "---- magicOpen candidate file: $MAGIC_DEF ----"
  sed -n '1,200p' "$MAGIC_DEF" | nl -ba | head -n 200
fi

Repository: grumpycoders/pcsx-redux

Length of output: 110


🌐 Web query:

SDL_EVENT_DROP_FILE SDL_DropEvent.data ownership must call SDL_free

💡 Result:

Yes—SDL’s drop events transfer ownership of the dropped filename/text string to the application, and you must free that memory with SDL_free once you’re done with it. SDL_DropEvent (SDL2) documents that the event contains a char* file “which should be freed with SDL_free” and that if the drop events are enabled “you must free the filename in the event.” [1] So, for SDL_EVENT_DROP_FILE/SDL_DROPFILE handling, treat event.drop.data (or the equivalent filename pointer in the drop event struct) as heap memory owned by SDL until you free it, then your code must call SDL_free(pointer) after processing the path. [1] Example pattern: - On receiving a drop-file event: copy event.drop.data / event.drop.file into your own buffer if needed, then call SDL_free on the SDL-provided pointer; do not keep using it after SDL_free. [1]

Citations:


🌐 Web query:

SDL_CleanupEvent SDL_EVENT_DROP_FILE data free

💡 Result:

In SDL3, the event type you care about is SDL_EVENT_DROP_FILE (part of SDL_DropEvent). The dropped filename is provided in SDL_DropEvent.data, and SDL has an API called SDL_CleanupEvent to release any dynamically allocated memory associated with events after you’re done processing them.[1][2][3] So to “SDL_CleanupEvent SDL_EVENT_DROP_FILE data free”: you generally do NOT manually free SDL_DropEvent.data yourself; instead, call SDL_CleanupEvent on the SDL_Event after handling SDL_EVENT_DROP_FILE, so SDL can free any internal allocations.[2][3] Practical pattern (SDL3): 1) Poll events. 2) When you get SDL_EVENT_DROP_FILE, read/use event.dropevent.data (the file name) immediately. 3) After processing that event, call SDL_CleanupEvent(&event) to clean up event-owned dynamic memory. Note: In SDL2, the older SDL_DropEvent has a char* file field that “should be freed with SDL_free” by the application, so the manual free behavior applies there, not SDL3.[4] This matches SDL’s documented reasoning: SDL3 introduced SDL_CleanupEvent specifically to free dynamically allocated memory in events for cases where applications don’t otherwise know when they’re done with event data.[5][2][3]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact code around the reported lines
echo "---- src/gui/gui.cc around 969-973 ----"
sed -n '940,1005p' src/gui/gui.cc | nl -ba

echo "---- Find magicOpen definition/usages ----"
rg -n "magicOpen\\b" -S src -g'*.cc' -g'*.h'

Repository: grumpycoders/pcsx-redux

Length of output: 168


🌐 Web query:

SDL3 SDL_DropEvent data ownership application must call SDL_free

💡 Result:

In SDL3, for SDL drop events of type SDL_EVENT_DROP_FILE, the event’s file/path memory is owned by the caller only after you receive the event: SDL allocates the string for you, and the application must free it with SDL_free once it’s done processing the drop event data.[1][2] Note: SDL3’s SDL_DropEvent struct uses const char data (rather than a mutable char), but the ownership rule still applies for the file name string carried by the drop event: you should copy it if you need to keep it beyond the scope of the event handling, and still release the SDL-provided storage via SDL_free after handling the event.[1][3] SDL2’s documentation states the same ownership rule explicitly for SDL_DropEvent: the char* file “should be freed with SDL_free”.[2]

Citations:


🌐 Web query:

SDL3 SDL_CleanupEvent SDL_EVENT_DROP_FILE SDL_DropEvent.data

💡 Result:

In SDL3, SDL_EVENT_DROP_FILE is delivered as SDL_EVENT_DROP_FILE inside SDL_DropEvent, and the dropped filename (for DROP_FILE) is provided in SDL_DropEvent.data. Relevant SDL3 definitions 1) Event type SDL_EVENT_DROP_FILE is one of the “Drag and drop events” in SDL3’s event type enum.[1][2] 2) SDL_DropEvent struct and SDL_DropEvent.data meaning In SDL3, SDL_DropEvent has a const char *data field whose meaning depends on the specific drop event type: - For SDL_EVENT_DROP_TEXT: data is the text - For SDL_EVENT_DROP_FILE: data is the file name - For the other drop sub-events: data is NULL This is stated directly in the SDL3 SDL_events.h struct documentation.[2] 3) Cleanup: SDL_CleanupEvent SDL3 introduced SDL_CleanupEvent to free memory associated with certain event types after handling them. SDL maintainers explicitly recommend calling SDL_CleanupEvent after handling SDL_EVENT_DROP_FILE (and SDL_EVENT_DROP_TEXT, etc.), to clean up the memory that SDL allocated for drop event data.[3][4] Practical implication After you handle an SDL_EVENT_DROP_FILE event in your SDL3 event loop, call SDL_CleanupEvent(event) (typically immediately after processing the event) to avoid leaking the memory associated with event.drop.data.[3][4] If you want the exact C field name to read: event->drop.data (const char *).[2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- src/gui/gui.cc around 940-1010 with line numbers (no nl) ----"
sed -n '940,1010p' src/gui/gui.cc | awk '{printf "%6d\t%s\n", NR+939, $0}'

echo "---- Look for SDL_CleanupEvent usage in repo ----"
rg -n "SDL_CleanupEvent\\b" -S src || true

echo "---- Look for SDL_EVENT_DROP_FILE handling & SDL_free in gui code ----"
rg -n "SDL_EVENT_DROP_FILE" -S src/gui || true
rg -n "SDL_free\\b" -S src/gui || true

Repository: grumpycoders/pcsx-redux

Length of output: 3929


🌐 Web query:

SDL3 SDL_DropEvent.data freed with SDL_free or via SDL_CleanupEvent

💡 Result:

For SDL3, SDL_DropEvent.data is dynamically allocated by SDL and should be released via SDL_CleanupEvent (not by calling SDL_free directly on the pointer). SDL_DropEvent in SDL3 exposes a const char *data field for SDL_EVENT_DROP_TEXT (text) and SDL_EVENT_DROP_FILE (file name) [1]. SDL added SDL_CleanupEvent specifically to free dynamically allocated memory associated with events [2][3]. The SDL3 drop-event documentation and header confirm that drop events carry text/filename data but do not indicate that the application should free that pointer manually; instead, the intended cleanup path for event-owned allocations is SDL_CleanupEvent [2][3]. Practical guidance: - After you finish processing an event (including drop events), call SDL_CleanupEvent(&event); this will free any heap memory SDL attached to that event [2][3]. - Only use SDL_free(event.drop.data) if SDL documentation for that specific field/event explicitly says it’s the app’s responsibility to free it; the SDL3 drop event field is covered by the general SDL_CleanupEvent mechanism [2][3].

Citations:


🌐 Web query:

SDL3 SDL_CleanupEvent frees memory for SDL_EVENT_DROP_FILE data

💡 Result:

Yes. In SDL3, SDL_CleanupEvent is intended to free any dynamically allocated memory associated with events, and this includes SDL_EVENT_DROP_FILE data. SDL_EVENT_DROP_FILE is one of the event types that can have heap-allocated memory associated with it when enabled (e.g., it allocates memory for each file path). [1][2] In response to the resulting memory leak, SDL3 introduced SDL_CleanupEvent, described as “used to free any dynamically allocated memory in events.” [3][1][4] Additionally, the SDL3 migration documentation explicitly notes that event memory is managed by SDL and that you should not free SDL_EVENT_DROP_FILE data yourself. [5] So, the correct pattern is: after you’ve finished processing an event (including SDL_EVENT_DROP_FILE), call SDL_CleanupEvent(&event) so SDL can release any internal allocations tied to that event. (If you never call it, you may leak memory for such events.) [2][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "magicOpen\\b" -S src | head -n 50
fd -t f "magicOpen*" src || true

Repository: grumpycoders/pcsx-redux

Length of output: 369


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '2670,2760p' src/gui/gui.cc | nl -ba

Repository: grumpycoders/pcsx-redux

Length of output: 110


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---- src/gui/gui.cc around magicOpen (2670-2760) ----"
sed -n '2670,2760p' src/gui/gui.cc | cat -n

Repository: grumpycoders/pcsx-redux

Length of output: 4506


🌐 Web query:

SDL3 SDL_DropEvent data const char *data freed with SDL_free or via SDL_CleanupEvent exact wording

💡 Result:

SDL3: The SDL_DropEvent.data pointer (const char *) is dynamically allocated by SDL, and is freed by calling SDL_CleanupEvent. SDL_DropEvent wording (SDL3): SDL_DropEvent has a member const char *data; described as “The text for SDL_EVENT_DROP_TEXT and the file name for SDL_EVENT_DROP_FILE, NULL for other events” [1][2]. SDL_CleanupEvent wording (SDL3): SDL3 added SDL_CleanupEvent “to free any dynamically allocated memory in events” [3]. The SDL3 event-memory-leak fix discussion also states that for SDL3, “we’ve added a function SDL_CleanupEvent … which will free the memory” and that apps can call it “after each event is processed and guarantee that no memory will be leaked” [4]. SDL_free vs SDL_CleanupEvent: There is no indication in the SDL3 drop-event docs that applications should free SDL_DropEvent.data with SDL_free; instead, SDL_CleanupEvent is the mechanism SDL provides to free dynamically allocated event payload memory [3][4]. So the exact guidance is: after handling an SDL event that may contain dynamically allocated payload (including SDL_DropEvent text/file data), call SDL_CleanupEvent to release that memory [3][4].

Citations:


🌐 Web query:

SDL3 SDL_CleanupEvent documentation SDL_EVENT_DROP_FILE frees data

💡 Result:

In SDL3, the memory associated with SDL_EVENT_DROP_FILE (and related drag-and-drop event payloads) is owned/managed by SDL, and you should not manually free the event’s data pointer. Instead, you should call SDL_CleanupEvent after handling the event so SDL can release the internal allocations. Evidence: - The SDL3 migration notes explicitly say “Event memory is now managed by SDL, so you should not free the data in SDL_EVENT_DROP_FILE”. It also notes the event data is dynamically allocated. [1] - SDL3 introduced SDL_CleanupEvent specifically “to… free the memory” for SDL3 events, and applications that call it after each event is processed “guarantee that no memory will be leaked” (with SDL_EVENT_DROP_FILE called out as part of that cleanup set). [2] - The SDL drop event structure shows that SDL_EVENT_DROP_FILE uses SDL_DropEvent::data as “the file name”. [3] So, for a typical SDL3 event loop: - When you receive an SDL_EVENT_DROP_FILE, read event.drop.data (const char*) for the file path. - Do not free it yourself. - After you finish processing the event, call SDL_CleanupEvent(&event) (or equivalent SDL3 API usage) to let SDL release the allocation tied to that event.

Citations:


Fix SDL3 drop-path lifetime handling (call SDL_CleanupEvent, don’t assume event “consumption” frees it).

In src/gui/gui.cc, the SDL_EVENT_DROP_FILE handler assumes SDL frees event.drop.data when the event is consumed, but SDL3’s dynamically allocated drop payload is released via SDL_CleanupEvent(&event). Without calling it, SDL_EVENT_DROP_FILE can leak memory per drop.

Minimal fix
         while (SDL_PollEvent(&event)) {
             ImGui_ImplSDL3_ProcessEvent(&event);
             switch (event.type) {
@@
                 case SDL_EVENT_DROP_FILE:
                     // SDL hands us a UTF-8 path that's owned by SDL and freed when
                     // the event is consumed; magicOpen copies what it needs.
                     if (event.drop.data) magicOpen(event.drop.data);
                     break;
@@
                 default:
                     break;
             }
+            SDL_CleanupEvent(&event);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/gui.cc` around lines 969 - 973, The SDL drop-file handler must
explicitly free SDL's dynamically-allocated drop payload rather than assuming it
is freed on event consumption: in the SDL_EVENT_DROP_FILE case, keep the
existing null check and call magicOpen(event.drop.data) when non-null, then
always call SDL_CleanupEvent(&event) afterwards to release event.drop.data;
ensure SDL_CleanupEvent is invoked whether or not magicOpen succeeds so there is
no per-drop leak (refer to the SDL_EVENT_DROP_FILE case, event.drop.data,
magicOpen, and SDL_CleanupEvent).

Comment thread src/spu/sdlaudio.cc
Comment on lines +148 to +164
if (!SDL_SetAudioStreamGetCallback(m_stream, trampoline, this)) {
const std::string err = SDL_GetError();
SDL_DestroyAudioStream(m_stream);
m_stream = nullptr;
SDL_CloseAudioDevice(m_device);
m_device = 0;
throw std::runtime_error("Unable to set SDL audio stream callback: " + err);
}

if (!SDL_BindAudioStream(m_device, m_stream)) {
const std::string err = SDL_GetError();
SDL_DestroyAudioStream(m_stream);
m_stream = nullptr;
SDL_CloseAudioDevice(m_device);
m_device = 0;
throw std::runtime_error("Unable to bind SDL audio stream: " + err);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the dummy-backend fallback for stream setup failures.

The non-safe path retries with init(true) for SDL init, device open, and stream creation failures, but callback registration and binding still throw immediately. That turns backend-specific setup failures here into startup aborts instead of the same degraded-safe behavior used earlier in init().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/spu/sdlaudio.cc` around lines 148 - 164, Currently failures from
SDL_SetAudioStreamGetCallback or SDL_BindAudioStream abort startup by throwing;
instead follow the same degraded-safe fallback used earlier: on failure of
SDL_SetAudioStreamGetCallback or SDL_BindAudioStream capture SDL_GetError(),
clean up (SDL_DestroyAudioStream(m_stream); m_stream=nullptr;
SDL_CloseAudioDevice(m_device); m_device=0), then switch to the dummy backend by
invoking init(true) or the existing degraded initialization path rather than
throwing; ensure you reference trampoline, m_stream and m_device when locating
the code so the error path mirrors the earlier init(true) retry behavior.

Comment thread src/spu/sdlaudio.cc
Comment on lines +223 to +239
for (uint32_t f = 0; f < chunk; f++) {
float l = 0.0f, r = 0.0f;
for (unsigned i = 0; i < STREAMS; i++) {
l += static_cast<float>(m_mixBuffers[i][f].L) /
static_cast<float>(std::numeric_limits<int16_t>::max());
r += static_cast<float>(m_mixBuffers[i][f].R) /
static_cast<float>(std::numeric_limits<int16_t>::max());
}

if (mono) {
const float lr = (l + r) * 0.5f;
m_outputBuffer[f * 2 + 0] = lr;
m_outputBuffer[f * 2 + 1] = lr;
} else {
m_outputBuffer[f * 2 + 0] = l;
m_outputBuffer[f * 2 + 1] = r;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp the mixed float output before passing it to SDL.

Each source is normalized and then summed, so voice + CDDA can exceed [-1.0f, 1.0f]. That will emit out-of-range float PCM and clip/distort on loud mixes.

Suggested fix
         for (uint32_t f = 0; f < chunk; f++) {
             float l = 0.0f, r = 0.0f;
             for (unsigned i = 0; i < STREAMS; i++) {
                 l += static_cast<float>(m_mixBuffers[i][f].L) /
                      static_cast<float>(std::numeric_limits<int16_t>::max());
                 r += static_cast<float>(m_mixBuffers[i][f].R) /
                      static_cast<float>(std::numeric_limits<int16_t>::max());
             }
 
             if (mono) {
-                const float lr = (l + r) * 0.5f;
+                const float lr = std::clamp((l + r) * 0.5f, -1.0f, 1.0f);
                 m_outputBuffer[f * 2 + 0] = lr;
                 m_outputBuffer[f * 2 + 1] = lr;
             } else {
-                m_outputBuffer[f * 2 + 0] = l;
-                m_outputBuffer[f * 2 + 1] = r;
+                m_outputBuffer[f * 2 + 0] = std::clamp(l, -1.0f, 1.0f);
+                m_outputBuffer[f * 2 + 1] = std::clamp(r, -1.0f, 1.0f);
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (uint32_t f = 0; f < chunk; f++) {
float l = 0.0f, r = 0.0f;
for (unsigned i = 0; i < STREAMS; i++) {
l += static_cast<float>(m_mixBuffers[i][f].L) /
static_cast<float>(std::numeric_limits<int16_t>::max());
r += static_cast<float>(m_mixBuffers[i][f].R) /
static_cast<float>(std::numeric_limits<int16_t>::max());
}
if (mono) {
const float lr = (l + r) * 0.5f;
m_outputBuffer[f * 2 + 0] = lr;
m_outputBuffer[f * 2 + 1] = lr;
} else {
m_outputBuffer[f * 2 + 0] = l;
m_outputBuffer[f * 2 + 1] = r;
}
for (uint32_t f = 0; f < chunk; f++) {
float l = 0.0f, r = 0.0f;
for (unsigned i = 0; i < STREAMS; i++) {
l += static_cast<float>(m_mixBuffers[i][f].L) /
static_cast<float>(std::numeric_limits<int16_t>::max());
r += static_cast<float>(m_mixBuffers[i][f].R) /
static_cast<float>(std::numeric_limits<int16_t>::max());
}
if (mono) {
const float lr = std::clamp((l + r) * 0.5f, -1.0f, 1.0f);
m_outputBuffer[f * 2 + 0] = lr;
m_outputBuffer[f * 2 + 1] = lr;
} else {
m_outputBuffer[f * 2 + 0] = std::clamp(l, -1.0f, 1.0f);
m_outputBuffer[f * 2 + 1] = std::clamp(r, -1.0f, 1.0f);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/spu/sdlaudio.cc` around lines 223 - 239, The mixed float samples from
m_mixBuffers (loop over STREAMS) can exceed [-1.0f,1.0f] and must be clamped
before writing to m_outputBuffer to avoid distortion; modify the mixing block in
sdlaudio.cc (the loop that computes l and r and writes to m_outputBuffer) to
clamp lr/l/r to the range -1.0f..1.0f (e.g. with std::clamp or a simple min/max)
right after computing the summed float(s) and before assigning into
m_outputBuffer[f * 2 + 0/1].

Comment thread src/spu/sdlaudio.cc
Comment on lines +254 to +276
void PCSX::SPU::SDLAudio::advanceFrames(uint32_t frameCount) {
m_frameCount.store(frameCount);

auto total = m_frames.fetch_add(frameCount);

#if HAS_ATOMIC_WAIT
auto goalpost = m_goalpost.load();
if (goalpost == m_previousGoalpost) return;

if (((int32_t)(goalpost - total)) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_triggered.notify_one();
#else
std::unique_lock<std::mutex> l(m_mu);
auto goalpost = m_goalpost;
if (goalpost == m_previousGoalpost) return;

if (((int32_t)(goalpost - total)) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_cv.notify_one();
#endif
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wake waitForGoal() on the post-increment frame total.

fetch_add(frameCount) returns the old value, but the goal comparison still uses that pre-increment total. Hitting the goal exactly therefore wakes one chunk/tick late.

Suggested fix
 void PCSX::SPU::SDLAudio::advanceFrames(uint32_t frameCount) {
     m_frameCount.store(frameCount);
 
-    auto total = m_frames.fetch_add(frameCount);
+    auto total = m_frames.fetch_add(frameCount) + frameCount;
 
 `#if` HAS_ATOMIC_WAIT
     auto goalpost = m_goalpost.load();
     if (goalpost == m_previousGoalpost) return;
 
-    if (((int32_t)(goalpost - total)) > 0) return;
+    if (static_cast<int32_t>(goalpost - total) > 0) return;
     m_previousGoalpost = goalpost;
     m_triggered++;
     m_triggered.notify_one();
 `#else`
     std::unique_lock<std::mutex> l(m_mu);
     auto goalpost = m_goalpost;
     if (goalpost == m_previousGoalpost) return;
 
-    if (((int32_t)(goalpost - total)) > 0) return;
+    if (static_cast<int32_t>(goalpost - total) > 0) return;
     m_previousGoalpost = goalpost;
     m_triggered++;
     m_cv.notify_one();
 `#endif`
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void PCSX::SPU::SDLAudio::advanceFrames(uint32_t frameCount) {
m_frameCount.store(frameCount);
auto total = m_frames.fetch_add(frameCount);
#if HAS_ATOMIC_WAIT
auto goalpost = m_goalpost.load();
if (goalpost == m_previousGoalpost) return;
if (((int32_t)(goalpost - total)) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_triggered.notify_one();
#else
std::unique_lock<std::mutex> l(m_mu);
auto goalpost = m_goalpost;
if (goalpost == m_previousGoalpost) return;
if (((int32_t)(goalpost - total)) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_cv.notify_one();
#endif
void PCSX::SPU::SDLAudio::advanceFrames(uint32_t frameCount) {
m_frameCount.store(frameCount);
auto total = m_frames.fetch_add(frameCount) + frameCount;
`#if` HAS_ATOMIC_WAIT
auto goalpost = m_goalpost.load();
if (goalpost == m_previousGoalpost) return;
if (static_cast<int32_t>(goalpost - total) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_triggered.notify_one();
`#else`
std::unique_lock<std::mutex> l(m_mu);
auto goalpost = m_goalpost;
if (goalpost == m_previousGoalpost) return;
if (static_cast<int32_t>(goalpost - total) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_cv.notify_one();
`#endif`
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/spu/sdlaudio.cc` around lines 254 - 276, In
PCSX::SPU::SDLAudio::advanceFrames the code uses auto total =
m_frames.fetch_add(frameCount) which returns the pre-increment value, so the
goal comparison uses the old total and can miss an exact hit; change the
comparison to use the post-increment total (compute newTotal = total +
frameCount or call fetch_add and then add frameCount) when comparing against
m_goalpost and deciding to notify (affecting the branches that update
m_previousGoalpost, m_triggered and call
m_triggered.notify_one()/m_cv.notify_one()) so the wake happens immediately when
the increment reaches the goal.

Comment on lines +1799 to +1800
// IMGUI_API ImGuiItemFlags GetItemFlags(); // get generic flags of last item
// Unsupported return type ImGuiItemFlags
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Unsupported ItemFlags bindings in generated iterator =="
rg -n "GetItemFlags|PushItemFlag|Unsupported return type ImGuiItemFlags|Unsupported arg type ImGuiItemFlags" third_party/imgui_lua_bindings/imgui_iterator.inl

echo
echo "== Current generator support for ImGuiItemFlags =="
rg -n "ImGuiItemFlags" third_party/imgui_lua_bindings/generate_imgui_bindings.pl

Repository: grumpycoders/pcsx-redux

Length of output: 844


Add ImGuiItemFlags handling to Lua bindings generator (GetItemFlags/PushItemFlag)

The generated iterator still marks both IMGUI_API ImGuiItemFlags GetItemFlags() (lines 1799-1800) and IMGUI_API void PushItemFlag(ImGuiItemFlags option, bool enabled) (lines ~396-397) as unsupported (Unsupported return type/arg type ImGuiItemFlags), and generate_imgui_bindings.pl contains no ImGuiItemFlags support, so these APIs are missing from Lua.

💡 Proposed fix (generator side)
  • Teach third_party/imgui_lua_bindings/generate_imgui_bindings.pl to recognize ImGuiItemFlags as a supported “flags” type:
    • Add ImGuiItemFlags to the return-type handling (so GetItemFlags no longer hits “Unsupported return type ImGuiItemFlags”).
    • Add ImGuiItemFlags to the argument-type parsing/whitelist used for functions like PushItemFlag (so it no longer hits “Unsupported arg type ImGuiItemFlags”).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@third_party/imgui_lua_bindings/imgui_iterator.inl` around lines 1799 - 1800,
The iterator marks ImGuiItemFlags as unsupported because the bindings generator
doesn't recognize that type; update
third_party/imgui_lua_bindings/generate_imgui_bindings.pl to treat
ImGuiItemFlags as a supported "flags" type: add "ImGuiItemFlags" to the
return-type handling so IMGUI_API ImGuiItemFlags GetItemFlags() is emitted (not
"Unsupported return type"), and add "ImGuiItemFlags" to the argument-type
whitelist/parsing used for function signatures so IMGUI_API void
PushItemFlag(ImGuiItemFlags option, bool enabled) is generated (not "Unsupported
arg type"); ensure the generator emits the corresponding Lua push/convert logic
for flags consistent with other flag types already supported in the script.

#define SDL_build_config_h_

/* General platform specific identifiers */
#include <SDL3/SDL_platform_defines.h>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where SDL_platform_defines.h exists and how includes are written nearby.
set -euo pipefail

echo "== Locate SDL_platform_defines.h candidates =="
fd -i 'SDL_platform_defines.h'

echo
echo "== Check include usage patterns for this header =="
rg -n --iglob '*.{h,hpp,c,cc,cpp,vcxproj,txt,mk}' 'SDL_platform_defines\.h|<SDL3/SDL_'

echo
echo "== Inspect SDL-related include dirs in project files =="
rg -n --iglob '*.vcxproj' --iglob 'Makefile' --iglob '*.mk' 'IncludePath|AdditionalIncludeDirectories|-I|SDL3'

Repository: grumpycoders/pcsx-redux

Length of output: 35965


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect third_party/SDL_build_config.h =="
nl -ba third_party/SDL_build_config.h | sed -n '1,120p'

echo
echo "== Locate SDL_platform_defines.h on disk (exact path check) =="
ls -la third_party/SDL/include/SDL3/SDL_platform_defines.h || true
test -f third_party/SDL/include/SDL3/SDL_platform_defines.h && echo "FOUND: third_party/SDL/include/SDL3/SDL_platform_defines.h" || echo "MISSING: third_party/SDL/include/SDL3/SDL_platform_defines.h"

echo
echo "== Find any SDL_platform_defines.h candidates =="
find third_party -type f -name 'SDL_platform_defines.h' -print | head -n 20
find . -type f -name 'SDL_platform_defines.h' -print | head -n 20

echo
echo "== Check include dirs in vsprojects/SDL/SDL.vcxproj =="
rg -n "AdditionalIncludeDirectories|IncludePath|-I" vsprojects/SDL/SDL.vcxproj || true

Repository: grumpycoders/pcsx-redux

Length of output: 173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect third_party/SDL_build_config.h (show lines 25-45) =="
sed -n '25,45p' third_party/SDL_build_config.h | cat -n

echo
echo "== Locate SDL_platform_defines.h on disk (exact path check) =="
ls -la third_party/SDL/include/SDL3/SDL_platform_defines.h 2>/dev/null || true
test -f third_party/SDL/include/SDL3/SDL_platform_defines.h && echo "FOUND: third_party/SDL/include/SDL3/SDL_platform_defines.h" || echo "MISSING: third_party/SDL/include/SDL3/SDL_platform_defines.h"

echo
echo "== Find any SDL_platform_defines.h candidates (limit output) =="
find third_party -type f -name 'SDL_platform_defines.h' -print | head -n 20
find . -type f -name 'SDL_platform_defines.h' -print | head -n 20

echo
echo "== Check include dirs in vsprojects/SDL/SDL.vcxproj =="
rg -n "AdditionalIncludeDirectories|IncludePath|-I" vsprojects/SDL/SDL.vcxproj || true

Repository: grumpycoders/pcsx-redux

Length of output: 1527


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ls third_party/SDL/include/SDL3/ (if present) =="
ls -la third_party/SDL/include/SDL3 2>/dev/null || echo "NO: third_party/SDL/include/SDL3"

echo
echo "== Any SDL_platform_defines.h anywhere in repo? (limit) =="
find . -type f -name 'SDL_platform_defines.h' -print | head -n 50

echo
echo "== Any SDL/include include dirs in vsprojects/SDL/SDL.vcxproj? =="
rg -n "third_party\\\\SDL\\\\include|third_party/SDL/include" vsprojects/SDL/SDL.vcxproj || true

echo
echo "== SDL_platform_defines.h references in SDL.vcxproj =="
rg -n "SDL_platform_defines\\.h" vsprojects/SDL/SDL.vcxproj || true

Repository: grumpycoders/pcsx-redux

Length of output: 6422


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "IncludePath|AdditionalIncludeDirectories|third_party\\\\SDL\\\\include" vsprojects/SDL/SDL.vcxproj || true

Repository: grumpycoders/pcsx-redux

Length of output: 6623


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "`#include` <SDL3/SDL_platform_defines\.h>" third_party/SDL_build_config.h

Repository: grumpycoders/pcsx-redux

Length of output: 109


Build blocker: missing SDL3/SDL_platform_defines.h at third_party/SDL_build_config.h:32

#include <SDL3/SDL_platform_defines.h> fails because third_party/SDL/include/SDL3/SDL_platform_defines.h is not present in the repo (empty find results). vsprojects/SDL/SDL.vcxproj still references that header under third_party/SDL/include/SDL3/, so SDL header vendoring/check-in is inconsistent.

  • Restore the missing SDL3 header (or update SDL_build_config.h to the actual header location/source of truth).
  • Add third_party/SDL/include to the compiler include paths (vcxproj currently adds only third_party/SDL/src).
🧰 Tools
🪛 Clang (14.0.6)

[error] 32-32: 'SDL3/SDL_platform_defines.h' file not found

(clang-diagnostic-error)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@third_party/SDL_build_config.h` at line 32, The include in
third_party/SDL_build_config.h references <SDL3/SDL_platform_defines.h> which is
missing; either restore the SDL3/SDL_platform_defines.h file under
third_party/SDL/include/SDL3 or change the include to point to the actual header
that exists in the repo (e.g., the non-SDL3 path used by the vendor copy), and
update the VS project include settings in vsprojects/SDL/SDL.vcxproj to add
third_party/SDL/include (in addition to third_party/SDL/src) so the compiler can
find the vendored headers; modify third_party/SDL_build_config.h and
vsprojects/SDL/SDL.vcxproj accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant