Add opt-in SDL3 window backend for Windows#310
Conversation
Make the SDL3 window backend (LLWindowSDL) buildable and runnable on Windows as a build-time opt-in (USE_SDL_WINDOW=ON); the native Win32 backend remains the default. Adds a vs2026-os-sdl preset. When enabled, Windows uses the same SDL main-callbacks entry point as Linux/macOS (llappviewersdl.cpp), but instantiates LLAppViewerWin32 so all Win32 platform overrides are retained (BugSplat, console, WER, velopack, NVAPI, single-instance mutex, FindWindow SLURL send). The native WINMAIN is carved out under !LL_SDL_WINDOW and its NVAPI session lifecycle is extracted into reusable helpers. LLWindowSDL gains Windows parity under LL_WINDOWS: a WndProc subclass to receive WM_COPYDATA second-instance SLURL hand-offs, DirectInput8 access for the joystick/SpaceNavigator path, a shared LLDXHardware::updateVRAMBudgetFromDXGI() VRAM budget query, and loading of the branded cursors from the exe's embedded .cur resources (handling 1bpp/8bpp/32bpp formats, hot-spots taken from the resources). The res-sdl BMP cursor path is now Linux-only and res-sdl is not shipped on Windows. The Win32 window/keyboard/dragdrop sources are excluded from SDL builds (llkeyboardwin32 can't compile under the SDL NATIVE_KEY_TYPE, and the factory never instantiates LLWindowWin32 there); lldxhardware stays compiled for both backends. Also fixes an AltGr LLWindowWin32 downcast in llviewerwindow that is invalid under the SDL backend. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LLWindowSDL's worker-thread shared GL contexts (texture upload, VBO
streaming) were each backed by a hidden 1x1 SDL_Window, which forced a
fragile main-thread-only deferred-window-destruction queue (SDL_DestroyWindow
isn't safe to call off the main thread on X11).
Replace that with platform-native context creation behind SDL:
* Windows — WGL sibling context on the main window's HDC
(wglCreateContextAttribsARB with version fallback), mirroring
LLWindowWin32.
* macOS — CGLCreateContext sharing the current CGLContextObj; bound
drawable-less (renders to FBOs).
* X11 — a 1x1 offscreen GLXPbuffer + glXCreateNewContext. Each worker
needs its own drawable (reusing the main window's would
BadAccess, as it's current on the main thread), but a pbuffer
touches no window manager and is destroyable from the worker.
* Wayland — a surfaceless EGL context (EGL_NO_SURFACE,
EGL_KHR_surfaceless_context), version requested explicitly with
eglBindAPI(EGL_OPENGL_API) re-asserted per worker thread.
GLX/EGL entry points are resolved via SDL_GL_GetProcAddress /
SDL_EGL_GetProcAddress (the viewer doesn't link libGL/libEGL under SDL); the
EGL display/config come from SDL_EGL_GetCurrentDisplay/Config. Each shared
context is an opaque heap LLSDLSharedContext; destroySharedContext now tears
down directly on the worker thread with no deferral. The live handles are
tracked only so destroyContext can reclaim any a worker failed to release,
replacing mOSRContexts/mDeadOSRWindows and the processMiscNativeEvents drain.
Only the WGL path is build-verified here; the GLX/EGL/CGL paths are written
against the API specs but compile-unverified without a Linux/macOS toolchain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SDL splash (LLSplashScreenSDL) was a no-op. Implement it like the Win32 splash loader: a small borderless, always-on-top window showing the branded app icon and an updatable status line while the viewer loads. It draws with a software renderer (SDL_CreateSoftwareRenderer on the window surface) rather than an accelerated one, so SDL doesn't spin up a GL/D3D device on the splash window that could collide with the main window's OpenGL context initialisation. Status text is rendered with SDL3_ttf using the bundled Inter variable font (WOFF2, decompressed by FreeType/brotli); the icon is loaded with SDL3_image from a branded PNG copied out of BRANDING_SOURCE_DIR into app_settings at configure time. The splash precedes createWindow()/init_sdl(), so showImpl() brings up SDL_INIT_VIDEO (reference counted) and TTF_Init(); hideImpl() releases exactly those, leaving the subsystem up for the main window. render() pumps events itself since no SDL event loop is running yet. Adds sdl3-ttf and the png feature of sdl3-image to the vcpkg manifest and links them through ll::SDL3. The implementation is platform-neutral, so it builds for the Linux and macOS SDL backends as well. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carry mouse scroll-wheel deltas as LLScrollDelta (integer mClicks plus a raw float mPrecise) end-to-end instead of an integer click count, so high-resolution wheels and touchpads deliver smooth sub-notch scrolling while discrete widgets keep their exact whole-notch behavior. - New LLScrollDelta type bundles the backend-accumulated integer notches with the raw per-event float delta, in coherent detent units (1.0 == one notch). - Thread it through the dispatch spine: LLWindowCallbacks, LLMouseHandler, and LLView (childrenHandleMouseEvent template unchanged), into LLViewerWindow. - SDL and Win32 backends emit both fields on every wheel event; the existing accumulators still drive mClicks, so discrete consumers are unaffected. - Camera zoom reads mPrecise for smooth zoom; LLAgentCamera and LLFollowCam take F32. Scale LLFollowCam's minimum-zoom floor by |z| so sub-notch events are not snapped up to the whole-notch minimum. - Discrete widgets (spin, menu, chiclet, fast-timer, emoji) read mClicks; chiclet gains a zero-guard for the new every-event dispatch. - Media scroll quantizes to mClicks at the plugin boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LLScrollbar accumulates the precise (sub-notch) scroll delta in an F32 residue and applies whole lines as the accumulator crosses an integer, carrying the remainder. mDocPos is integer "lines", so this is what lets high-resolution wheels and touchpads scroll smoothly instead of dropping everything below one whole notch; a whole notch still moves mStepSize lines exactly as before. Flows to every scrollbar-backed widget: scroll lists, scroll containers, accordions, hex editor, emoji picker, and fast-timer view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read the precise (sub-notch) scroll delta instead of integer clicks in the continuous-zoom handlers so high-resolution wheels and touchpads zoom smoothly: minimap (LLNetMap), world-map floater, mesh/image/anim upload previews, and mouselook FOV adjust (LLToolCompGun). These already used float zoom math, so a whole notch zooms exactly as before. LLSlider is intentionally left on integer clicks: it snaps to increments, so one notch == one increment remains correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New F32 setting (default 1.0) scaling the precise scroll component in LLViewerWindow, so it tunes every smooth path at once: camera zoom, smooth list/container scrolling, and minimap/world-map/preview zoom. Item steppers (spinners, menus, combo boxes) read the integer click count and are intentionally unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-introduce the numpad-distinct mechanism removed upstream in 2012 (9b27e32): LLKeyboard::e_numpad_distinct + the NumpadControl setting, so the numeric keypad emits distinct KEY_PAD_* codes and can drive the avatar/camera independently of the arrow keys and the digit row. - LLKeyboard: restore e_numpad_distinct enum + get/setNumpadDistinct. - SDL backend: translateNumpadKey honors the mode + NumLock against the raw SDLK_KP_* keysym, tried before the NumLock-fold fallback; symmetric on key up so levels do not stick. SDLK_KP_DIVIDE -> KEY_DIVIDE for Win32 parity (keeps numpad / driving start_gesture). - Win32 backend: restore the mode-aware translateExtendedKey (early numpad-map lookup + MASK_EXTENDED split) and the ND_NUMLOCK_ON guard in inverseTranslateExtendedKey. - Operators + - * / stay non-distinct; numpad +/- zoom via the existing KEY_ADD/KEY_SUBTRACT bindings (avoids the SDL TEXT_INPUT char path). - Numpad Enter stays aliased to Enter (keeps numpad-Enter-sends-chat). - llsdl.cpp: pin SDL_HINT_KEYCODE_OPTIONS (must not add hide_numpad) - numpad-distinct relies on receiving raw SDLK_KP_* keycodes. - NumpadControl S32 setting (default 1) + change listener and an initial sync in the LLViewerWindow ctor; default PAD_* bindings for first_person/third_person/sitting plus a Preferences combo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The include was dropped in the SDL backend cleanup, but gIconResource (declared in llwindowwin32.h) is still referenced by WINMAIN, which is compiled only when USE_SDL_WINDOW is off. Restore the include under that guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scrollByWheel returned changeLine()'s result, which is false until the residue crosses a whole line, so non-opaque nested scrollers forwarded sub-notch precise events to the parent and both scrolled on one gesture. Report the gesture as handled mid-content; keep chaining to the parent at the scroll boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The numpad-distinct decision read live NumLock at both key edges, so toggling NumLock while a numpad key was held left the down KEY's level stuck (e.g. avatar kept moving). Record the down translation and replay it on key-up. Win32 keys by scancode because the VK swaps with NumLock (VK_NUMPAD2 vs VK_DOWN); SDL keys by its stable keysym. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- TEXT_INPUT Ctrl-accelerator filter reads the keystroke-captured mKeyModifiers
instead of live SDL_GetModState(), so a fast Ctrl+A whose Ctrl-release lands
in the same pump can no longer leak its literal char into a focused field.
- getNativeAspectRatio guards an empty resolution list (was OOB resolutions[-1]
plus a divide-by-zero on a garbage height).
- Resize handlers (RESIZED and PIXEL_SIZE_CHANGED) floor width/height to 1px so a
0-dimension event never reaches handleResize as a degenerate 0x0 viewport.
- handleEvents drops events once mWindow is null (teardown), covering the
unguarded mWindow derefs in the focus/resize/DPI handlers.
- LLSplashScreenSDL tears its window down if the software renderer fails rather
than leaving a blank always-on-top window up for the whole load.
- flashIcon/maybeStopFlashIcon/gatherInput guard mWindow before SDL_FlashWindow.
- unsetenv("LD_PRELOAD") scoped to Linux (it was a no-op on the macOS branch;
macOS injects via DYLD_INSERT_LIBRARIES).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-event input handlers (mouse motion at up to the mouse poll rate, button, drop) and convertCoords each called SDL_GetWindowPixelDensity / SDL_GetWindowSizeInPixels, both of which hit GetClientRect (a USER32 syscall) on Windows — thousands per second on the render thread during mouselook. Cache the density and pixel height in refreshPixelMetrics(), refreshed at every resize / DPI / monitor change (createContext, switchContext, RESIZED, PIXEL_SIZE_CHANGED, DISPLAY_CHANGED, DISPLAY_SCALE_CHANGED) and reused by refreshMinSizePixelShadow. convertCoords keeps a live fallback until the first refresh. Events drain in order, so a resize/pixel-size event refreshes the cache before any later mouse event in the same pump — no stale coordinates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughCMake/presets and packaging add SDL configuration and splash assets; LLScrollDelta is introduced and propagated through window callbacks, UI controls, scrollbars, and viewer classes; precise wheel accumulation and sensitivity are added; numpad distinctness, bindings, and translation/state are implemented; Windows SDL runtime gets DXGI, DirectInput, splash, WM_COPYDATA, and shared GL context updates. ChangesSDL windowing and input migration
Sequence Diagram(s)sequenceDiagram
participant LLWindowSDL
participant LLWindowCallbacks
participant LLViewerWindow
participant LLScrollbar
participant LLAgentCamera
LLWindowSDL->>LLWindowCallbacks: handleScrollWheel(LLScrollDelta)
LLWindowCallbacks->>LLViewerWindow: handleScrollWheel(LLScrollDelta)
LLViewerWindow->>LLScrollbar: handleScrollWheel(LLScrollDelta)
LLViewerWindow->>LLAgentCamera: handleScrollWheel(delta.mPrecise)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
indra/llui/llscrollcontainer.cpp (1)
267-281:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve the scrollbar's unhandled result at the vertical boundary.
Line 281 always returns
true, even whenvertical->handleScrollWheel()returnedfalse. That defeats the new boundary-chaining behavior inLLScrollbar::scrollByWheel(), so an inner scroll container with a visible vertical bar will trap wheel events at top/bottom instead of letting the parent container continue scrolling.Suggested fix
LLScrollbar* vertical = mScrollbar[VERTICAL]; if (vertical->getVisible() && vertical->getEnabled()) { - // Pretend the mouse is over the scrollbar - if (vertical->handleScrollWheel( 0, 0, delta ) ) + const bool handled = vertical->handleScrollWheel(0, 0, delta); + if (handled) { updateScroll(); } - // Always eat the event - return true; + return handled; }🤖 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 `@indra/llui/llscrollcontainer.cpp` around lines 267 - 281, The vertical scrollbar block unconditionally returns true, swallowing wheel events even when vertical->handleScrollWheel(...) returns false; change it to capture the boolean result (e.g., bool handled = vertical->handleScrollWheel(0,0,delta)), call updateScroll() only if handled is true, and return handled so unhandled boundary results from LLScrollbar::scrollByWheel() can propagate to parent containers; reference mScrollbar[VERTICAL], LLScrollbar::handleScrollWheel, updateScroll, and LLScrollbar::scrollByWheel.
🤖 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 `@indra/llui/llscrollbar.cpp`:
- Around line 415-437: The handler currently advances mScrollWheelResidue before
checking boundary forwarding, leaving fractional residue when the event is
forwarded; fix by clearing that residue when you decide to forward at an end
stop: inside the branch after computing lines (the code around
mScrollWheelResidue, lines and changeLine(lines, true)), if you are going to
return false because the scroller is parked at the boundary (the cases using
precise>0 with mDocPos < getDocPosMax() or precise<0 with mDocPos > 0 that
currently return false), set mScrollWheelResidue = 0 (or otherwise revert the
fractional advance) before returning so the child doesn't keep leftover fraction
for subsequent gestures.
In `@indra/llui/llslider.cpp`:
- Around line 280-289: In LLSlider::handleScrollWheel, preserve high-precision
wheel input by using delta.mPrecise when delta.mClicks is zero instead of
discarding the motion: compute an effective delta (use delta.mClicks when
non-zero, otherwise delta.mPrecise) and multiply that by getIncrement() when
computing new_val, then call setValueAndCommit(new_val); apply the same change
to the other handleScrollWheel overload (the second occurrence around lines
291-300) so both vertical/horizontal paths use mPrecise as a fallback.
In `@indra/llwindow/lldxhardware.cpp`:
- Around line 489-539: Replace the unsafe reinterpret_cast and unchecked call to
QueryVideoMemoryInfo: call EnumAdapters to get an IDXGIAdapter*, then call
p_dxgi_adapter->QueryInterface(__uuidof(IDXGIAdapter3),
(void**)&p_dxgi_adapter3) to obtain a real IDXGIAdapter3* (and handle/release
interfaces correctly) instead of reinterpret_casting to IDXGIAdapter3; then
zero-initialize a DXGI_QUERY_VIDEO_MEMORY_INFO local (info = {}) and call
p_dxgi_adapter3->QueryVideoMemoryInfo(...), check the HRESULT and if it fails
log/continue and do not read info.Budget; on success compute budget_mb from
info.Budget and proceed to the existing VRAM logic, release p_dxgi_adapter3 when
done. Ensure any early exits still Release the original IDXGIAdapter* where
appropriate and remove the reinterpret_cast usage.
In `@indra/llwindow/llkeyboardsdl.cpp`:
- Around line 254-282: mapSDLtoWin currently only accepts the raw SDL key and
thus misses the NumLock-aware keypad folding you added in
translateNumpadKey/adjustNativekeyFromUnhandledMask; change mapSDLtoWin to
accept the event modifier state (e.g. add a MASK or SDL_Keymod parameter), and
inside mapSDLtoWin perform the same NumLock folding (mirror the logic from
adjustNativekeyFromUnhandledMask/translateNumpadKey so SDLK_KP_* keys yield
VK_DECIMAL or VK_0..VK_9 when SDL_KMOD_NUM is set and produce VK_LEFT/UP/etc
when not). Update every caller of mapSDLtoWin to pass the current mask (the
sites noted in the review), and keep the existing behavior for non-numpad keys;
ensure SDLK_KP_DECIMAL maps to VK_DECIMAL when NumLock is active and preserve
mNumpadKeyDown/mTranslateNumpadMap semantics in the forwarding path.
In `@indra/newview/llappviewersdl.cpp`:
- Around line 1026-1037: Replace the blocking SendMessage call with
SendMessageTimeout so the handoff can't hang and verify the receiver actually
handled WM_COPYDATA: build the same COPYDATASTRUCT, call
SendMessageTimeout(other_window, WM_COPYDATA, NULL, (LPARAM)&cds,
SMTO_ABORTIFHUNG, <timeout_ms e.g. 2000>, &dwResult), check that
SendMessageTimeout returned non-zero and that dwResult (the lpdwResult) is
non-zero before treating it as success; if either check fails, log the failure
(include getWindowTitle()) and return false instead of returning true. Ensure
you update the msg_result handling to use the SendMessageTimeout return and the
dwResult delivery check.
In `@indra/newview/llviewercontrol.cpp`:
- Around line 192-199: The handler handleNumpadControlChanged currently casts
newvalue.asInteger() directly to LLKeyboard::e_numpad_distinct and can pass
invalid values to gKeyboard->setNumpadDistinct; change it to validate/normalize
the integer first (e.g., clamp to the known enum range or map unknown values to
a safe default like LLKeyboard::NPAD_DISTINCT_OFF) and only call
gKeyboard->setNumpadDistinct with a verified LLKeyboard::e_numpad_distinct
value; reference gKeyboard, setNumpadDistinct, and LLKeyboard::e_numpad_distinct
when making the change so the cast is guarded and undefined modes cannot be set.
---
Outside diff comments:
In `@indra/llui/llscrollcontainer.cpp`:
- Around line 267-281: The vertical scrollbar block unconditionally returns
true, swallowing wheel events even when vertical->handleScrollWheel(...) returns
false; change it to capture the boolean result (e.g., bool handled =
vertical->handleScrollWheel(0,0,delta)), call updateScroll() only if handled is
true, and return handled so unhandled boundary results from
LLScrollbar::scrollByWheel() can propagate to parent containers; reference
mScrollbar[VERTICAL], LLScrollbar::handleScrollWheel, updateScroll, and
LLScrollbar::scrollByWheel.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 1421782c-7acc-4479-b6a7-346a2609ce0c
📒 Files selected for processing (107)
.gitignoreindra/CMakeLists.txtindra/CMakePresets.jsonindra/cmake/Linking.cmakeindra/cmake/SDL3.cmakeindra/cmake/UI.cmakeindra/llrender/CMakeLists.txtindra/llui/fsvirtualtrackpad.cppindra/llui/fsvirtualtrackpad.hindra/llui/llaccordionctrl.cppindra/llui/llaccordionctrl.hindra/llui/llaccordionctrltab.cppindra/llui/llaccordionctrltab.hindra/llui/llcombobox.cppindra/llui/llcombobox.hindra/llui/llfloater.cppindra/llui/llfloater.hindra/llui/llflyoutbutton.cppindra/llui/llflyoutbutton.hindra/llui/llmenugl.cppindra/llui/llmenugl.hindra/llui/llmodaldialog.cppindra/llui/llmodaldialog.hindra/llui/llscrollbar.cppindra/llui/llscrollbar.hindra/llui/llscrollcontainer.cppindra/llui/llscrollcontainer.hindra/llui/llscrolllistctrl.cppindra/llui/llscrolllistctrl.hindra/llui/llslider.cppindra/llui/llslider.hindra/llui/llspinctrl.cppindra/llui/llspinctrl.hindra/llui/lltextbase.cppindra/llui/lltextbase.hindra/llui/lltooltip.cppindra/llui/lltooltip.hindra/llui/llview.cppindra/llui/llview.hindra/llwindow/CMakeLists.txtindra/llwindow/lldxhardware.cppindra/llwindow/lldxhardware.hindra/llwindow/llkeyboard.cppindra/llwindow/llkeyboard.hindra/llwindow/llkeyboardsdl.cppindra/llwindow/llkeyboardsdl.hindra/llwindow/llkeyboardwin32.cppindra/llwindow/llkeyboardwin32.hindra/llwindow/llmousehandler.hindra/llwindow/llscrolldelta.hindra/llwindow/llsdl.cppindra/llwindow/llwindowcallbacks.cppindra/llwindow/llwindowcallbacks.hindra/llwindow/llwindowsdl.cppindra/llwindow/llwindowsdl.hindra/llwindow/llwindowwin32.cppindra/newview/CMakeLists.txtindra/newview/app_settings/key_bindings.xmlindra/newview/app_settings/settings_alchemy.xmlindra/newview/llagentcamera.cppindra/newview/llagentcamera.hindra/newview/llappviewersdl.cppindra/newview/llappviewerwin32.cppindra/newview/llappviewerwin32.hindra/newview/llchiclet.cppindra/newview/llchiclet.hindra/newview/llfasttimerview.cppindra/newview/llfasttimerview.hindra/newview/llfloaterbvhpreview.cppindra/newview/llfloaterbvhpreview.hindra/newview/llfloaterimagepreview.cppindra/newview/llfloaterimagepreview.hindra/newview/llfloatermodelpreview.cppindra/newview/llfloatermodelpreview.hindra/newview/llfloaterworldmap.cppindra/newview/llfloaterworldmap.hindra/newview/llfollowcam.cppindra/newview/llfollowcam.hindra/newview/llhexeditor.cppindra/newview/llhexeditor.hindra/newview/llmachineid.cppindra/newview/llmediactrl.cppindra/newview/llmediactrl.hindra/newview/llnetmap.cppindra/newview/llnetmap.hindra/newview/llpanelemojicomplete.cppindra/newview/llpanelemojicomplete.hindra/newview/llpanelprimmediacontrols.cppindra/newview/llpanelprimmediacontrols.hindra/newview/llpanelpulldown.cppindra/newview/llpanelpulldown.hindra/newview/llpopupview.cppindra/newview/llpopupview.hindra/newview/lltool.cppindra/newview/lltool.hindra/newview/lltoolcomp.cppindra/newview/lltoolcomp.hindra/newview/lltoolpie.cppindra/newview/lltoolpie.hindra/newview/llviewercontrol.cppindra/newview/llviewermedia.hindra/newview/llviewerwindow.cppindra/newview/llviewerwindow.hindra/newview/llwindowlistener.cppindra/newview/skins/default/xui/en/panel_preferences_move_general.xmlindra/newview/viewer_manifest.pyindra/vcpkg.json
- llscrollbar: clear the residue when a sub-notch wheel event is forwarded to a parent scroller at a boundary, so reversing direction responds immediately instead of first unwinding leftover fraction. - lldxhardware: QueryInterface to IDXGIAdapter3 (not a reinterpret_cast of the IDXGIAdapter from EnumAdapters), and zero-init + HRESULT-check QueryVideoMemoryInfo before reading info.Budget. - llappviewersdl: SendMessageTimeout (SMTO_ABORTIFHUNG, 2s) for the second- instance SLURL handoff so a hung primary can't block startup; require delivery before treating the URL as handed off. - llviewercontrol / llviewerwindow: clamp NumpadControl to the valid enum range before the e_numpad_distinct cast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createSharedContext's GLX path freed the glXChooseFBConfig result with a direct XFree, but the SDL backend doesn't link libX11 (GLX entry points are resolved through SDL_GL_GetProcAddress), so the ubuntu Viewer build failed to link with "undefined reference to XFree". Resolve XFree from the already-resident libX11 at runtime via SDL_LoadObject / SDL_LoadFunction, matching how the GLX functions are resolved. On the X11 server path libX11 is always loaded; if it can't be found the array is left unfreed (a tiny, bounded, once-per-worker leak) rather than crashing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Brings the SDL3 window/input backend (already the default on Linux) to Windows as an opt-in alternative to the native Win32 backend, behind
USE_SDL_WINDOW(vs2026-os-sdlpresets). The native Win32 backend remains the default and is unchanged for normal builds.Along the way this lands the high-precision (sub-notch) scroll-wheel work and restores distinct-numpad avatar control, both shared across all backends.
What's included
SDL3 window backend (Windows)
LLAppViewerWin32is driven through theSDL_App*callbacks, keeping the Win32 platform overrides (NVAPI, Velopack, crash handling, single-instance mutex, WM_COPYDATA SLURL hand-off, DirectInput8/joystick, serial number).LLWindowSDLsplash screen (SDL_ttf + SDL_image, branded icon)..curresources; DXGI VRAM-budget query shared with the native backend; COM init tolerant of SDL's early COM bring-up.Input / scrolling (all backends)
LLScrollDelta: integermClicksfor discrete consumers,mPrecisefor continuous) — smooth sub-notch scrolling/zoom for scrollbars, camera, minimap/world-map/previews and mouselook FOV;MouseWheelScrollSensitivitysetting; momentum scrolling on macOS.NumpadControlsetting (off / NumLock-off / always), matched across the SDL and Win32 keyboards.Hardening / fixes
gIconResourceundeclared in the default (non-SDL) Windows build.GetClientRect).Platform / opt-in
-DUSE_SDL_WINDOW=ON(or thevs2026-os-sdlpresets). Default builds use the native Win32 backend and are unaffected.Testing
gIconResourcepath).🤖 Generated with Claude Code