Repository navigation
release: v3.10.0 â desktop features and emulation fixes - #1042
Conversation
* feat(app): C_TVMENU action, hold bindings and pad capabilities in remap (#991) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * feat(app): hold arbiter and release guard for dual-use buttons (#991) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * feat(app): route hold bindings, the TV-menu toggle and the release guard through input (#991) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(app): UP edges always reach the hold arbiter; hand-offs mask held inputs and reset the menu latch (#991) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * feat(app): TV focus model, nav intents and style metrics (#991) Pure units behind the TV shell: LinearFocus/GridFocus/FocusMemory and the A-Z jump helpers, NavInput (newest-direction repeat, stick hysteresis, trigger paging) with the button/key intent tables, and the u = h/1080 style metrics with the 120 ms ease-out tween. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * feat(app): TV in-game menu logic, App adapter and headless script test (#991) tv::Host + AppHost, the tv::Shell menu model (Resume, Save/Load state slot pickers, Controls, Reset and Close game with a No-first confirm), input ownership while the menu is open (Esc = Back = Resume, F4 and the menu buttons still reach the app), App::menuPause/menuResume (silent, keeps a user pause, netplay does not pause, hardcore refuses), and the --tv-test-script runner with a virtual gamepad plus the tv_menu_headless CTest. Drawing comes in the next part. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(app): TV menu owns input while open, releases reach the app, Start stays with the app (#991) The menu masks all local input while open (netplay included), refuses pause toggles while it holds the pause, lets key/button releases through, leaves Start with the app, and the headless script drives only its own virtual pad. Confirm pages are [Yes, No] with focus on No. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * feat(app): TV in-game menu drawing, smoke test and controls hooks (#991) Draws the in-game menu over the dimmed game (rail-and-list, focus ring, hint bar), adds the tv_ui_smoke test, the C_TVMENU row with "Add hold" and the Guide-chord hint in the controls screen, the cheat-sheet row, and a windowed --tv-test-script path for screenshots. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(app): TV menu final-review fixes: pad caps in the controls screen, deterministic headless tap (#991) On pads without Share, a Share-hold TV-menu binding resolves to Guide-hold, and the controls screen and bind capture use the pad's caps. The headless Share tap is atomic. Also: unfocused rows at body size, wheel scrolls slot pages, withheld hold keys stay out of the core, Back stays in the hint bar, Controls only shows the sheet, no menu toggle during capture, and the menu releases a captured mouse. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(app): design spec for running emulation on its own thread (#967) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app): Phase 1 implementation plan for the emulation thread (#967) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app): emulation thread phase 1 plan: late latch, nightlies off (#967) Two decisions from 2026-10-16 folded into the plan and the spec. Input latency: a late latch in the threaded UI loop (new Task 15), plus an event-to-present VJ_PERF_CSV column to measure it (new Task 14). The UI thread publishes input as each event arrives while it waits, then takes the newest frame just before the vblank. The fake-clock model in late_latch_unit (real LateLatch) gives event -> present 25.3 ms vs 25.0 inline for 5 ms core frames, but 34.2 ms for 14 ms frames, which only mode B can close; Task 18 measures inline / latched / unlatched A/B/C/C/B/A. Rollout: the setting defaults off in every build in phase 1 (it switches off RetroAchievements, netplay and the debugger); the VJ_BUILD_CHANNEL plumbing moves to the phase 3 plan. Old Tasks 14-16 become 16-18. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶ997) (#998) A 404 from the site mirror wrote a .miss marker that held for 7 days, so a disc looked up before the mirror served its art (or during a deploy gap) stayed blank for a week after the site had the picture. Mirror misses now live in memory until the app quits, and marker files an older version wrote for the mirror are removed and ignored. libretro-thumbnails keeps its 30-day markers. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
FindCURL sets CURL_INCLUDE_DIR to <SDK>/usr/include, and CURL::libcurl passes it as -isystem. CMake drops that only when the path matches the compiler's implicit include list. With two Xcodes installed and ccache's c++ wrapper on PATH (compiler_check=mtime on the /usr/bin/c++ xcrun shim), CMake's compiler probe is served from cache and the list names the other Xcode's SDK. The real SDK path is then emitted ahead of libc++'s headers and every C++ file linking curl (ach_http_test, vj_library) fails in <cstring>. On Apple, strip <SDK>/usr/include from CURL::libcurl after both find_package(CURL) calls; it is on the default search path anyway. Non-SDK curl (Homebrew), Linux and Windows are unchanged. Closes #1004 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶe 2) (#999) * docs(app): flesh out TV mode phase 2 plan against the merged phase 1 code (#991) Screen stack beside the in-game menu, TV-owned GameRef/Art/StateRef host types, geometry from mockups A and B, test bodies for Home/Library/Details, Auto acting as Manual until phase 4, and the spec changes phase 2 needs approved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): TV mode state machine and input ownership (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): TV mode fullscreen hold is state, leaving fullscreen turns TV off (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): TV shell, Manual TV mode, --tv and the View menu entry (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): TV shell leaves bind capture and text fields their input; clickable section tabs (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): TV Home screen with shelves and hero panel (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): TV Library screen with rail, cover grid and letter jump (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): TV pad trigger glyphs, held Backspace stops at an empty filter (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): TV Game details and the TV-mode toggle in the in-game menu (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): TV script verbs for windowed screenshots (wait library, shot, quit) (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): TV hint bar band and device-true hints; Home hides a shelf that cannot fit (#991) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): TV mode final-review fixes: fresh launches, focus follows the game, input ownership (#991) - A launch from a TV screen starts the game fresh: the resume prompt needs a mouse (X and Game details already offer the auto-save). - ui.tv_mode is read, never written, until the setting has its UI (phase 4), so the Auto default can still reach users. - Focus on Home shelves and Library lists follows the game's key, so coming back from a game lands on it after Recent reorders. - A focused desktop text field keeps the keyboard (no forced WantCaptureKeyboard); OS key-repeat of nav keys is dropped (NavInput repeats directions itself); an open desktop window takes the pad away from the TV screens under it. - Empty Library grid shows only the hints that work; Home says "Scanning your library..." during the first scan; Ctrl/Cmd+L and File > Game library open the desktop library window while TV mode is on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶse (#1013) (#1016) build_site.py now writes "thumbnails/Atari - Jaguar CD.zip" next to the mirror. It holds only Named_*/*.png under an "Atari - Jaguar CD/" folder, so it unzips straight into RetroArch's thumbnails directory and can be offered to libretro-thumbnails#141 as-is. Entries are sorted with a fixed timestamp, so the same images always give the same bytes. The core info now lists "Atari - Jaguar CD" alongside "Atari - Jaguar", so once libretro-super builds that rdb, RetroArch offers this core for discs a scan matches. The mirror README records what still has to land upstream: the thumbnail repo, the rdb build line, and a RetroArch scanner fix (every Jaguar CD track is AUDIO, and the scanner only hashes data tracks). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶset (#1014) (#1015) RA's game list includes every game it has a hash for, so the Jaguar games tab showed 85 of 126 entries with 0 achievements and 0 points, and the header counted them as games with achievements. viewGames now leaves out games without a published set (unless a progress row reports one), and the header counts with countWithSets. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(app): FrameExchange triple buffer for the emulation thread (#967) Frame moves to frame.h so CoreHost, FrameExchange and VideoOut share it. Content epochs keep a previous game's frame from ever being shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): EmuCommands, the emulation thread's ordered command queue (#967) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): PacingClock mode A, the audio clock as master with an even frame grid (#967) A wall-clock grid at the core's period, trimmed at most 0.5% by the smoothed SDL queue level: true Jaguar rate on the sound card's crystal without the 10.7 ms device-pull jitter a raw queue trigger would add. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): PacingClock refill rebases the grid so it converges (#967) During refill frames start at up to 2x the core rate but the grid still advanced a full period per frame, leaving it ahead of the clock by the extra audio just queued. The loop then slept that long, drained the queue below the low-water mark and re-armed refill: bursts and ~66 ms stalls forever, with underruns. Rebase the grid on each early start while refilling, and make testRefillFromEmpty check evenness, rate and underruns after the refill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): EmuThread, the single owner thread of the core (#967) post() at frame boundaries in order, call() that runs inline on the thread itself and after a stop, callFor() and a join timeout for a core stuck in retro_run, advance() for headless and tests, inline mode for everything else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): InputSnapshot, the newest input for the emulation thread's poll (#967) States are newest-wins; pointer motion is summed until a frame takes it and is delivered exactly once. InputManager::poll can run on the UI thread with the keypad overlay ticking per emulated frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): EmuThread: pending call returns on abandon, exact advance, rate-based pacing test (#967) call() polls for abandonment instead of blocking forever on a queued task; advance(n) counts only budgeted frames so a frame already in flight cannot satisfy it; the pacing test checks rates over measured windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): AudioOut safe across the emulation and UI threads; mode-A update (#967) update(false) counts underruns without nudging the playback ratio: in mode A the pacer follows the audio clock and the two controllers must not fight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): CoreHost owner thread, EmuSnapshot and queued writes (#967) Inline nothing changes. With an owner, the UI reads options, AV info and discs from a pinned EmuSnapshot, option values through a UI-side mirror (the config is still written on the UI thread), and its writes reach the core at the next frame boundary. Any other core call off the owner is refused, counted and logged once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(app): the real core inline vs on the emulation thread, frame for frame (#967) Also pins a load while frames run (no stale frame, empty buffer inside the load call, rising serials) and the owner rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): CoreHost publishes the snapshot before a load call returns (#967) A load or unload made through EmuThread::call() returned before the batch ended, so the UI could pin the previous option table, AV info or disc list. finishLoad, stopSession, a failed startSession and the disc eject / index commands now publish themselves. With no post set while threaded, queued writers are counted and dropped instead of recursing into themselves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): Settings > Interface > Emulation thread and --emu-thread (#967) Off by default in every build for now (nightlies default it on in phase 3, once nothing is switched off while threaded). Decided once per run; headless runs stay inline unless --emu-thread on. Nothing starts a thread yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): VJ_PERF_CSV input-latency and underrun columns, --perf-input (#967) Latency is the oldest input event's SDL timestamp to the core's poll; the first three columns are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): content and state actions through EmuThread::call; headless --emu-thread on (#967) Load, boot BIOS, unload, reset, save and load state, SRAM flushes and the shutdown unload run where the core lives; inline they run in place as before. The save-state thumbnail is the frame the state was taken on. UI checks ask "halted?" instead of CoreHost::inFrame(). The headless smoke tests pass with --emu-thread on with the inline hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): switch off what is not thread-safe yet while the emulation thread is on (#967) RetroAchievements, netplay, the Tools windows and the GDB stub option are off for a threaded run (phases 2 and 3 bring them over). Cheat changes from the Cheats window reach the core as one command, never half a list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(app): windowed emulation-thread check for the Mac Studio (#967) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): the interactive loop with the emulation on its own thread (#967) The UI thread polls and publishes input, takes the newest frame, drains messages and counters and presents; it never waits for the emulation. The 5 s SRAM flush is posted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): VJ_PERF_CSV event-to-present column (#967) The swap that first shows a frame which polled an input event, timed from the event: inline from the iteration's oldest polled event, threaded through the InputSnapshot publish number each frame carries. --perf-input now injects its key from an SDL timer at a swept delay, so events land evenly over the display period in both loops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): late latch for the threaded loop (#967) With the emulation thread and vsync on, the UI thread waits until just before the next vblank (the slowest recent UI build plus a pad that grows on a missed vblank), publishing input as each event arrives, then takes the newest frame. It tracks the vblank grid from swaps that block and stays off while it cannot. The fake-clock model: event -> present 25.3 ms for 5 ms core frames (inline 25.0, unlatched 46.4); 34.2 ms for 14 ms frames, which only mode B can fix. --late-latch off for A/B measurement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): late latch backs off on persistent misses, hysteresis (#967) The latch now releases (with a 300-swap cool-down) when more than a quarter of its last 60 waits missed the vblank they aimed at, so a build whose GPU work lands late cannot keep every frame a vblank behind. Once locked it lets go on jitter only at 2 ms (locks below 1 ms), and a wait that itself overran its wake time no longer counts as a miss. The locked/released log line and the perf note show misses, releases and the pad. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(app): ThreadSanitizer job over the emulation-thread tests (#967) VJ_APP_TSAN instruments the app, the core, SDL and the tests; the job runs ctest -L emu_thread and fails on any race report. tsan_canary proves the sanitizer is really on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app): the Emulation thread setting (#967) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): route UI disc commands through call() so staged_ is never written off the UI wait (#967) A disc insert can re-stage the CD BIOS in the core (stage_cd_bios via open_disc_and_resolve_boot), which writes CoreHost::staged_ from the VJ_EVT_BIOS_STAGED/REJECTED event. Posted from the UI, that write ran on the emulation thread while the BIOS panel read stagedBios() every frame. The File menu tray/disc items and desktop.cpp toggleDiscTray/selectDisc now run inside EmuThread::call (in place when inline), like reset. core_host.h's threading comment and the plan's boundary table say so. Latent today: the app never registers disc images with the core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app): thread-test fail patterns, README frame-skip wording, two comments (#967) - README: the drift alone skips a frame about every 18 s, but frames finishing near the take point also repeat/skip (~1-2.5/s measured at 60 Hz); the display-locked mode planned later addresses it, hence off by default. - emu_thread_flag_rejects, emu_thread_limits, netplay_refused_threaded and gdb_locked_threaded only had a pass regex (ctest then ignores the exit code): they now also fail on "ThreadSanitizer|owner violation". - late_latch.h: kLockAfter counts tracked observations; single outliers do not reset it. emu_thread.h: a call queued before an abandon may still run later on the detached thread, so captures must be by value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(app): emu-thread tests on CI runners: Windows timer, MSVC capture, libdbus TSan (#967) - emu_thread_unit raises the Windows timer resolution to 1 ms (timeBeginPeriod, winmm), as SDL_Init does in the app; at the default ~15.6 ms, fast-forward could not beat normal speed (63 vs 97 frames/s on the Windows runners). - Its "keeps running" check is a deadline poll instead of a fixed-window quarter-rate floor, which a starved macOS runner missed (6 of 36 frames). - emu_commands_test: kEach is static constexpr, so MSVC does not need it captured (C3493). - tsan.supp: suppress libdbus's own lock-order-inversion between SDL_Init and SDL_Quit on the main thread (system library, not instrumented); it was the only report in the Linux TSan job, on every threaded app run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): precise emulation-thread waits on MinGW (#967) MinGW's timed condition wait (winpthreads) rounds short timeouts up to the scheduler tick (~10-16 ms) even with the 1 ms timer resolution SDL sets, so threaded fast-forward ran below normal speed on the MinGW Windows runner (67.5 vs 98.2 frames/s; MSVC was fine) -- and the shipped Windows packages are MinGW builds. EmuCommands::waitUntil now waits on the condition only while more than 20 ms remain, then polls in 1 ms sleeps until the deadline or a post()/notify(). Other platforms keep the plain condition wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(app): emu_thread_unit reports the host's timer precision for the fast-forward check (#967) The MinGW runner still showed fast-forward at one frame per ~15.6 ms (64 vs 98 frames/s) after the polling wait, which points at the platform's timers rather than EmuThread. The test now measures sleep_for(1 ms) and the steady_clock step and prints them with the rates. The fast-forward ratio is still asserted whenever it holds or the host can pace 2.5 ms frames; it is reported instead only when it misses on a host whose sleeps exceed 6 ms or whose clock steps exceed 1 ms (or that cannot even sustain normal speed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶop (#967) (#1018) * test(app): VJ_PERF_TRACE frame/take timing trace + evenness model (#967) VJ_PERF_TRACE=<prefix> with --perf-frames writes <prefix>-emu.csv (each emulated frame: due on the pacing grid, start, end, trim, audio queue, serial) and <prefix>-ui.csv (each UI iteration: take time, serial shown, swap begin/return, the late latch's wake/target/budget), all on SDL_GetTicksNS, so frame ends and take points can be placed against the vblank. VJ_PERF_CSV counts frames run between takes, not frames shown. pacing_clock_test --model prints the fake-clock model: the +/-0.5% trim moves the grid's phase ~1 ms per minute even with 5 ms device-pull jitter, while repeats+skips grow ~32/min per ms of frame-end spread. Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 462c146) * fix(app): late latch: cap late-start exemptions, one long build no longer sets the budget (#967) Two items from the Phase 1 review: - a wait that overran its wake time is excused from the miss rate only kMaxLateExempt (8) times in a row; a wait that always overruns is the latch's own cost and must be able to release it; - the budget is the second-slowest of the last 60 builds: one long build (a window opening, a shader compile) misses its vblank whatever the budget says, and as the max it also gave up the wait, or moved the take point several ms earlier, for the next second. Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit bdc8786) * fix(app): mode A steers the emulation grid's phase off the UI's take point (#967) The threaded loop repeated and skipped frames 60-160 times a minute where the 60.054-vs-60 Hz beat owes 3-4 skips. A frame ends at its due time plus its wake-up lateness and cost, so while the slowly drifting grid carries a take point through that spread, whether each frame makes the take is decided by its own cost: spread / 15 us frames in a row come out as random repeat + skip pairs. The single-thread loop never sees this: it runs the frame for each vblank inside that iteration. PacingClock now remembers the last 120 frame ends against their due times, and when the next frame's window (p2..p98, plus 0.75 ms) would contain a take point, steps the grid once across it in the direction the beat already moves it. Each crossing is then the one skip (or repeat) it has to be. Only the phase moves, by at most the window once per beat; the rate stays the trimmed audio rate (pacing_clock_test: 3 ppm over 150 s against a 200 ppm sound card, no underrun). The UI publishes its take time and the display period while the latch has the vblank grid locked (with or without the wait); --pace-steer off disables it for A/B. Model (pacing_clock_test --model): 4 ms cost spread, 60 Hz, take 3 ms before the vblank: 133 repeats + 136 skips per minute unsteered, 0 + 3 steered (4 steps); 120 Hz: 577 frames held 1 or 3 refreshes -> 7. Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit a015047) * docs(app): mode A phase steering in the emulation-thread design (#967) Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 133ee01) * fix(app): steering holds the end window across cost switches (#967) Measured on the Studio (yarc, VJ_PERF_TRACE): the emulation thread wakes 0.5-3 ms late and the frame cost switches with the content between ~7.5 and ~2.9 ms for seconds at a time. Two faults showed in the first steered run (12 repeats + 35 skips, 29 steps): - the 'not before this frame can end' guard used the window's p98, so after a switch to cheap frames every earlier step was refused while the ends dithered on the take point; the guard is now only 'not before the frame starting now' (a step that lands early just starts late); - the 120-frame window forgot the slow mode within 2 s, so the switch back landed the ends on the take point; the window's edges are now held and shrink back at 0.2 ms/s. pacing_clock_test models both (wake lateness + a switching cost, six take offsets): 18-38 repeats+skips per minute before, 3-5 after. Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 90643d1) * fix(app): steering follows the trimmed drift, at most one step per 30 frames (#967) Studio, Cybermorph, steering v2: 20 repeats + 38 skips and 98 steps. Every step went earlier (the untrimmed 60.054-on-60 Hz direction) and queued its few ms of audio; 55 ms in a minute held the trim at +0.17%, past the beat's 0.09%, so the grid was drifting the other way and each step put the window back in front of the take point. The direction now comes from the trimmed period, which makes the steps self-correcting (a late step drains what an early one queued). A floor of 30 frames between steps keeps any other mistake from becoming a display lock. pacing_clock_test: a grid slower than the panel (a 0.15% slow sound card) went from 5224 steps and 0 repeats (a lock) to 4 repeats and 5 steps in 120 s; a 9 ms window: 6-7 events in 120 s. Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 395e255) * docs(app): VJ_PERF_TRACE summary script, --pace-steer in the README (#967) Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 1cbe57b) * fix(app): steer only where the grid creeps along the take grid (#967) At ratios off a whole number (50.08 Hz PAL on 60 Hz, 60.05 on 75 or 144 Hz) successive frames land ~3 ms apart on the take grid, so some frame's end window always holds a take point: that is 5:6-style judder, not a slow crossing. pacing_clock_test showed 99-119 steps a minute there and the rate pulled off the audio clock. Steering now needs the untrimmed creep to be under 0.2 ms a frame (60.05 Hz on 60/120/59.94 Hz: 15-32 us): 0 steps, no underrun, rate kept at all three ratios. Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 9adae8e) * test(app): perf_trace_summary.py computes event -> present exactly from the trace (#967) ~11,000 virtual events per run instead of 120 injected keys, whose run averages moved by up to 3.4 ms between identical runs. Refs #967 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 469cf23) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶorks (#1003) (#1006) * fix(app): register the loaded CD with the core's disk control so the disc menu works (#1003) The core starts its disk-control image list empty even after it booted a CD (libretro.c: "nothing to insert (image list is empty)") and has no .m3u loader, so the app's Open/Close Tray item, File > Disc and Cmd::DiscTray / Cmd::DiscSelect were unreachable for every title. - CoreHost::loadContent appends the CD it loaded (cue/cdi/chd/iso only: the core registers disk control for a cartridge too, and closing the tray would try to mount the cartridge as a disc), and only while the core's list is empty. - CoreHost::discAppend: RetroArch's "Disk Image Append", owner-only. - File > Add disc image... (Cmd::DiscAdd, ImGui and native menus): a file picker; the image joins the list and is selected, so a closed tray swaps at once. Inside EmuThread::call like the other disc commands (an insert can re-stage the CD BIOS, #967). - Headless --disc-swap IMAGE drives addDisc, Open tray, File > Disc and Close tray, reading the disc list the menus read (the pinned snapshot when threaded). smoke_disc_swap (inline) and smoke_disc_swap_threaded (--emu-thread on, label emu_thread) swap synth_jagcd.chd -> synth_jagcd_multi.chd -> synth_jagcd.chd; with the registration removed both fail with "the loaded disc is not in the disc list (0 image(s))". No core change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 0033152) * test(app): disc swap threaded in real-BIOS boot mode, where each insert re-stages the CD BIOS (#1003) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 98599c6) * fix(app): disc labels keep only the file name on Windows too (#1003) The core's disk_get_image_label splits the path on '/' only, so on Windows File > Disc listed whole 'D:\...\Disc.chd' paths, and smoke_disc_swap failed on the MinGW runner. The label now loses everything up to the last '/' or '\\' app-side; the core is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 08b340f) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶan be inserted (#1007) (#1009) File > Boot BIOS > CD BIOS (no disc) left the disc section hidden. discInfo().available meant count > 0, and that boot registers disk control with an empty list. So the boot that exists to take a disc offered no way to insert one. CoreHost::loadNoContent now latches emptyCdTray_ when the core booted the CD BIOS, meaning it was not told VJ_BOOT_CART_BOOT_ROM. available is then count > 0 || emptyCdTray_. "Registered" alone would not do: the core registers disk control for cartridges too. With an empty list, only Add disc image... shows (ImGui and native menus). The tray toggle is hidden because Close tray would have nothing to insert. No app-side eject is needed. The core's set_image_index with the tray closed already runs eject + insert itself, so addDisc's append + select boots the disc. Headless --disc-insert IMAGE taps the core log during the add and quotes its "disk control: inserted" line in the PASS line. - smoke_disc_insert_bios runs inline, and _threaded runs with --emu-thread on. - smoke_disc_insert_cart_hidden is the negative control: the cart boot ROM keeps the menu hidden. - With the gate reverted, both insert tests fail. Core untouched. (cherry picked from commit 8a8e323) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶn the opened disc (#1010) (#1011) * fix(app): follow a disc swap in the content identity; keep the .srm on the opened disc After File > Disc / Add disc image... booted another disc, contentPath()/contentName() and everything keyed on them still named the first disc. CoreHost now keeps two identities: - contentPath()/contentName(): the disc in the tray. It moves only after an insert that worked and left the tray closed (followMountedDisc). The core moves its image index before it tries the insert and keeps it when the insert fails, so the index alone is not enough. Off the owner thread these answer from the snapshot, since discSetEject/discSetIndex may run as queued commands. - loadedPath()/loadedName(): what was opened. The .srm (and the second-copy fallback) is named after it, and Restart reopens it. The core keeps SAVE_RAM (the EEPROM pair and the Memory Track) across an insert and never reloads it, so re-keying the file would write disc A's Memory Track into B's .srm. App::followDiscSwap (main loop, addDisc, the headless check) catches up on a swap: - save-state slots and thumbnails follow the tray (statePath reads contentName): the core refuses a state taken on a different disc (savestate v14 disc identity); - achievements use rc_client_begin_identify_and_change_media, so the session and hardcore survive a disc of the same game; an unidentified first disc gets a fresh identify instead; - cheats re-key (the core re-reads the per-title row on insert, titledb_probe_disc); - a queued state load and the resume offer for the previous disc are dropped. The window title and Discord read contentPath every frame, so they follow without changes. Recents, the library's recently played and the per-game control overrides stay with the opened disc: reopening must start from its .srm. --disc-swap now checks after each step that the content and the state slot name the disc in the tray while loadedPath and saveRamPath stay on the opened disc, and saves a state on the second disc. The three disc-swap tests get their own --data-dir so that state lands in the build tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit b96ce11) * docs(app): the content identity a disc swap moves is read from the snapshot Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit c7e7dfc) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶtrols (#1019) * fix(app): add the Pro Controller (6-button) device with its own X Y Z L R controls Settings > Input now offers "Pro Controller (6-button)" on both Jaguar ports. The core has no RETRO_DEVICE id for it, only virtualjaguar_pN_device = pad_pro, and the app locks that option to auto, so the core's pro_controller[] flag could never turn on from the app. The option lock now takes the Pro ports into account (lockedCoreOptionFor), and CoreHost::refreshLocks() hands a changed value to the core through GET_VARIABLE_UPDATE. retro_run() runs check_variables() before update_input(), so a switch takes effect on the next frame. The app stores a private id for the device in the joypad class and gives the core a standard pad for it. New controls X / Y / Z / L / R (Pro) are appended after C_TVMENU, so saved ids keep their meaning. On a Pro port the Router presses them as RetroPad X / L2 / R2 / L / R, and the core's Pro path turns those into keypad 9 / 8 / 7 / 4 / 6. They are never synthesised as keypad digits. Gamepad defaults on a Pro port, Genesis 6-button style: A / B / C = East / South / West, X / Y / Z = Left Shoulder / North / Right Shoulder, L / R = the triggers. Keypad 0-4 have no pad button there and are reached through the keypad overlay (Share). 5 / 6 stay on the stick clicks, 7 / 8 / 9 / * on the right stick and # on Guide. The standard-pad defaults are unchanged: a new remap_unit digest, frozen from the tree before this change, covers every control that already existed. The keyboard has no Pro bindings, because A S D / Q E already press keypad 9 8 7 / 4 6, the same wires. The cheat sheet, the controls overlay and the remap table show the Pro buttons for a Pro port. Closes #1017 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): keep the Pro Controller lock on the emulation thread's owner After #1000 the option state CoreHost keeps for the core (locked_, the session values) belongs to the emulation thread, and the UI reads a published snapshot of it. Three changes follow: - refreshLocks() queues itself through `post` when it is called off the owner. On the owner it bumps the option serial and publishes, so the UI's copy of the locked values follows. - App::proPorts is atomic, because the option lock reads it on the emulation thread. - applyPortDevices() works out each port's kind from the lock's own value, not the UI snapshot, which lags until the queued refresh has run and been pinned. With --emu-thread on, limitFeaturesForEmuThread() replaces the option lock. Its lock called the 2-argument lockedCoreOption(), so pad_pro never reached the core in that mode. It now uses lockedCoreOptionFor() with the Pro ports, as wireHost()'s lock does. Tests: - core_host_thread: a lock value moved from the UI thread reaches the core before the next frame's input (RetroPad X reads as keypad 9 under pad_pro, keypad 0 without), the UI snapshot shows it after a pin, and no owner violations occur. - pro_controller_option_on / _off: the app itself hands the core pad_pro for a config naming the Pro Controller, with the emulation thread on and off. The _on test fails when limitFeaturesForEmuThread() keeps the old lock. Refs #1017 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶ1008) (#1012) Neither the core nor the app read .m3u. The app now does (app/src/m3u.h, RetroArch's format: one image per line, relative to the playlist or absolute; '#' lines, a BOM, CRLF and a "|label" suffix are ignored). - CoreHost::loadContent: an .m3u boots its first disc and appends every disc to the core's disc list (owner thread, inside the load). Each entry must exist and be a cue/cdi/chd/iso, or the load fails with the entry named: a missing disc would still register and only fail when picked. - Identity, on #1011's model: the playlist is what was opened (loadedPath/loadedName: one .srm per set, Restart, recents, the library's recently played, per-game controls); the disc in the tray is the content (contentPath/contentName: the window title, save-state slots, achievements, cheats) -- the first disc at boot, then whatever a swap boots. finishLoad takes the opened path for this. - File > Open filter takes .m3u; the headless load check, tv_script's load and the library's "load this state" compare against loadedPath. - Library: isDiscExt includes m3u; the playlist claims the discs it lists (they leave the grid) and is identified by its first disc's track-1 CRC. - Headless --expect-discs N: the disc list the menus read holds N images; at boot the content is disc 1 and the .srm / recents are the playlist's; after Open tray, File > Disc > last, Close tray the content and state slot are the last disc's while the .srm stays the playlist's. smoke_m3u_playlist (inline) and smoke_m3u_playlist_threaded (--emu-thread on, emu_thread label) load tests/fixtures/synth_jagcd_set.m3u (../-relative entries to test/roms/synth_jagcd{,_multi}.chd). - library_unit: parser cases and the scan dedupe / hash. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Achievements and netplay back on with the emulation thread (core-event stream, rc_client_do_frame on the emulation thread, netplay as a UI-thread object fed by the stream), the cheat batch as a regular command, the 250 ms busy spinner, and the Phase 1 open items (MinGW waits, limits after a failed start, the vsync-off nap). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶbelled as the TV-mode toggle (#1020) (#1024) A fixed (not rebindable) Ctrl+Shift+T / Cmd+Shift+T toggles TV mode with or without a game running (the game keeps running, the in-game menu stays shut). It is wired into App::handleHotkey, the ImGui View menu and the native View > TV Mode item, and is ignored while a text field has the keyboard or a binding is being captured. The C_TVMENU action is relabelled "TV menu / TV mode" and the cheat sheet lists the shortcut. The headless TV script's `key` verb takes ctrl+shift+ modifiers; tv_menu_headless and platform_test cover the shortcut. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶait (#967) (#1023) The emulation thread woke ~2 ms (p99 ~3 ms) after each frame's due time on the Mac Studio: a default-QoS std::thread waiting on a condition deadline gets timer slack. It now runs at user-interactive QoS on Apple, and its condition wait ends 1 ms before the due time, with short sleeps covering the rest. --emu-wake plain restores the old wait for A/B. VJ_PERF_TRACE's UI rows gain the late latch wait's input events and its worst overshoot of one blocking call (SDL_WaitEventTimeout or SDL_DelayPrecise): waits overran 1-18 ms with no event to handle, and each one missed its vblank. Refs #967 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(app): Neuro API client, so an AI can play as Player 1 Settings > Neuro (or --neuro ws://HOST:PORT) connects the app to a Neuro game API server (https://github.com/VedalAI/neuro-sdk): Neuro-sama, or any backend that speaks the API (Randy, Tony, Gary), plays through registered actions. - Controller actions: press_buttons, button_sequence, hold_buttons, release_all; every schema-valid input fits the 30 s queue. - Game profiles (built-in Tempest 2000, or <data>/neuro/*.json): named button macros, an "about" text sent as context, and RAM watches in the state text. - Forces the next move whenever the last input has played out (never while paused); results are sent on validation, as the API asks. - WebSocket client on a worker thread (no TLS, no new dependencies), with reconnect; MSG_NOSIGNAL / SO_NOSIGPIPE so a dropped server cannot kill the app. - Headless --neuro runs in real time. - Tests: neuro_unit and neuro_loopback (a WebSocket server on a free port). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: Neuro API guide (actions, profile format, testing with Randy) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(app): Neuro API client in app/src/neuro/, behind VJ_APP_NEURO The neuro_* sources move to app/src/neuro/. The CMake option VJ_APP_NEURO (ON by default) builds them, defines VJ_HAVE_NEURO and adds the neuro_* tests; with it OFF the app has no Settings > Neuro tab and no --neuro. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): NOMINMAX before winsock2.h in neuro_ws.cpp (MSVC std::min) windows.h's min/max macros broke std::min on MSVC; discord_ipc.cpp guards the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶcore block, indicators, privacy (#1025) (#1027) - Agent: a dropped or failed link clears the queue and any hold_buttons (a held button no longer outlives the server); stop() clears stale events. - WsClient: the name lookup runs on an abandoned-on-stop helper thread, so stop() never waits on getaddrinfo; reject masked server frames and reserved bits; 1 MiB message cap; byte caps on the event and send queues; jittered backoff; a "://" in the URL path is not a scheme. - RetroAchievements hardcore blocks Neuro (disconnect + release, Settings notice, toast); connects again when hardcore ends. - Toasts on connect/disconnect and an "AI plays P1" chip on the background draw list; the server is told (and actions fail) while the in-game menu has the pads or P1 is a mouse. - Settings > Neuro: URL field follows the config and applies on click-away; warns on a non-loopback ws:// server; settings are re-read 4x a second, not per frame. - JSON: a lone high surrogate no longer swallows the pair behind it. - privacy page row, docs/neuro.md (hardcore, indicators, emulation-thread note). - tests: wrong Accept, oversize/masked/reserved frames, flood cap, hold released on disconnect, hardcore no-connect/release/reconnect, input gating, surrogates, URLs. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
âĶily and custom button profiles (#1021) (#1030) * docs(app): controller navigation of the desktop library, design (#1021) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app): controller navigation of the desktop library, plan (#1021) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): NavInput per-pad sources, supplied intents and long-press Accept (#1021) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): controller-family menu-button profiles with a per-model custom layer (#1021) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): a menu-button family change clears the model's overrides; ASCII minus label (#1021) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): DesktopNav focus model for the desktop library (#1021) Pure focus over the library's regions (Favorites strip, Recents strip, grid, or the States list), reusing tv::LinearFocus/GridFocus/FocusMemory/jumpInitial. Maps pad intents to Activate/OpenMenu/ToggleFavorite/TabPrev/TabNext/ FocusSearch/Back commands, follows the focused item by key when lists change, remembers focus per tab and region, and raises a one-shot scroll flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): TV mode reads each pad's menu-button profile (#1021) The shell asks the host what a pad's button or trigger means (the pad's controller-family profile plus the model's custom layer) instead of the fixed Xbox table; hints name the profile's binding. Search and AcceptLong are the desktop library's, so a button mapped to one is neither fed to the TV screens nor consumed. A removed pad drops only its own holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): route pads to the desktop library when it owns the input (#1021) DesktopPad (tv::NavInput with long-press Accept + DesktopNav + pending commands) and deskPadActive/HandleEvent/Frame per design section 2: TV off, no running game (or a paused one with the library window open), home library or window showing, no capture or other desktop window. Pad buttons and triggers go through the pad's NavProfile, the left stick through padAxis, the right stick Y is kept for scrolling; a mapped press is consumed. Start, unmapped buttons and the keyboard fall through. Every edge of active / library window / tab / pad popup resets the nav and the queue. Script: expect desk-pad and desk-pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): zero the desktop nav's right-stick scroll on ownership edges and focus loss (#1021) A right stick let go while the library did not own the pad left scrollY set, so the grid would scroll on once it did. Zero it on every edge of active / library window / tab / pad popup and on window focus loss. Script: new 'axis NAME VALUE' verb and 'expect desk-scroll', plus a Settings open/close line for desk-pad (Ctrl+,). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): pad focus, ring and actions in the desktop library (#1021) The library publishes its Favorites / Recently played strips, grid and State shots to the desktop nav each frame, draws a focus ring (theme text colour, soft shadow) on the focused tile while the pad drives, scrolls it into view only when it is not fully visible, scrolls with the right stick, and executes the queued commands: launch (arming the release guard), the tile menu and BIOS popup anchored at the tile, favorite, tab switch, search focus and Back. A pad press in the search box leaves it. The status line shows the last-used pad's button hints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): pad scroll only for the pad, stick scroll hands focus on, shared button labels (#1021) Bring-into-view scrolling acts only while the pad drives (the mouse's view no longer snaps on a scan, search or tab click). When the right stick scrolls the focused grid tile out of view, focus moves to the nearest fully visible tile of its column (pure deskNearestVisibleIndex, DesktopNav::focusIndex without a scroll flag). The face-label logic moves into padButtonLabel (pad_profile) and is used by TV hints and the desktop hint line. The hint rides the status line (no line comes and goes) and says only what works on the States tab. A stale queued tile command is dropped, the redundant second command pass goes, and the design doc's search-box rule says any pad press leaves it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): drive library popups with the pad through ImGui's gamepad nav (#1021) Pad-opened popups (tile menu, BIOS popup, delete modal) get ImGui's own gamepad navigation fed with our intents (directions, Accept -> GamepadFaceDown, Back -> GamepadFaceRight), so the Switch confirm/back swap holds. The SDL3 backend is in Manual gamepad mode; HasGamepad is set between its NewFrame and ImGui's, only while the popup is open. Accept acts on press inside popups. tv_ui_smoke guards that gamepad nav does not make ImGui want the keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): edit a controller model's menu buttons in the controls screen (#1021) Settings > Controls gets a Menu buttons section for one connected controller model: a layout combo (Auto shows the detected family), a Set / Default row per menu intent and Reset menu buttons. With every gamepad selected it shows the last-used pad's family table read-only. InputManager::beginNavCapture reuses the bind capture, writes no game binding, refuses the pad's TV-menu buttons, D-pad and sticks, and reports Swapped / Reserved. The headless tv script gains capture nav:INTENT and a {padguid} placeholder in expect cfg. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): menu-button Default swaps the partner back; swap note ignores hold flag (#1021) Default on a menu intent now goes through setNavBinding with the family's button, so a swapped partner gets its old button back and no two intents share one. The Swapped note compares kind/code/dir like setNavBinding. The combo is labelled Family. Script: navdefault INTENT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): pad nav review fixes: visible popup cursor, Play via the pad path, no pad under foreign popups (#1021) - A popup the pad opened shows its focused first row at once (ConfigNavCursorVisibleAlways while its nav is on), since Accept acts on press. - The tile menu's Play row goes through activateEntry, so a pad user gets no mouse-only resume prompt and the confirm press is guarded. - The library does not own the pad while an ImGui popup the pad did not open is up (right-click or menu-bar menu, Lists popup, combo, modal). - Remove the unreachable pad hook on the delete-state modal. - Menu-button capture also refuses the pad's controls-overlay (C_HELP) button; resetNavBinding bounds-checks the intent; picking the family a pad already has no longer wipes its menu-button changes (items that do say so). - DeskLists are built and published only while the pad drives the library, and moved into the nav; Pad::nav defaults to the Xbox map; tv_host.h comment says SDL_gamepad.h is for enums only. - Docs: README and TV-mode design updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
âĶ1029) Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 2 to 3. - [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases) - [Commits](SignPath/github-action-submit-signing-request@v2...v3) --- updated-dependencies: - dependency-name: signpath/github-action-submit-signing-request dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
âĶcalls (#967) (#1031) * docs(app): emulation thread phase 2 plan (#967) Achievements and netplay back on with the emulation thread (core-event stream, rc_client_do_frame on the emulation thread, netplay as a UI-thread object fed by the stream), the cheat batch as a regular command, the 250 ms busy spinner, and the Phase 1 open items (MinGW waits, limits after a failed start, the vsync-off nap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): EmuSnapshot in its own file, with the core-event stream (#967) CoreEventQueue carries log lines, vj_events and option writes from the emulation thread to the UI thread in order; log lines are bounded, events and option writes never dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): core log lines, events and owner option writes reach the UI thread through the stream (#967) CoreHost::onUiEvent gets every log line and vj_event on the UI thread (pinSnapshot), with an epoch that rides the command queue; option writes made on the emulation thread inside a call() are applied to the UI's mirror and the config there too, never on the emulation thread. A content switch drops queued lines and events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): netplay works with the emulation thread on (#967) Netplay stays a UI-thread object: the core's link lines and events reach it through CoreHost::onUiEvent, filtered by an epoch bumped at every start/stop/bounce, and the endpoint bounce completes after the core has read "disabled" for 2 emulated frames (3 threaded), counted with frameCount(). Threaded twins of netplay_pair (both sides, voice modem, a mixed threaded/inline pair) and of netplay_no_partner, which must exit 1, never 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(app): the cheat list reaches the core as one regular command (#967) CheatManager::apply() builds one CheatBatch (reset, then every set) and hands it to commit, which the app posts to the emulation thread; the three sinks and the UI-thread batch collector are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(app): RetroAchievements safe on two threads (#967) rc_client events are copied into AchEvents and applied in pump(); core RAM, the memory map and the cheat resets are reached on the emulation thread (call/post); read_memory off it answers address checks from the map without touching RAM; the rich-presence line is computed with the frame and published; the mock server is locked. Inline runs behave as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): high-resolution short waits for the emulation thread on Windows (#967) The MinGW runner paced threaded fast-forward at one frame per ~15.6 ms (63.4 vs 96.6 frames/s); develop's newest completed MinGW runs (x64 and ARM64) pass emu_thread_unit, but ctest hides a passing test's output so the timer note is not visible there, and no MinGW run on develop has yet carried the 1 ms precise-wait check. preciseSleepNs uses a high-resolution waitable timer, and every app job now prints emu_thread_unit's timer report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(app): the precise-sleep check uses the shortest wake (#967) A starved host still has some on-time wakes, while timers that are coarse by platform (every wake ~15.6 ms on MinGW) have none, so the check asserts on the shortest of 20 preciseSleepNs(1 ms) waits; the note prints mean and min. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): RetroAchievements works with the emulation thread on (#967) rc_client_do_frame (and with it rc_client_idle) runs after each frame on the emulation thread; the UI pump posts the idle while paused. A threaded headless RA run steps one frame at a time in the inline order, so every ra_* test has a _threaded twin that lands its steps on the same frames. Only the debugger tools and the GDB stub stay off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): precise_sleep.h includes a lean windows.h (#967) emu_commands.h reaches ~24 MinGW translation units through app.h; WIN32_LEAN_AND_MEAN keeps legacy winsock and macro collisions out of them (the waitable-timer and Sleep calls need nothing outside it). A WaitForSingleObject that does not return WAIT_OBJECT_0 now falls back to Sleep(1), so a failed wait cannot make waitUntil spin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): quiesce the emulation thread before RetroAchievements shuts down (#967) App::shutdown destroyed the rc_client while a free-running emulation thread could still be inside rc_client_do_frame or a posted emuIdle: it now unloads and stops the thread first, then ach.shutdown() (still before video.shutdown(), images.stop() needs the GL context). AchHttp::ensureWorker runs inside request()'s locked block, and shutdown() moves the worker out under the lock and joins outside it, so request() is safe from the UI and the emulation thread (new two-thread case in ach_http_test). onContentLoaded and onReset resync framesSeen_ so frames that ran before the load are not counted as the new game's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(app): a call() that takes over 250 ms shows a busy spinner (#967) EmuThread::call() runs a wait hook on the waiting thread from 250 ms on; the app's hook pumps OS events and swaps a spinner drawn with scissored clears -- no ImGui frame, no call(), no read of a field only a call() writes (CoreHost counts those while the UI waits). Headless, it logs once per call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): a failed emulation-thread start lifts the feature limits; vsync off naps to the next refresh (#967) Phase 1 final-review minors 2 and 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app): emulation thread phase 2 -- achievements, netplay, the busy spinner (#967) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): drop the previous game's achievement trackers and indicators after a content switch (#967) Events carry the generation they were queued in; onContentLoaded and onContentClosed bump it, and drainEvents drops the stale tracker, challenge, progress, attempt started/failed and reset events (no HIDE would ever come for them). A real unlock, completions, scoreboards and the connection state are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): drive rc_client's mutating calls from the emulation thread only (#967) App::reset runs ach.onReset() inside the emulation-thread call (rc_client_reset raises events); applyHardcore, setEnabled(false), logout and the pause check (rc_client_can_pause advances the pause-decay state) go through coreCall, so none of them runs while rc_client_do_frame does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): the vsync-off present loop runs on a display-period deadline grid and handles input while it naps (#967) The nap was anchored at the swap end, so the period was refresh + build + swap (~54-57 Hz on a 60 Hz display) and no input was published during it. LateLatch::advanceGridNs / gridNapNs keep a deadline grid; App::presentNap waits for events as latchWait does and hands them to the emulation thread at once. (SDL 3.4 has no SDL_WaitEventTimeoutNS.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): the emulation-thread setting hint names only what is still switched off (#967) RetroAchievements and netplay work with the thread on; the Tools (debugger) menu and the GDB stub do not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(app): pin that a reload drops the previous game's queued core log lines (#967) The block had no onUiEvent, so it only proved option survival. It now queues a log line inside the same call as the reload, and asserts it is not delivered while the option write still is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): final-review minors -- ci report step, inline pin, display mode, RAII guards, stale docs (#967) ci: the timer-report step is informational (continue-on-error). CoreHost::pinSnapshot applies queued option events when the UI thread is the owner (thread stopped / inline). The display period is re-read on SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED. busyFrame's UI-waiting flag and call()'s wait-hook flag are RAII. netplay.h and cheats.h comments, docs/agent/testing.md (TSan recipe) and the ach_event_unit label are brought up to date. The off-thread read_memory answer notes the rcheevos v12.5 assumption. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): serialize achievement replies and synchronize scope capture (#967) Run HTTP and mock rc_client replies on the core owner; guard scope visibility and its sample ring. Add fail-first ownership/concurrency regressions to the sanitizer suite and prevent Windows GL-loader macros from breaking the busy spinner. * fix(app): synchronize achievement UI client access (#967) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d61ab63)
App packagesBuilt from
Updated by CI at 2026-10-21T00:28:23.082Z |
ð Kimi reviewVerdict: one real defect in the core changes â the new MEMCON1 open-bus readback in Findingssrc/tom/tom.c:~2052 â Notes on the rest (shown portion)
Coverage caveat197 of the changed files were truncated from the diff â everything under 4 generated/vendored file(s) excluded from review. Diff truncated before review â large PR. 197 changed file(s) not reviewed: |
ð Kimi reviewVerdict: no confident defects in the emulation changes shown; the core C is C89-clean and the MMULT/blitter/PIT logic is internally consistent. Two items deserve a confirmation before merge, plus the usual scope caveat for the truncated app-side files. Findings / verification points, most significant first:
Notes on things I checked and am satisfied with:
Scope caveat: 197 of the changed files (the entire 4 generated/vendored file(s) excluded from review. Diff truncated before review â large PR. 197 changed file(s) not reviewed: |
|
Checked the findings in the earlier review and the follow-up review against release head
No emulation source change is justified by these findings. Logs: |
|
Completed a separate review of the app areas omitted from the truncated bot review: emulation-thread ownership/lifecycle and frame handoff, audio/snapshots, playlist/disc/state ownership, achievement callbacks and event lifetimes, Neuro transport/actions/profiles, controller holds and release guards, desktop/TV focus and library launches, pacing/late latch, and shared window/build changes. Found and fixed: Validation: fresh app build completed; the full CTest run passed 159/160, with the sole Neuro failure traced to an old parser object compiled during the initial build before the fix. After rebuilding the parser in both the app and test targets, all three Neuro tests passed. All 160 distinct app checks are validated. The core/harness inputs are unchanged, so the previously refreshed private-ROM matrices remain applicable. CI: the reported package-template failure was The app-review hold is cleared. Merge still waits for all fresh applicable PR and Release checks on b8164d8 plus any new review feedback. No final tag/publication is authorized. |
|
Consolidated duplicate app-package builds in 2e46ca4. For a same-repository The shared PR gate requires the newest exact-head/branch/repository Release push run, all twelve package/runtime jobs successful, and all six nonempty, unexpired package artifact groups. Pending/failing/cancelled/skipped jobs and missing artifacts cannot pass. It rechecks live head/tree and rerun attempts. The waiting gate uses a hosted runner so it cannot occupy the Studio runner needed by Release metadata jobs. Native app tests/TSan, all Release packaging tests, signing, artifact names/14-day retention, and release artifact/checksum collection are preserved. The sticky PR download comment points directly to the canonical artifacts (shared filenames use Validation: 17 safety tests pass, Node syntax and actionlint structural/expression checks pass. A read-only live API check confirmed #1042's matching tree and correct canonical-run selection; it correctly refused readiness while package jobs remained pending. Fresh CI is now required on |
|
Production core CI consolidation is now pushed in 3c68316. For same-repository release PRs whose synthetic merge tree exactly matches the head tree, C/C++ CI now reuses ten compile-only Release targets: macOS Intel, WASM, four Android ABIs, iOS, tvOS, Vita, and Switch. This saves eight runner allocations per eligible PR. Other PRs, forks, manual runs, and differing merge trees retain independent builds. The seven native compiler/test rows and their production â TEST_EXPORTS â production transitions remain unchanged, as do MSVC, sanitizers, coverage, clang-tidy, and SwiftPM. Android SIMD assertions and Mach-O production export checks also run on the canonical Release builds. A workflow contract checks matching compiler/SDK settings before sharing is allowed. The new Verify and retain shared production cores check must pass: it verifies the exact-head Release push run, successful canonical jobs, all ten raw and ten shipped artifact groups, and downloaded binary provenance/hash/production ABI metadata. It then uploads the original ten C/C++ artifact names. Final Release archives, split-debug artifacts, and checksums are preserved; raw intermediates are excluded from final release assets. The existing shared app-package verification remains required too. Validation: 30 Node safety tests pass, the Ruby workflow/toolchain contract passes, workflow structural/expression lint and shell checks pass, and the original compiler matrix/native test steps were compared against the prior version. No emulator source or boot-matrix inputs changed. Fresh PR and Release CI must finish before merge. |
Release v3.10.0
Ship TV mode and controller library navigation, Pro Controller controls, fullscreen menu auto-hide/native breakout-window chrome, multi-disc playlists and per-disc state resume, opt-in emulation threading with achievements/netplay, and optional Neuro API support.
Include the completed AtariAge follow-ups: GPU/DSP MMULT address persistence/wrap (#1048), live PIT/MEMCON1 readback (#1047), GT regression coverage (#1049), and the reproduced Fast blitter DSTA2 computed-Z destination-lane correction (#1051, backport
338e2b65).Core options and savestate format v17 carry over. Emulation threading remains experimental and off by default; Tools/debugger and GDB require inline mode. The three version copies, release notes, WHATSNEW and boot matrices are included. This GitFlow release targets master.
Refs #1036
Validation
Combined core candidate
344dc0bcon Mac Studio.62118c06adds validation docs/matrices;24ae1bdbimports the TOM PR's follow-up screenshot reference. Neither changes emulation sources.make TEST_EXPORTS=1 test: passed. Three skips: missing optional Fountain live-abort fixture, CHSE-capable chdman and one-session Red Book disc. Host-sensitive network checks ran and passed.378c15af: all 93 blitter command checks pass; acid baseline has 152 cases, 149 pass / 3 established known failures, zero regressions and zero missing. C89 lint passes. The new DSTA2 GT ROM and all associated regression tests are included unchanged from fix(blitter): use A2 destination lanes for GT computed ZÂ #1051. Earlier source-session RetroArch testing confirmed red before / green after the fix.b5e40090, matrix input identity67acceb9dc39, committed inf6e2e7b7): 155 cartridge titles in HLE/BIOS and 28 CD title/mode rows. Against both v3.9.1 and pre-GT candidate24ae1bdb: zero backward moves, zero added/removed rows, no new watchdog signatures or asymmetries. Three existing cartridge asymmetries are unchanged. All four matrix comparisons and the sweep pipeline exited 0.Merge is authorized after all applicable checks on the latest PR head and release build are green and unresolved reviews are addressed. Follow the live head rather than an older validation snapshot. Fresh private-ROM boot matrices on the final core candidate are complete; published-package qualification remains a pre-tag check; tagging, published-asset verification, master-to-develop back-merge and downstream info/site updates are separate publication work.
Tracker reconciliation
Confirmed AtariAge bug tickets belong to v3.10.0. The 4.0 milestone tracks threaded debugging/display pacing, stable-default qualification, completed TV pages/Auto mode and package/install/update validation. The hardware timing campaign and dual-Jaguar rollback remain separate future work. See #254 and #371.