Commit Graph
527 Commits
Author SHA1 Message Date
Joe Mattiello fd06d26f75 feat(app): add raw memory bitmap viewer and width guessing (#1095)
* feat(app): add raw memory bitmap viewer and width guessing

* fix(app): preserve synthetic OP previews across content switches
2026-10-26 00:54:02 -04:00
Joe MattielloandClaude Opus 5.5 fe699d5d3f fix(app): release a gdb-held halt on Detach/Reset/Open/Close and the Phase 3 halt follow-ups (#1090)
The app never answers the halt pump with DETACH any more (#1069).  Inside
GDBHalt() a dropped host let a gdb client's still-armed Z0 park the core
again with nobody to pump: threaded, the mirror read "not halted" and every
frame-lane call waited for gdb; inline, the window froze.  Detach, Open,
Close, Reset, a disc change and the hardcore flip all reached that.

One rule for leaving a halt (docs/app-debug-tools.md): every release is a
CONTINUE with the host kept, decided by one lane post on the emulation
thread with the core's real state.  Detach disarms and drops the app's
breakpoint list and defers the host drop to DebugCore::frameEnded(),
outside retro_run(); a new halt before then cancels the drop (that halt
needs the host) and an attach() in that window supersedes it.  Reset,
Open, Close and the disc commands (App::releaseForCall / callReleased)
disarm the enabled breakpoints for the release and re-arm them before the
first frame after the command, so the list survives a Reset (#1081 M1).
The give-up predicate of the call that follows fires only on a halt newer
than the released one (DebugCore::releaseSerial), which also covers a
Detach made while a step is in flight (M5).

Also from the Phase 3 review (#1081): the SRAM-autosave and achievements
idle posts are coalesced to one in flight so a long halt leaves the frame
lane bounded (M2; rc_client_idle still does not run while halted, on
purpose), halt_reason() is read before haltMu_ (M4), the same-pc fold is
documented in the pump (M3), nothing is refused while a Detach completes
(M6), and systemRam() carries its owner-thread note (M7).

Tests: debug_e2e_threaded sections 13 (threaded: Detach then Reset while a
gdb client holds a Z0 halt in the hot loop; an app breakpoint survives a
Reset) and 14 (the same Detach inline) fail on the old code and pass now;
debug_session_unit pins the rule against the mock (frame-end drop, the
cancel on a new halt, release + re-arm, the release serial, Detach then
Break).  Part of issue 967.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-24 07:11:16 -04:00
Joseph MattielloandClaude Opus 5.5 c38b7cb0a6 Merge master into develop after the v3.10.0 installer hotfix (#1083)
The installer fix and tests already reached develop via #1065. This brings over
the completed v3.10.0 qualification record from master and keeps develop's
installer.yml, which drops the develop push trigger as part of the CI trim (#1074).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 20:38:34 -04:00
Joe MattielloandClaude Opus 5.5 1dec1cb2fd feat(app): nightly builds default the emulation thread On, with the phase 3 gate (#967 3d) (#1082)
* test(app): no_threaded_limits keeps every feature on while the emulation thread runs (#967 G1a)

Fails when a threaded-only gate, its user-facing text or its limit test comes back, when the GDB stub is locked for any
reason but hardcore, when a file starts asking "am I threaded?" beyond its allowlisted count, or when tsan.supp grows.
Verified red on the pre-3a tree (abc9054e) and green on develop.

Refs #1079
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): Studio gate for the emulation thread with every feature on (#967 G1b)

--show-window tools opens all eight Tools windows, --perf-halt does F9's break and continue at fixed UI iterations, and
emu_thread_gate_check.sh asserts no owner violation, a window presenting at display rate while halted by F9 and by gdb
(ports from the OS at run time), a connected Neuro client answering presses, all with --emu-thread on.

Refs #1079
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): no_threaded_limits counts bare threaded() and fails on under-counts (#967 G1a fixup)

Check 5 now also counts the bare threaded() calls inside App and CoreHost, counts occurrences instead of lines, and fails on
an allowlist that is too loose; the over-broad "Off while" text pattern is anchored to the old Settings string; commented-out
add_test lines and ==/!= comparisons no longer confuse checks 3 and 4.

Refs #1079
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): build channel and the emulation-thread default rule (#967 3d)

VJ_BUILD_CHANNEL (release | nightly | dev, default dev) sets VJ_EMU_THREAD_DEFAULT (1 for nightly), next to VJ_TV_DEFAULT.
The default applies only while ui.emu_thread is unwritten: an explicit Off stays Off in nightlies and an explicit On stays
On in releases (emu_thread_default_unit). Nothing is built with the nightly channel yet.

Refs #1079
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): show the build channel and the emulation-thread default (#967 3d)

Settings > Interface says which way the default goes in this build, About names the channel, and the start-up log reports
channel, default, the stored choice and this run. The smokes derive their expected output from the configured channel, so
a nightly configure checks the On default and an explicit Off still winning; the inline hashes are untouched.

Refs #1079
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): the Studio gate wakes the display before each run and stays quiet after a kill (#967 3d)

A window created while the Studio display sleeps blocks forever in the GL swap, threaded or inline (soak-results.md), so the
G1c attempt-1 "hang" was the display. Before each of runs A, B and C the gate now wakes it (caffeinate -u -t 3), holds it
awake for the run (caffeinate -u -d -i, ended after the run) and re-checks system_profiler. Still asleep: the run is skipped and
reported "display asleep, run skipped" as an environment failure (exit 3 when nothing else failed), distinct from a hang.

A killed or skipped run no longer prints python tracebacks or "[: : integer expression expected": the CSV helpers answer
"no data" for a missing file, empty reads become 0, the per-run asserts that need logs are skipped after a watchdog kill,
and the shell's "Terminated" job notices are silenced.

Refs #1079
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): testServeDebugLane waits for the lane service to finish, not for a fixed sleep (#967 3b flake)

emu_thread_unit failed on GitHub's hosted macOS runner (run 39929091926) at "it waited about maxWait despite the frame post
(0.0 ms)". The test waited for the fake core to take its serveNow count, then slept 10 ms and read `waited`, which the
emulation thread stores only after the 4 ms lane wait returns; a thread descheduled in between left `waited` at its initial
0. Not the stale debug wake: nothing sets wakeDbg_ before that first wait (reopen() clears it, the frame post only sets wake_),
and a stale wake would make a wait of microseconds, not leave the value unset.

The test now counts finished rounds (stored after `served` and `waited`) and waits for the count, with a 5 s bound, and the
fake core sleeps 30 ms before serving so the old reading order fails every time. The assertion is kept; its upper bound is 2 s
instead of 0.5 s for a slow VM.

Refs #1079
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(app): nightly builds default the emulation thread on (#967 3d)

app-package.yml derives CHANNEL from release_tag (nightly; v* -> release; else dev) beside TV_DEFAULT and passes
-DVJ_BUILD_CHANNEL to the macOS, Windows and Linux configures; the flatpak job rewrites the committed manifest's
-DVJ_BUILD_CHANNEL=release to the lane's channel. app.yml configures one Linux row with the nightly channel so the On
default is built and tested on every PR. Docs updated, including the design text the G1d review found out of date
(closed windows and Neuro's watch copier, frame advance while halted, debugCall waits, the unstated bounds).

Refs #1079
Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 16:15:50 -04:00
Joseph Mattiello a8ed6b5332 docs: record completed v3.10.0 package qualification 2026-10-23 15:42:06 -04:00
Joe Mattiello 329a96edc8 fix(installer): preserve portable data and qualify published package updates (#1059)
* fix(installer): retain portable data and qualify published packages

* docs: record published Mac and clean Linux qualification evidence

* test(packaging): add clean Intel macOS qualification lane

* fix(ci): use distro audio packages and authenticated qualification metadata

* test(packaging): qualify downloaded Windows setup upgrade and uninstall

* test(packaging): require real upgrades, core loading and immutable candidate evidence

* test(installer): run Linux core qualification with a virtual GL display

* test(installer): resolve release tags for qualification provenance
2026-10-23 15:39:53 -04:00
Joe MattielloandClaude Opus 5.5 a1d9c4807a feat(app): Community cheats in the Cheats window (#1071, part 2) (#1076)
* feat(app): sha256 for verified downloads

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): CRC-keyed community cheat set in CheatManager

CheatManager loads <communityDir>/<CRC32>.cht for the running cartridge by CRC,
independent of the filename/title-matched database set. The pack is guarded by
vj_rom_crc32 like a revision-bound database set (unbound or mismatched: every
cheat unsupported plus a restriction), crc 0 (discs) never loads one.

Toggles persist under cp.<id> (database keeps on.<id>); ids are unchanged so an
updated pack keeps toggles for unchanged cheats and a changed cheat comes back
off, and cp.* keys that match no cheat are pruned on load. apply() orders
database, community (5000+i), then manual codes (10000+i). New public API:
loadChtText, setCommunityDir, reloadCommunity, communitySet,
removeCommunityToggles, CheatOrigin on isEnabled/setEnabled.

Also corrects two stale comments in cheats_test.cpp (sha256 vectors, linked
sources).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): drop dead prune loop, document setEnabledByName fall-through

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): community cheat store service

CheatStore fetches the community manifest, installs a cartridge's pack after
verifying it (size, sha256, vj_rom_crc32 guard, parse), tracks per-pack state,
and feeds CheatManager. Network and worker are injected hooks; results are
applied on the main thread through pump().

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): harden cheat store against throwing hooks, racing remove, message floods

A throwing fetch hook can no longer leave stop() hanging (RAII job slot plus
catch); remove() refuses while an install for the same CRC is in flight; and
manifest messages are capped at 8 entries of 200 bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Community cheats section in the Cheats window

Wires CheatStore into the app (#1071): the store, its one-request worker and
the http::get fetch hook (any status but 200 is an error, the store's cancel
flag is passed through) are set up in cheatsInit with downloads gated by the
single Library "Download art and cheat packs" switch (library.art, label
renamed) and cheats.community.url as the base URL override. The store is
pumped every frame, told of game load/close, and stopped (cancel first, then
the worker) before the cheat manager goes.

The Cheats window gains a "Community cheats" section between Database cheats
and Manual codes (install / update / remove / check states, credit line,
collapsed conversion notes, 256-write warning, hardcore line); both sections
share drawCheatRows, which now passes CheatOrigin. Buttons are disabled while
a request is in flight and stay live in hardcore, where only toggles are off.

--cheats-community-install is the headless twin (fetch the manifest, install
the loaded cartridge's pack); cheat_community_install runs it against a
loopback server serving site/cheats, then offline, then with nothing to
install from.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(cheats): describe the community cheat store (#1071)

Privacy page: a row for the project website's cheat store (when it is asked,
what is sent: the pack index and the one pack named by the cartridge's CRC32,
nothing else) and cheats/community/ in the local-files list; the art row
follows the renamed "Download art and cheat packs" switch. App page, app
README, the converter README and WHATSNEW describe installing packs from the
Cheats window.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): marshal cheat store game events to the main thread, report turned-off cheats

The store is main-thread only but cheatsAfterLoad / cheatsOnClose run on the
emulation thread in threaded mode. They now post the load / close
(CheatStore::postGameLoaded / postGameClosed, last post wins) and pump()
applies it before finished requests, so refresh(), announce() and the message
toasts stay on the main thread.

After an Update, the section says how many cheats that were on changed and
came back off (CheatManager::lastPrunedEnabled, CheatStore::turnedOff; cleared
by the next load or Remove). A store that was never reached says so instead of
"No community pack", and Check for updates is shown disabled rather than
hidden while checking or with downloads off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): keep an unreadable community pack removable, pump the store during a halt

The Cheats window derived Remove (and Update) from cm.communitySet(), so a
<CRC>.cht that exists but does not parse showed sidecar text with no way out.
Remove and Update now follow the store state alone; only the cheat rows need
the parsed set, and an unreadable or restricted pack gets a one-line notice.
The database "no set matches" status is dimmed when a community pack already
supplies cheats, a greyed Update says downloads are off, and haltedFrame pumps
the store so installs finish during a debugger halt.

Also documents the emulation-thread toast/notice contract in cheatsAfterLoad
and tidies style (comma-operator statements, std::set, include order).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): keep the community store fixtures byte-exact on Windows

The fixture pack's sha256 and size are pinned in the fixture manifest.
Git for Windows' autocrlf turned its LF into CRLF on checkout, so
cheats_unit failed verifyPack on every Windows app job. Mark the
fixtures -text so they are checked out exactly as committed.

Refs #1071

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): headless community install repairs a locally edited pack

--cheats-community-install skipped install() when the installed pack's
bytes no longer matched its sidecar (EditedLocally), then failed its own
Installed check, although the Cheats window offers Update in that state.
Install there too when the store offers the pack. The loopback smoke test
now hand-edits the installed pack and expects the command to replace it.

Refs #1071

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 03:23:05 -04:00
Joe MattielloandClaude Opus 5.5 caca12ad71 feat(cheats): CI-generated community cheat packs on the project site (#1071, part 1) (#1073)
* docs(cheats): community cheat store design and plan (#1071)

Refs #1071

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(cheats): map printf_notify to app toasts

Refs #1071

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(cheat-import): BigPEmu API map; int_full settings omitted; printf_notify ignored

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(cheat-import): refuse side effects in message args; scope Unsupported.call to API gaps

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(cheat-import): committed cartridge identity table (hashes only)

scan_cartridges.py walks a ROM directory (read-only) and writes a sorted
FNV1a64 -> CRC32 table with size, title and name; no ROM bytes or paths.
load_cartridges rejects duplicate fnv/crc, bad hex and format != 1. The
generated cartridges.json (152 rows) lets CI map community script hashes
to the CRC32 the app binds packs to without any ROMs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(cheat-import): community cheat pack generator (#1071)

build_packs.py converts every pinned community C cheat source with convert.py
and writes the site/cheats tree: per-cartridge .cht packs (one per CRC32, sources
merged, identical cheats deduped, same-name cheats suffixed), manifest.json,
the unsupported.json worklist with per-call API usage, index.html and README.
Deterministic, bounded fetch of one codeload tarball per source, --source-dir for
local checkouts, --summary and --bump for the bump workflow. sources.json pins
jguffer/BigPEmu_Scripts at 2421150.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* data(cheats): generate community packs from jguffer/BigPEmu_Scripts@2421150

23 cartridge packs (72 cheats, 9 partial) plus the unsupported.json worklist
(55 entries), manifest and index, generated with build_packs.py from the pinned
codeload tarball; byte-identical to a local checkout run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(cheat-import): worklist unmatched literals beside a hit; bound fetch errors and member count

- identity row for table-missing literals in a file that also has a hit
- http.client.HTTPException during fetch is a FetchError (exit 2)
- tar expansion cap counts a header block per member and caps member count
- Collector uses the api_map given to build()

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* data(cheats): regenerate for generator fix (unmatched-literal identity rows)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(site): publish community cheat packs under /cheats/

build_site.py copies the committed site/cheats tree verbatim; check_site.py
gains check_cheats (manifest format/sha256/bytes/CRC guard, unlisted .cht,
unsupported.json, index.html links, 256 KiB manifest and 1 MiB tree limits)
with unit tests. pages.yml watches site/cheats and tools/cheat-import. The
converter README now documents the author's permission, the credit, the
sources -> cartridges -> packs workflow and how to add a source; the
community_test harness and its fixtures are removed (the committed packs and
the regeneration diff are the conversion contract).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(cheats): regen-diff job, concurrency, weekly pin-bump PR

cheat-import.yml: add site/cheats/** to the path filters, a per-ref
concurrency group, drop the community_test.py fetch step (the files are
gone), and add a packs job that regenerates twice, diffs the runs and the
committed tree, flags unmapped BigPEmu calls, then runs the site-check tests
and build_site + check_site.

cheat-packs-bump.yml: weekly (Mon 06:23 UTC) poll of each pinned github
source; on movement, bump the pin, regenerate, validate, force-push the fixed
branch cheats/pin-bump and open or refresh a develop PR (validation failures
head the body instead of being dropped), then dispatch cheat-import.yml on the
branch because GITHUB_TOKEN PRs do not fire pull_request workflows.

test_committed_sources no longer pins the commit/date value: every bump PR
would otherwise fail the unit tests; only the shape is asserted.

Refs #1071

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(cheats): gate bump workflow to the upstream repo, drop bot trailer

Only libretro/virtualjaguar-libretro opens bump PRs (the Provenance-Emu
fork runs crons too), with a 30 minute job timeout. The bot-authored bump
commit no longer carries a Claude co-author trailer.

Refs #1071

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(cheats): pin pycparser, force LF, add job timeouts

Pin pycparser==3.11 (newest release that installs on the Python 3.10 CI leg;
2.22, 2.23, 3.0, 3.11 and 3.12 all regenerate byte-identical packs). Add a
root .gitattributes forcing LF for the generator and generated tree so pack
bytes cannot drift by checkout. Give both jobs a 20 minute timeout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(cheats): dedupe pack header title, refuse control-character paths

The pack's first line printed the title twice when the cartridge name began
with the title. Print the name alone in that case. Fetch and local-dir reads
now fail closed on any path containing a control character, since names can
reach ::warning:: annotations and PR bodies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* data(cheats): regenerate packs with deduplicated header title

Line 1 of each pack no longer repeats the title when the cartridge name
already starts with it. Regenerated from the pinned source commit; two runs
and a --source-dir run are byte-identical.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(cheats): even indentation on a bump-workflow continuation line

editorconfig-checker wants multiples of 2 in workflow files.

Refs #1071

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 02:45:05 -04:00
85cd3f1642 feat(app): debugger halt and GDB stub with the emulation thread on (#967 phase 3b) (#1068)
* feat(app): bounded halt pump on DebugCore; the UI mirrors the halt

DebugCore owns the halt: pump() serves the debug lane, waits at most kPumpWait (4 ms) on a debug-lane-only wait and
returns VJ_DBG_RESUME_NONE to GDBHalt's own loop, which keeps the socket service and the halt timeout.  The UI writes
its verdict tagged with the halt serial it saw, so a verdict for a dead halt never resumes the next one; frameEnded()
closes a halt the core left on gdb's say-so.  DebugSession mirrors HaltState once per UI frame (syncHalt) and fires
onHaltUi / onResumeUi on serial edges; attach/detach/breakNow are ordered through the lane and a pending detach.
Inline keeps the nested UI frame.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): a STEP the core never re-halts must not leave the UI mirror halted

pump() returns STEP without bumping the halt serial, so the bar stays up while the core steps.  If the frame ends
before the core re-halts (the stepped instruction ended it, or the target had stopped), frameEnded() found nothing
halted and the mirror stayed halted for good; inline, stepEmulation() is gated on halted(), so the frame that would
re-halt never ran and F9 / Detach wrote verdicts nothing took.  DebugCore now remembers a STEP in flight (cleared by
the next halt entry or setIface) and frameEnded() closes it like a foreign resume.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): verdicts and detach only against the core's live halt

After a STEP verdict the halt serial is unchanged but the core is running until it re-halts, so DebugSession::detach()
offered a DETACH for a serial the re-halt then dropped, left detachPending_ set (attach / Break refused) and, with
hardcore turning on in that window, left the core attached.  resume / step / detachAndResume now require the core's
halt to be live (parked, same serial); detach and detachAndResume fall back to the lane detach otherwise, and that post
also releases the pump if it runs inside a live halt.  haltedFrame() syncs the mirror first so the first nested frame
meets the current halt; the inline resume hook keeps headless audio unpaused as before.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): the emulation thread pumps the halt; quitting ends it

Threaded, the pump's wait is App::haltWait (serve the debug lane for at most 4 ms, refill the views when a request is
pending); frameEnded() runs after every retro_run so a halt gdb ended is closed on our side (inline: from syncDebug
outside a frame).  beginQuit() sets quitting_, wakes the pump and waits at most 200 ms for the detach the pump answers
with, before the auto-save, the SRAM flush and the 3 s unload; closing the window inside an inline halt takes the same
path.  haltedFrame runs only inline.  The halt bar says "HALTED by gdb" for a halt the app did not ask for; a halt at
the CPU window's own temporary breakpoint (run to cursor, step over) is ours (DebugSession::ownsBreakAt).

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): calls and frame advances give up when a halt begins; content changes detach first

EmuThread::call and advance take a give-up predicate: a call queued behind a frame that halts returns instead of parking
the UI behind the busy spinner; advance registers its budget on the debug lane and waits with the same predicate.  Every
UI-thread call passes one (App::emuCall).  Threaded, open / close / reset / disc detach the debugger first and then
proceed; save / load state and boot refuse with a toast; frame-lane posts wait for Continue and the halt bar and the
Core options tab say so.  Inline keeps refusing (the call would run inside retro_run).  RetroAchievements replies ride
the debug lane, so none is ever cancelled.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): a given-up disc follow-up retries; close reports a re-halt; deterministic give-up and re-drain tests

followDiscSwap advances seenDiscSwaps_ only when its call ran, so a halt no longer loses the RA / cheats follow-up.
closeContent says so when its detach predicate gave up.  The netplay headless step stops re-budgeting a frame whose
advance gave up.  testCallGiveUpLosesToStart covers the compare-and-swap-lost branch of call(), and
testAdvanceBudgetDoesNotOvertakeFramePost covers the loop's second frame-lane drain; the 300-iteration smoke loop is gone.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): the halt and the GDB stub work on the emulation thread; a silent host behind every halt; hardcore locks the stub

The phase 1 limits are gone: Break / Step / breakpoints work threaded and virtualjaguar_gdb_stub is a plain option again.
Whenever the stub is enabled the app attaches its host on the emulation thread inside the load (before the first frame,
so a gdb_wait boot halt is seen) and again after any frame it is missing, so every gdb halt flows through the app's
pump: the halt bar shows it, calls give up, quitting detaches.  RetroAchievements hardcore locks the stub to disabled at
load, and a hardcore flip with the stub open reloads the game so the lock reaches the core (phase 3 plan, decisions 2
and 3).

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): B4 review: pre-frame silent host, gdb halts survive headless inline, Detach clears breakpoints

- emuRunOneFrame re-attaches the silent gdb host before the frame, so a gdb halt right after a Detach is hosted
- headless inline haltedFrame releases only app halts and quit; gdb's halt keeps the core's socket loop serving
- Detach drops the session breakpoint list with the core's table
- Settings > Emulation thread hint reflects that the halt and stub work threaded
- locked-option warning is generic; a hardcore restart waits out an inline halt

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): the halt and the GDB stub end to end on the emulation thread

debug_e2e_threaded drives break / step / continue / detach / hardcore / quit from a UI thread that never blocks over
50 ms, pins the option post that applies after Continue, the save state that gives up when a halt begins, a posted lane
detach run inside a live halt, the 3 s join, and a gdb_wait boot halt with no client (Detach, quit).  gdb_threaded_halt
finds a headless run halted at boot from a gdb client, reads memory, continues, and quits the app mid-halt, threaded and
inline, plus an inline hardcore variant: SIGINT / SIGTERM end a headless run cleanly (installed after SDL_Init, headless
only).  The headless inline loop now closes a gdb halt after each frame (frameEnded + syncHalt), without which the
hardcore reload retry stays blocked.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): B5 review: the cancelled save is proven absent after frames run; free ports; signal handler resets

debug_e2e_threaded waits for frames after the halt ends before asserting the cancelled save left no file (it is held while
halted, so the old check was vacuous) and saves to a different file; the gdb_wait sections probe a free port; the test
also fails on ThreadSanitizer.  The signal handler restores SIG_DFL after storing the flag (a second signal kills a wedged
run) and g_signalQuit is asserted lock-free.  gdb_threaded_halt.py's watchdog kills live children; a missing Python3 is
reported at configure time.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): the halt and the GDB stub on the emulation thread

Halt model, Detach, hardcore lock and reload, gdb_wait, headless signal quit,
testing; README, testing.md, design section 6 (2026-10-22 as-shipped notes),
plan 3b task list.  Includes the MSVC fix: debug_e2e_threaded includes
<ws2tcpip.h> for socklen_t.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): quitting while a gdb client holds a halt no longer hangs

The pump answered a quit with DETACH, which drops the app's host inside the core.
A gdb client's breakpoint that is still armed (a hot loop) then fires again at
once, with the socket open and no host: the core parks in GDBHalt() for the
client, and the quit's flush, autosave and unload wait on it forever.  The pump
now answers CONTINUE and the host stays attached (unloading drops it).  Every
wait in the quit epilogue is bounded (flush 1 s, autosave 1.5 s, skipped while
the core is still halted); the unload keeps its 3 s backstop.

Also: re-attach the silent gdb host before each frame (inline stepEmulation,
headless inline loops), comment/doc fixes, EmuThread::stop precondition, and the
three gdb tests' PASS regexes lose their ^ anchor.

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): a hardcore restart that gives up stays pending and retries

Achievements::pump cleared resetPending_ before the restart and ignored its result. Threaded, the
restart can give up when the core halts again (a gdb client with a breakpoint still armed): hardcore
was then active without a clean restart and the open GDB stub bypassed the hardcore lock.

The restart now stays pending until it takes. reset() returns whether it ran. A give-up with the game
still loaded (and, for the reload, the stub still open) retries at the next pump; a restart that
failed to load the game, or closed the stub, is dropped. The cheat reset is posted once and the
restart is counted once (--ra-expect-reset), the give-up is logged once.

Tests: VJ_TEST_RESTART_GIVEUPS=N makes the first N restarts report a give-up (a headless run cannot
stage the re-halt); gdb_hardcore_flip_giveup_* (reload path) and ra_hardcore_reset_giveup_* (reset
path), inline and threaded.

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): a signalled threaded --neuro headless run still prints the Neuro report

The signal-quit block returns before the TIMING/report section; report first (it only
touches UI-thread state) so a SIGTERM'd --neuro run behaves like a finished one.

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): gdb_* tests take their port from the OS at run time (#967)

The six tests with hard-coded ports 23457-23463 (gdb_enabled, gdb_enabled_threaded,
gdb_hardcore_flip_*, gdb_hardcore_flip_giveup_*) all failed on the hosted Windows
ARM64 runner with 'failed to open port': every one of the distinct ports was
refused, which points at a reserved/excluded range, not a collision (the Python
gdb tests, which already pick a free port, passed there).

tests/with_free_port.py binds 127.0.0.1:0, reads the port back, substitutes {PORT}
in the command and, with --config-ini, writes the saved stub setting for the
hardcore tests.  The assertions and ctest regexes are unchanged.  Registered only
with Python3 (STATUS message otherwise).  RESOURCE_LOCK vj_gdb_port on every test
that binds a stub port closes the bind-0 TOCTOU window.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): start Winsock before the core loads so the GDB stub can bind on Windows (#967)

Every gdb_* test that opens the stub failed on hosted Windows x64 with
"[GDB] stub enabled but failed to open port N", on fixed and on OS-chosen
free ports alike. The core's GDBSockOpen (src/debug/gdbsock.c) calls socket()
without WSAStartup, because its libretro host (RetroArch) starts Winsock. The
app never did on that path (neuro and netplay only start it for themselves),
so socket() failed with WSANOTINITIALISED and the stub stayed disabled. The
feature was therefore broken for users on Windows too, not only in tests.

platform::initNetworking() now runs WSAStartup once, right after SDL_Init and
before any core load, headless runs included. A no-op on macOS and Linux.
Winsock is reference counted, so neuro's and netplay's own WSAStartup stay
harmless. Also corrects the comments that blamed reserved port ranges.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): with_free_port.py hands the app explicit stdout/stderr (#967)

The Packages / Windows x64 job builds the GUI-subsystem exe (VJ_APP_WIN32_GUI). There, gdb_enabled, gdb_enabled_threaded and the gdb_hardcore_flip* tests missed their PASS regexes. Popen(cmd) left the child's standard handles to inheritance, so Windows started the GUI-subsystem app with no stdout. console_attach.cpp then attached the runner's console, and the output never reached ctest's pipe. The wrapper now passes stdin=DEVNULL and stdout/stderr explicitly, so the output stays on the pipe. Tests that ctest launches directly already get redirected handles and were unaffected.

Refs #1075

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: dry <dry@x>
2026-10-23 02:36:04 -04:00
e7009c440a ci: trim develop-push runs, debounce nightly, route macOS to Studio runner (#1074)
* ci: stop re-running PR-covered workflows on develop pushes; debounce nightly; route macOS to Studio runner

Drop develop from push: in c-cpp, app, regression-test, acid-test, installer,
jaguar-demos, microbench, jaguar-toolchain and cheat-import (PRs already ran
them); ignore develop in pull-request.yml. release.yml's nightly waits 10 min
in its guard job so a merge burst builds once. macOS rows of c-cpp/app/
regression and the unsigned iOS/tvOS nightly rows use the self-hosted Studio
runner on trusted events only, with a VJ_STUDIO_RUNNER=off kill switch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci(spm): find blitter_simd objects independent of SwiftPM build layout

Xcode 26 SwiftPM keeps objects under .build/out/Intermediates.noindex/...
instead of <triple>/debug/virtualjaguar.build, which broke the vtable check on
the Studio runner. Search by object name and fail loudly if one is missing.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: dry <dry@x>
Co-authored-by: Claude Sonnet 5.5 <[email protected]>
2026-10-22 23:24:46 -04:00
Joe MattielloandClaude Opus 5.5 7e2f5967ee feat(app): TV mode auto-switching, first-use hint and build default (TV mode phase 4) (#1067)
* docs(app): revise TV mode plan Phase 4 against develop (#991)

Phase 4 was written before Phases 1-3 and desktop pad navigation (#1030)
landed. Re-point the tasks at the current code: the controller trigger
gates on deskPadActive and consumes the switching press, package defaults
are set in app-package.yml (not release.yml), the first-use hint text
comes from the pad's actual TV-menu binding. Adds Phase 4 approvals A-G
and a placeholder for the user's layout fixes.

Refs #991

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV mode switches on in fullscreen and on a controller press (#991)

Wires the Auto setting. Until now Auto acted as Manual (effectiveSetting), so
the fullscreen and controller triggers TvMode already implements were never
fed. Why each piece is shaped as it is:

- App tells TvMode about fullscreen edges only (tvFullscreenEdge against
  tvNoteFsSeen_): TvMode re-applies a repeated onFullscreen(true), and macOS
  sends enter events on space and display changes, which would turn TV mode
  back on after a manual off.
- A mapped pad button (a menu intent: D-pad, face buttons, shoulders, Start)
  pressed while the desktop library owns the pad and no game is loaded
  switches TV mode on and is consumed, so it neither moves desktop focus nor
  acts on the TV screen it reveals. Guide/Share/touchpad/paddles/sticks are
  not triggers: they carry app actions or drift. The call sits before the
  TV shell so presses the shell consumes still give a controller-started TV
  mode a fresh mouse budget (otherwise mouse jitter would sum up and drop
  TV mode while the pad is in use).
- Mouse travel and a printable key the TV UI did not use hand a
  controller-started TV mode back; F-keys, Esc and Ctrl/Cmd/Alt shortcuts
  never count (F4 and Ctrl+Shift+T are toggles themselves).
- effectiveSetting() is gone; a config with no ui.tv_mode takes
  defaultSetting(VJ_TV_DEFAULT_SETTING) (Auto), never written back, so the
  build default can move. setTvSetting/setTvAuto are the one way to change
  the settings; the script gains `fullscreen on|off` and `expect tv-setting`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV mode in Settings > Interface, the Auto rows in TV Settings, VJ_TV_DEFAULT (#991)

Settings > Interface gets a TV mode section (Off / Manual / Auto, plus the two
Auto checkboxes while Auto is chosen) and TV Settings gets the matching
Auto: in fullscreen / Auto: when a controller is used rows, so the Phase 4
triggers can be switched without editing the config.  Both routes go through
App::setTvSetting / setTvAuto and save afterwards; the setters themselves never
save.

The default a fresh config starts from now comes from CMake: VJ_TV_DEFAULT
(auto | manual | off, validated) defines VJ_TV_DEFAULT_SETTING for the
virtualjaguar target, so a nightly can start on Auto while a release package
starts on Manual.  A stored ui.tv_mode still wins, and is only written when the
user changes it.  The headless script also pins approval C at App level (Esc,
Enter and a Ctrl shortcut never end a controller-started TV mode) and walks the
new rows, shifting the script's later lines to make room.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(app): release packages default TV mode to Manual, nightlies to Auto (#991)

The TV mode default now comes from CMake's VJ_TV_DEFAULT.  app-package.yml sets
it per build from release_tag: vX.Y.Z tags and prerelease start a fresh config
on Manual, nightly, prNNN and dev builds stay on Auto.  The three native
configures pass it as -DVJ_TV_DEFAULT, and the Flatpak, which is built straight
from its manifest, gets the same value through one sed on config-opts before
flatpak-builder runs (the committed manifest keeps CMake's default).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): make the TV Settings Auto-rows walk independent of earlier tests (#991)

The headless block that walks to "Auto: in fullscreen" relied on the shell
still remembering Settings > Interface with focus on More settings..., left by
a test far above it.  It now presses Back twice (Home from either level of the
Settings screen), opens a fresh Settings visit and clamps its way to the row:
Up x4 / Down x3 on the groups, Up x8 / Down x4 in the list.  It also checks
that the Auto checkbox change, which saves the config, still leaves ui.tv_mode
absent, and the CMake comment now says app-package.yml always passes
VJ_TV_DEFAULT.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(app): fail the Flatpak build when the TV default sed did not apply (#991)

Check the manifest for the exact config-opts line after the sed instead of
printing it, so a changed manifest cannot silently ship the wrong default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): anchor the Auto-rows walk with expect tv-focus (#991)

The previous walk assumed a fresh Settings visit starts on the groups.  It does
not: the shell's memory is never cleared, so the Base layer resumes the level
(list or groups) and row an earlier test left, and TV mode comes back on at
Home, so the Back presses before the first visit did nothing.  The stray Right
after the Up/Down presses only passed because the later Up x8 re-anchored it.

Add `expect tv-focus=LABEL` (Screen::focusLabel(), Shell::focusLabel(); Settings
names the group on the groups and the row in its list) and rewrite the walk so
every step is asserted before the next one acts: open Settings, one Back, LB x3
to Home, RB x3 back (now on the groups either way), Up x4 reads Picture, Down x3
Interface, Right enters the list, Up x8 reads Show FPS, Down x4 reads Auto: in
fullscreen, and only then Left / Right change a value.  Also fix the Settings
screen's comment: the Base layer resumes the remembered level, only the menu
slot always starts on the groups.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): one-time TV mode hint on the first controller press in the library (#991)

Under Manual nothing tells a new player how to reach TV mode, and the
button differs per pad (Share hold, Guide hold, a paddle) and per OS. The
first pad press the desktop library takes now shows a 6 s toast naming this
pad's own TV-menu button and the keyboard's (tv::firstUseHint; the fixed
Ctrl/Cmd+Shift+T stands in until two are known).

The key ui.tv_hint_shown is saved before the toast and a per-run flag backs
it, so it cannot show twice; Auto (whose switching press returns before the
hint site) and Off never show it. A Settings reset of Interface clears the
key, so it may show once more. The pad text uses the short names (Share,
Guide) rather than Binding::display()'s "Share / Capture".

The TV script gains expect toast=TEXT, toast-count=N and hintreset, and the
Settings > Interface hint now says nightly and local builds start on Auto.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): TV mode section in the README (#991)

Document what TV mode is, the three settings and where they live, the Auto
rules, the build default (-DVJ_TV_DEFAULT: Auto for local and nightly
builds, Manual for releases), the first-use hint, --tv, the 500 ms hold and
the Guide / keypad-chord limitation. The existing key-table rows link to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): hint names the pad's own button; mouse verb sends real motion; Home copy for a library with games (#991)

Review of Task 18 and the Studio windowed check found four things.

The first-use hint named "Share" on every pad, but SDL's MISC1 is the
DualSense mic and the Switch Pro Capture button, and Guide is PS / Home on
those. tvMenuButtonName(family, binding) names it per family (the virtual
pad keeps "Share (hold)"). Settings > Reset group on Interface now also
clears the per-run flag, so the hint may show again as the README says.

The windowed mouse check left TV mode on because SDL reports a warped
pointer with xrel = yrel = 0, so the 32 px budget never accumulated (the
product path was fine: the events reached TvMode::onMouseMove with zero
travel). `mouse` now also pushes a MOUSE_MOTION carrying the travel, in
headless runs too, and a headless block pins the handleEvent wiring.

Under Auto a new player with 20 games landed on Home saying the library
was empty. With games but no shelves Home now says "Nothing to continue
yet. Press RB for your Library."; the old text stays for a truly empty one.

The Off "no hint" check now proves the press reached the desktop nav.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): Home empty state without a per-frame library copy; no doubled "(hold)"; README Off wording (#991)

TV Home with no shelves is the landing state of a new Auto user, and it
asked the host for List::All every frame to find out whether any games
exist, which builds a GameRef (several strings) per library entry. A
Host::hasGames() answers it with no copy and an early exit.

tvMenuButtonName appended " (hold)" to navButtonName(), whose fallback
Binding::display() already adds it, so a rebound hold on a paddle read
"Paddle P1 (hold) (hold)". It now names a non-hold copy and suffixes once.

README: Off never turns TV mode on, but with a game running the TV-menu
button still opens the in-game menu (spec 8).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): TV mode plan Task 16a closed, no layout fixes needed (#991)

The owner tested phases 1-3 on a TV and reported no layout fixes, so
the last open task of the plan is done and phase 4 completes #991.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): Settings opens the TV Settings screen while TV mode is on (#991)

Every Settings entry (Ctrl/Cmd+Comma, the menu bar, the native macOS menu / tray
and Cmd::Settings) set showSettings, the desktop window, even in TV mode, so a
player in a game could not reach settings without the plain UI.  They now go
through App::openSettings(): TV mode on with no game switches the Base layer to
the Settings section; with a game it opens the in-game menu (same pause /
hardcore gate as the TV-menu action) with the Settings screen in its slot, so B
returns to the menu root.  TV mode off is unchanged.  Ctrl/Cmd+Comma still
closes an open desktop window (More settings...); in TV mode a second press
leaves the TV screen as it is.  The BIOS-tab entries stay desktop (the TV
Settings has no BIOS tab).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): the TV menu opens without pausing when hardcore refuses; BIOS entries use the TV hand-off (#991)

Settings must stay reachable mid-game, so when Achievements::allowPause refuses
the in-game menu (the F4 action and the Settings route alike) opens anyway,
like the netplay rule: the menu owns the pads, the game runs on, and the menu
shows a 'Hardcore: the game keeps running' banner.  menuResume restores the
pad ownership as after a netplay menu.  The refusal toast is dropped from this
path (nothing refuses any more).

The BIOS-tab entries (menu bar, native Cmd::BiosSettings) now go through
App::openBiosSettings: with TV mode on it is the pad-driven More-settings
hand-off (the menu first with a game), else the desktop window as before.

Test seams: the 'command NAME' and 'hardcore-pause deny|allow' script verbs and
the menu-owns-input expect.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(app): TV Settings routes through the native commands, the Save page, hardcore and BIOS (#991)

Native-menu path via the new 'command' verb (a synthesized ctrl+, is ignored in
a real window), the menu root and Save page routes, the hardcore-refused menu
(runs on, owns the pads, hands them back), command bios in TV mode, and smoke
tests for Shell::openSettings and the hardcore banner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): TV mode plan Phase 5, every setting and the Library in TV mode (#991)

The owner found TV mode stops at the game: Settings and the Library open
the desktop windows mid-game, and TV Settings lists a handful of rows
with the core options hidden until a game has run. Phase 5 reads the
core's option definitions at startup so every core option and every
choice-like app setting has a TV row, and routes the Library through TV
screens too (an in-game Library row; a launch asks first).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): core options readable at startup; TV Settings catalogue of every core option and app choice (#991)

The owner could not see the game core's settings in TV mode: the option table only
reached the app through CoreHost::startSession, so until a game had run the TV
Settings hid the three curated core rows and said "Available once a game has run".

CoreHost::probeOptions() runs retro_set_environment once at startup, before the
emulation thread exists, answering only the option commands (everything else is
refused), then the core's own display pass, so defs, categories and visibility are
filled exactly as a load fills them.  The core is then taken back to a fresh
process's state with retro_deinit, the call every reload makes before its
retro_set_environment: the display pass leaves its show_* latches at "hidden", and
without the reset the first game's own display pass finds no change and every gated
row (netlink host, voice chat, mouse and rotary tuning, CD-only keys...) stays
visible.  core_options_probe checks that the first load after a probe has the same
visibility as the second.

The TV Settings model is now a catalogue: SettingRow::group is a string key plus a
label, coreOptionRows lists every visible core option by the core's categories
(overridden = read-only, Host-kind typed keys read-only, "(custom)" for a value off
the list, a restart note), sliderRow turns the desktop sliders into stepped lists
with a "(custom)" entry for a finer value, and AppHost lists every choice, toggle and
slider of the desktop tabs (Picture, Sound, Interface, Library, Achievements,
Discord, Neuro) before the core's groups.  The curated core rows, the speed note and
the fixed four-group enum are gone.  tv_settings.cpp only follows the new group type;
the rail's scroll and the long-list focus are the next task.  tv_menu_headless walks
the longer lists with Down x20 (clamping) and a new test-only `settings-group KEY`
verb that sets the shell's rail memory, so no count of rail items depends on the
build.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): a "(custom)" setting value sits between its neighbours, so Left and Right step the right way (#991)

A slider value off the list (the audio buffer's own default of 64 ms, an off-grid
volume, dead zone, tile size) was appended last, so Left jumped to the end of the
list and Right did nothing.  sliderRow now inserts "<value> (custom)" at its sorted
position and stepValue maps indices past it back to the grid: 64 ms sits between
60 and 70.  A typed numeric core option whose presets are in numeric order (a clock
scale, a port) gets the same through coreCustomPos, which setSetting uses to map
the stepped index back to the option's values; text, addresses and unordered presets
keep the entry last.

Also says in a comment why probeOptions drops the display callback: gating options
do not show or hide dependent rows until the first game loads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV Settings scrolls its rail, pages its list, and shows every group (#991)

With the core's categories joining the app's tabs there are ~17 groups, and
the rail and the in-game groups level drew every one at a fixed pitch, so the
tail ran off the screen and the focus could land there.  Both now scroll a
whole item at a time (LinearFocus::first eased by the Phase 3 tween, snapped
on the first draw), keep the focused item in view, show "more" arrows with
room reserved for them, and draw no item that is wholly outside the window.
The list scrolls the same way, so no row is cut mid-text at rest.

LT / RT (PagePrev / PageNext) jump a screenful on the list, the rail and the
in-menu groups level, and stop at the ends; a "Page" hint appears when the
list scrolls.  A held direction already repeats on the nav's timer, so
Shell::repeatIntent() (NavInput::lastWasRepeat) lets the screen tell a repeat
from a press where it matters: a held Left stops at a row's first choice
instead of leaving for the rail, and a Right held from the rail enters the
list without then stepping its first row.

Test support: expect tv-group=KEY (Screen::focusGroup), an opt-in text
position probe in the widgets, and a headless walk that steps the core's
internal resolution with no game loaded, checks opt.* is saved and that
the next game reads it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Library is a TV screen while TV mode is on; in-game Library row with a launch confirm (#991)

While TV mode is on every Library entry point (Ctrl/Cmd+L, File > Game library,
the native Cmd::Library) goes through App::openLibrary(): the Library section
with no game, the in-game menu with the Library in its slot with one (the
hardcore pause gate and its toast apply, as for Settings). The menu gains a
"Library" row after Load state; the slot screen is wide() so the rail and cover
grid draw over the dimmed game. Typing reaches the slot; Details and the letter
picker (Base stack only) are not offered there.

Launching from the slot asks first (Page::ConfirmLaunch, "Close <running> and
start <title>?", No / Yes, focus on No); Home, Library and Details launch
through Shell::requestLaunch, which starts at once when no game runs. Yes goes
through the existing library launch (the app auto-saves and closes the menu),
then launched() arms the release guard. Script expects: menu-page=..., and
tv-focus reads the in-game menu while it is open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): TV Library review fixes: no desktop library over TV mode at startup or on its on-edge; launch confirm rows match Reset/Close (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): TV Settings and placeholder tiles wrap long text instead of cutting it

Studio shots showed long setting labels cut with "..." (two Per-Title
Enhancement rows read the same; notes lost their ends) and Library
placeholder titles split mid-word or running off the tile.

- Settings in the in-game menu's slot takes the whole panel (wide(), as the
  Library does) and names itself in a caption line; the label gets the row
  width less a value column sized by the widest choice, and wraps at words
  to two lines (a note too); every row of a group is as tall as the tallest
  so whole-row scrolling still holds.
- widgets::wrapLines / textLines: word wrap with a last-line "..." cut at a
  word; the text probe still sees the whole text once.
- Placeholder tiles wrap at words, drop to the 24 u floor before splitting a
  word, and sit at the foot with the "..." only on the last line that fits;
  the menu's Library uses tighter columns so "Cybermorph" fits whole, and
  waits to draw a row until it fits above the panel's foot.
- Scroll arrows sit clear of the focus ring (arrow room 26 u); the groups
  level has its caption so its up arrow stays inside the list area.
- The rail and the groups level list "Page" for LT / RT, joining only the
  glyphs that exist.
- Core options already prefer desc_categorized (coreRow); the Per-Title rows
  have none in libretro_core_options.h, so wrapping is what tells them apart.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): F8 does not pause the game behind the hardcore run-on menu

While the menu runs on over a hardcore game (its banner says the game keeps
running) a pause-decay countdown can end and F8 then paused the game the
player cannot see, to be resumed as the menu closes.  menuRunsOn_ is treated
like menuHeldPause_: F8 is refused with the same toast.  tv_menu_headless
covers it (allow the pause while the menu is open, then F8) and `command bios`
with no game (the window opens alone, B closes it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): the launch confirm lists Yes above No

Phase 5 item 8 said "rows No / Yes", and the test description said Down for
Yes; the rows are Yes, No with the focus on No, so Yes is Up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 22:11:32 -04:00
Joe MattielloandClaude Opus 5.5 6ce76c1cf8 feat(app): Neuro on the emulation thread (#967 phase 3c) (#1066)
* feat(app): AgentPlayer and the snapshot's agent channel (#967)

The Neuro agent's input becomes a program the emulation thread replays:
InputSnapshot gains an agent channel (AgentStep / AgentProgram /
AgentProgress; publishAgent, takeAgent, reportAgent, agentProgress) behind
its own mutex and a gen atomic, so the per-event lock is untouched.
InputQueue gains version, lineage, firstFrame, trim and program, and
AgentPlayer is the pure emulation-thread replica (load, frame, progress),
driven by pollAgent. Unit-level only: the app is wired in the next task.

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Neuro presses through the input snapshot (#967)

The UI-side Agent now publishes the input it accepted as an AgentProgram
(Agent::publishTo) and absorbs the replica's progress (Agent::absorb), which
trims its queue and drives busy(), pressedNow() and framesPlayed().  The app's
input poll runs the AgentPlayer replica one step per emulated frame, inline and
threaded alike (pollAgent); Agent::frame() and InputManager::agentControls are
gone (InputManager::addAgentControls keeps the inline hashes unmoved).

The #1025 pins are ported to the replica (testAgentGating through a Replica
helper, loopback with a frame-numbered record) and extended (testAgentProgram:
busy covers older progress, release_all and hardcore clear the replica within
one poll, Neuro off, append mid-play does not replay).  New neuro_core_e2e runs
the replica against the real core (inline, lock-step threaded, free-running at
600 Hz, disabled) and neuro_app_inline runs the headless app against a fake
Neuro server.

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Neuro reads RAM watches through a DebugViews request (#967)

Neuro no longer reads core RAM through the retro_get_memory_* externs.
The profile's watches are one DebugViews request, a Memory request over
the covering range or a Dram request when the span is wider than 64 KiB,
and the emulation thread's copier fills it after each frame.

neuro::WatchFeed (neuro_watch.h) owns the request on the UI thread: it
gives a changed range a fresh id so the old range's buffer is never read
as the new one, reads the buffer at most every 0.25 s, and adopts it only
when it is ok, current and answers exactly the request asked.  Anything
else leaves the state text at "?".  GameState::ram / ramSize become
watchBytes / watchBase, watchValue gains a base argument, and
Agent::watchRange gives the covering range.

Headless inline runs move the refresh serial (syncDebug) and run the
copier after each frame, and the end-of-run report prints the state line
(neuro_watch_inline pins a cheat byte read through the Dram path).

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Neuro works with the emulation thread on (#967)

The gate that turned Neuro off while the emulation thread is on is gone:
settingsFrom no longer disables it, neuroInit does not log "off for this
run", and Settings > Neuro drops the hint.  The program/replica design from
the earlier 3c commits (AgentProgram through the input snapshot, AgentPlayer
in the core's poll, watches as a DebugViews request) is what makes this safe.

The threaded headless --neuro run ticks Neuro in lock step: one frame at a
time, in real time, like the inline loop, and neuroHeadlessReport prints in
both modes.

neuro_off_threaded is replaced by its twins: neuro_on_threaded,
neuro_watch_threaded and neuro_loopback_threaded (the loopback run with the
poll on its own thread; its frame-numbered press record equals the inline
run's).  Docs: neuro.md, app/README.md, docs/agent/testing.md.

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): Neuro final-review fixes: one lock-step headless loop, watch staleness (#967)

- App::runHeadless: the threaded mock-mode and --neuro loops are one lock-step
  loop in the inline loop's order, so the 3b rebase meets two loops, not three.
- WatchFeed::dropIfStale: neuroTick drops a previous game's bytes before it
  lends them to the state text, not one tick later.
- WatchFeed cadence: a rejected buffer is not re-read every UI frame; the
  clock resets on release, a key change and a dropped epoch.
- neuroHeadlessReport forces its read (comment now matches the code).
- docs/neuro.md: the 2 MiB copy runs while Neuro is on, about 10 times a second.
- Tests: dropIfStale and rejected-buffer cadence (testWatchFeed), a non-zero
  low byte for the Dram index, absorb on a stale lineage (testAgentProgram);
  the testAgentGating comment moves back above its function.

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 19:38:19 -04:00
Joseph Mattiello 00d205caa4 Merge master (v3.10.0) back into develop
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	.github/workflows/cheat-import.yml
#	Makefile
2026-10-21 23:43:06 -04:00
Joe Mattiello cf036d7432 fix(installer): preserve portable data and qualify published package updates (#1059)
* fix(installer): retain portable data and qualify published packages

* docs: record published Mac and clean Linux qualification evidence

* test(packaging): add clean Intel macOS qualification lane

* fix(ci): use distro audio packages and authenticated qualification metadata

* test(packaging): qualify downloaded Windows setup upgrade and uninstall

* test(packaging): require real upgrades, core loading and immutable candidate evidence

* test(installer): run Linux core qualification with a virtual GL display

* test(installer): resolve release tags for qualification provenance
2026-10-21 19:37:32 -04:00
Joe MattielloandClaude Opus 5.5 8cf078c01e feat(app): TV Settings, Overlays, Achievements and More settings (TV mode phase 3) (#1060)
* docs(app): revise TV mode plan Phase 3 against develop (#991)

Phase 3 was written before Phases 1-2, the emulation thread (#1000,
#1031) and desktop pad navigation (#1030) merged. Re-point every task at
the current code: More settings reuses desktop_nav_popup's ImGui nav
feed, achievement data reaches tv/ as TV value types, in-game menu rows
live in Shell::buildRoot. Adds the Phase 3 approval list (A-G) and
Phase 4 staleness notes.

Refs #991

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV Settings screen with the curated rows (#991)

Settings is the third TV section (Home, Library, Settings): a rail of groups
(Picture, Speed, Sound, Interface) beside the focused group's rows.  Left / Right
step a row, Accept steps it forward and wraps, the last row opens the desktop
Settings window.  Every row is an existing app setting or core option and goes
through the call its desktop control makes, then saveConfig(); a row the build or
session does not have (no shader runtime, no core options before the first game,
a locked option) is absent, and one a session override pins is read-only with
the reason under its value.

The row rules are a pure model (tv_settings_model) unit-tested without App or
ImGui; AppHost only gathers the inputs.  Screens now get Back first (Screen::back)
so Settings' list can step up to its rail without leaving the section.  The shader
preset list and apply are exposed from shaders_ui.cpp, and the aspect table moves
to aspect_choices.h so the model and Settings > Video share it.  Choosing TV mode
Manual <-> Auto keeps TV mode on (configure() restarts it from off); Off turns it off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): TV Settings clicks follow the drawn chevrons (#991)

A click on a row's value stepped by quarters of the row, but the value is
right-aligned: on a short value ("On", "50 %") the left chevron sits past the
three-quarter mark, so clicking it stepped forward (Volume went up).  The row
now records where its value is drawn and a click left of the value's middle
steps back, right of it forward.  A focused row grows past the unscaled card,
so its right chevron was also outside the hover rectangle and unreachable; the
hover test is widened by what the row grows.  The smoke test clicks the real
chevrons of "50 %".

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV Achievements screen and the Achievements and Settings sections (#991)

TV mode had no way to see RetroAchievements without leaving for the desktop UI.
Sections are now Home, Library, Achievements, Settings (LB/RB). The Achievements
screen shows the tracked game's achievements and leaderboard trackers, or with no
game the RA Jaguar list, where A launches a game the library has. Y on Home opens it,
and Game details gains an Achievements row that opens it on that game.

The status line the desktop list builds moved into ra::achStatusLine so both lists
draw the same text. Launching from the list opens the game fresh (no resume offer,
which needs a mouse), as every other TV launch does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): show achievement descriptions in TV mode (#991)

The desktop achievements window shows what an achievement asks of the
player under its title; the TV rows showed only the title and the status
line, so a locked achievement said "Locked  Progress: 5/10" and never what
to do. AchItem carries the description and the row draws it as a muted
line between the title and the status (trackers have none). A row takes up
to two muted lines inside its height, at the 24 floor of the type scale.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV Overlays screen and Achievements, Overlays and Settings in the in-game menu (#991)

The in-game menu is a short list of actions; the spec puts Achievements,
Overlays and Settings in it, between Load state and Controls. Each opens
the same tv::Screen the Base layer uses, in one slot beside the menu's page
model: the panel's list area becomes the screen's, the screen gets every
intent, and Back asks the screen first (Settings steps from its rows up to
its groups) and then clears the slot onto the row it came from. The menu
keeps holding the pause while the slot is set, and a root rebuild waits for
the slot to clear.

Embedded screens draw no rail and no safe margin (the menu's own inset
instead) and their way out reads "Menu". Settings gains a first level for
it: the groups as rows. Achievements in the menu cannot start another game
from the Jaguar list.

OverlaysScreen lists Show FPS, the keypad and controls overlays, and every
debug tool with a toggle, with a tool's reason wrapped under its label when
the debug windows are off. Accept toggles in place; the keypad and controls
overlay are mouse-driven windows over the game, so they close the menu.

The headless script walks Down x5 to Settings, changes Internal resolution
and checks, after the menu is closed, that the core reads 2x (new
core-option expect); that test rules the shader runtime out so the Picture
group has the same rows on every machine.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): More settings from TV mode with ImGui pad navigation (#991)

"More settings..." used to set showSettings and nothing else, so on the Base layer the shell stepped aside but the window had no
pad navigation, and inside the in-game menu (which keeps the input regardless) Accept opened the window while the menu still
took every pad press.

tv_more_settings.{h,cpp} is the App-side glue.  While the window it opened is up (and TV mode is on or the menu is open) it takes
the pads before the shell, feeds our own NavInput to deskFeedPopupIntents, and turns on ImGui gamepad and keyboard navigation (the
keyboard through the new deskSetKeyboardNav, so WantCaptureKeyboard gates F4 and Ctrl/Cmd+Shift+T as a text box does).  B or Esc
closes the window only when no popup is open and no item is active; otherwise B goes on to ImGui as Cancel and Esc is ImGui's, so
a combo or a text box is left first.  The menu stays open underneath, holding the pause, and has the pad back when the window
closes.  Esc is handled in the glue as well as in handleHotkey: keyboard navigation makes WantCaptureKeyboard true, which gates
handleHotkey, and with the menu open the shell would otherwise take the arrows, Enter and Esc too.

drawSettings, while live, is its own ImGui window ("###vjtvmore", NoSavedSettings) centred at 80 % of the viewport with TV-sized
text (tv::bodyFontScale), so imgui.ini and the desktop window's size are never touched.  Shell::handleEvent now clears padDriven_
on a mouse move even when a desktop window has the input, so the pointer is back in the window.  saveConfig's stale comment about
ui.tv_mode is corrected.

Tests: tv_menu_headless (Base layer hand-off, Esc and Ctrl+Comma, and the in-menu hand-off with the menu and its pause intact),
tv_ui_smoke (nav flags, pointer, menu keeps the slot), tv_focus_test (bodyFontScale).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): More settings only live over the menu or the Base layer; pointer return; Speed note (#991)

Review fixes for the TV "More settings..." hand-off:

- tvMoreSettingsLive is now "menu open, or TV mode on with no game running".  With a game running the pads are polled, not read
  from the events the glue takes, so a window driven by the pad would have been played through when a Share hold closed the menu
  under it (or a ROM was dropped on it).  The window then continues as the plain desktop Settings window and the flag clears.
  A script case holds Share with the window open over the menu.
- The pointer's return after pad navigation is seen in drawBase (the mouse moved), not in Shell::handleEvent, which App::handleEvent
  never calls while a desktop window has the input.  The dead branch is gone and the smoke test goes through the draw path.
- The Speed group is there before a game has run, with the read-only line "Available once a game has run" (approved change G),
  on the Base screen and in the menu's groups level; a note row has the full row width for its text.
- The handleHotkey Esc rule is gone: the glue consumes Esc before it can be reached.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): More settings focus, hints, demotion and TV-sized title; tv_mode Off clears the remembered state (#991)

- "Reset all settings..." carries NoNavDefaultFocus, so pad / keyboard navigation in the More settings window no longer starts on it
  (the first A opened the reset modal).
- The Base layer draws no hints while a desktop window has the pads: the screen's A Open / LB RB Sections / B Groups did nothing there.
- When the hand-off ends (the menu closed under the window by a Share hold, or a game dropped on it) with a game running, the window is
  closed too: a pad player could not close a plain desktop window and the pads are the game's again.  Script cases for both.
- The window's font is pushed before Begin so the title bar and its close box are TV-sized, and popped after End.
- Choosing Off in the TV-mode row also clears ui.tv_manual_on, or a later Manual switched TV on at once.
- The Overlays list leaves the room under its rows to the "more" arrow when it scrolls (the half-cut last row ran into it).
- tv_more_settings.h: the "live" comment matches the definition.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): keep More settings pad focus off the Reset button; reserve the Overlays arrow band; no menu hints under the window (#991)

- While the TV More settings window is live the "Reset all settings..." button is NoNav (the mouse still reaches it): NoNavDefaultFocus
  alone left it the only default-focus candidate when the first tab is empty (no game, no core options), so two stray presses opened the
  reset modal.  ImGui's tabs never take default focus, so the selected tab asks for it (SetItemDefaultFocus, live only): the pad starts on
  a tab and D-pad Right goes to the next one.
- The Overlays list reserves the 24u arrow band in the scroll target and the hit tests as well as the clip, so the focused last visible
  row is not cut off.
- The in-game menu draws no hints while a desktop window ("More settings") has the pads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-21 19:36:49 -04:00
Joe MattielloandClaude Opus 5.5 5d0afd4b37 feat(app): Tools windows work with the emulation thread on (#967 phase 3a) (#1061)
* feat(app): a debug lane in the emulation thread's command queue (#967)

EmuCommands gets Lane::Debug beside the frame lane; the loop drains both at
each frame boundary. EmuThread::debugCall / debugPost are safe inside
retro_run() once the halt pump serves the lane (phase 3b).

Refs #967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): DebugViews, per-window requests the emulation thread fills (#967)

A request table with a double buffer per request: the UI sets and reads, the
emulation thread (inline: the same thread) fills after each frame. A changed
request or a content switch invalidates its buffer; no lock is held across a
core call.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): DebugCore owns the debug interface; DebugSession marshals to it (#967)

DebugCore wraps every vj_debug_iface entry point (emulation thread only);
DebugSession routes each action through coreCall/corePost, which run inline
until App wires them to the debug lane. setHaltAvailable gates attach, Break
and breakpoints so a threaded run can keep the windows and refuse the halt.
Inline behaviour is unchanged.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Memory viewer and Palette read published view buffers (#967)

The Memory grid, its selection footer and the Palette ask the view copier for their bytes and draw the
published buffers; a content switch (DebugViews epoch) resets them. SearchJob::step and the CRY table
each take one callCore. Adds the shared counting test fixture (debug_ui_fixture.h) and the
debug_no_direct_iface static guard that the other tool windows reuse.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): resized Memory window adopts its wider buffer; guard catches core() calls (#967)

fetchWindow compared only serial and start address when deciding a buffer was already adopted, so a paused
window that grew (bytes-per-row change at the region top) kept the shorter window. It now compares size too.
CheckNoDirectIface also rejects the session's DebugCore wrappers (s.core().peek( and friends). The smoke test
pins the resize case and asserts the Palette really adopted a changed CLUT byte.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): CPU debugger reads Regs and Disasm view buffers (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): CPU walks drop stale answers; rows re-adopt on every answer (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Object list reads the OpSnapshot request; step-a-frame via advance(1) (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): Object list step bumps the serial after the frame; release posts the clear (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Blitter log and Textures through view buffers and core calls (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): Cheat scanner on the Dram request; Audio scope available with the thread on (#967)

The scan pass is one DebugSession::snapshotDram (callCore: the 2 MiB peek runs on the emulation side); the live column reads a published Dram request and copies the buffer only when a newer fill exists. The raw memoryRegion path is gone. The scope gets a framesSeen test hook and a threaded-audio gate test.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): Cheat scanner shows -- until a value was read; new rows carry the scan's own values (#967)

Per-row known flag: cleared on a new result list, release and an epoch change; edit prefill and Add as cheat refuse a row with no value. doScan derives the rows from the RAM image it just read, not the older published buffer. Tests: search before the first answer, stale held buffer, collapsed window, unreadable core.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): the Tools windows work with the emulation thread on (#967)

The App fills the debug views after each emulated frame (and on the UI thread
inline), wires DebugSession to the debug lane, hands the core's interface
over again and narrows the thread gate to "halt not available": Break, Step,
breakpoints and the GDB stub stay off until phase 3b. --show-window opens a
tool threaded. DebugSession::iface()/setThreadBlocked() are gone.
debug_views_threaded runs the real core on the emulation thread with the
session wired as App wires it.

Final-review fixes folded in:
- "Step a frame" refuses while the debugger is attached in the threaded case too, because advance() has no give-up.
- The scanner's live column waits on its own buffer (DebugViews::serialOf) instead of on any pending view.
- The -emu.csv perf trace gains a trailing fill_ns column that times the debug-view copier.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): correct the guard's getter list and the Step-a-frame comments (#967)

CheckNoDirectIface.cmake named haltSerial() as a legal core() getter. DebugCore has no haltSerial() yet; it arrives in phase 3b. The op_view.h and op_ui.cpp comments now say that Advance also refuses while the session is attached, because emu.advance() has no give-up.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): a replaced core closes the tools like an unload (#967)

Threaded openContent() on a running game publishes null and the new
debug interface inside one blocking emu.call(), so the UI's syncCore()
saw only the final non-null interface: closeAllTools() was skipped and
the cheat scanner kept the old game's candidates, which it could then
compare or write against the new game's RAM. syncCore() now closes the
tools whenever it replaces a core it had (generation moved, had a core),
which is what inline does when it sees null then the new interface.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): every breakpoint control greys out while the halt is unavailable (#967)

With the emulation thread on, only the toolbar's Break button was gated:
the margin click, the context menu's Set breakpoint and "Add at cursor"
still called toggleBreakpoint(), which always refuses without the halt
machine and reported the misleading "table is full / not attached"
error. All three now share one predicate (canAddBp): the menu item and
button grey out with haltDisabledReason() as the tooltip, the margin
hover says why, and toggleBp() itself answers with that reason without
touching the core. Removing a breakpoint stays allowed. The Break
tooltip uses the same reason text.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): Tools-window actions wait for the frame in progress (#967)

README: with the emulation thread on, an action that reaches the core (a Tools-window edit, a load, a state save) waits for the current frame to end, including link play's bounded peer wait. It does not run immediately.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-21 19:36:13 -04:00
Joe MattielloandClaude Opus 5.5 9c5656e271 perf(dsp): build the untimed DSP loop without GDB code when nothing is armed (#1057)
* perf(dsp): build the untimed loop without GDB code when nothing is armed

DSPExecUntimed() is now compiled twice from dsp_exec_untimed.h: a plain
instance with no breakpoint test and no idle-skip gate, and an armed
instance used only while gdbArmedDSP is set.  DSPExec() picks one per
slice, alongside the existing timed/untimed split.

The armed instance keeps the breakpoint test where it was, after the IRQ
dispatch and the PC-escape bail, so a breakpoint on an ISR's first
instruction still fires.  Arming is sampled once per slice, as before.

Preprocessed, the plain loop is develop's loop with the GDB lines
removed and nothing else changed.

Refs #1054

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(debug): record the #1054 DSP loop specialisation and its layout caveat

The parked specialisation now ships, so the design doc's "parked" and
"~2% unexplained" text no longer describes the code.  Records the A/B/B/A
numbers.  It also records that code layout moves this loop by about +/-1%:
compiling the check out makes real games slower, and the port beats the
ceiling on identical C.  The claim stays narrow: yarc gains, games are
neutral.  It also notes that the DSP_HOOK control arm will now read
about 0.

Refs #1054

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(dsp): keep the local-RAM opcode fetch inside dsp_ram_8

The six fetch sites (both exec loops and the four delay-slot fetches)
read dsp_ram_8[off] and [off + 1] whenever dsp_pc was below base +
0x2000.  So an odd PC at base + 0x1FFF read one byte past the array.
cppcheck flagged it (arrayIndexOutOfBoundsCond) once the code moved
into a changed file.  The bound is now base + 0x2000 - 1, the same form
gpu.c already uses.  That one address now goes through DSPReadWord(),
which aligns.  Every other PC is byte-identical.

clang-tidy now skips src/jerry/dsp_exec_untimed.h the way it skips the
*_simd_* fragments: on its own it is only undeclared dsp.c statics.
Its code is still linted through dsp.c via HeaderFilterRegex.

Refs #1054

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-21 02:30:22 -04:00
Joe MattielloandClaude Opus 5.5 2abb032ab5 fix(app): end the late latch's event wait on time on macOS (#1026) (#1058)
* test(app): stall probe for late-latch waits (#1026)

Dev-only, VJ_PERF_TRACE: brackets the late latch's blocking calls and,
for each call that returns over 3 ms late, records the UI thread's CPU
time, the main run loop's activity transitions and sampled scheduler
state and stacks into <prefix>-stall.txt. The UI trace gains
wait_cpu_ns.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): end the late latch's event wait on time on macOS (#1026)

The late latch's SDL_WaitEventTimeout often returned milliseconds past
its deadline, which started the build late and missed the vblank. Two
causes, both measured on the Studio with the stall probe:

- A launch from a terminal (and every ssh-run benchmark) starts the
  main thread at the default QoS, not user-interactive as LaunchServices
  does. Under CPU load the UI thread was runnable but not running.
- SDL's timeout is the Cocoa run loop's own timeout, which Core
  Foundation fires from a dispatch worker at qos_class_main(), the main
  thread's QoS at launch. Raising the thread does not move that worker,
  and under load the UI thread slept in the run loop 3-20 ms past its
  deadline (run state waiting, ~0.03 ms CPU).

The UI thread now runs at user-interactive QoS however it was launched.
Its timed event waits (latch and threaded nap) end on a wake event that
LatchWaker pushes from its own user-interactive thread. SDL's timeout
stays as a 5 ms backstop. A main-run-loop CFRunLoopTimer was tried and
fired ~3 ms late on every wait from a terminal launch, so it is not
used. --latch-wake plain keeps the old wait for A/B runs.

perf_trace_summary.py now also reports UI vblank misses and waits that
end over 3 ms late.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): address #1026 review (waker test bounds, probe stack walk)

- latch_waker_unit: every wake within 10 ms, all but 2 of 20 within
  3 ms, so a loaded shared runner does not flake; it runs the waker on
  every platform (start(true)) so the Linux ThreadSanitizer job covers
  it. The app still starts it on macOS only.
- Stall probe: bound the frame-pointer walk by the sampled SP (only the
  stack above it is mapped) and release the thread port.
- App::shutdown() also stops the waker and the probe before SDL_Quit.
- Comments/docs: re-arm granularity, early return on a stale wake, and
  --latch-wake plain keeps the QoS raise.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): wake on time on hosted runners and in the waker's last ms (#1026)

latch_waker_unit failed on GitHub's hosted macOS VM, where every
1 ms sleep oversleeps 3-8 ms. Its absolute 3 ms bound was testing the
host's timers, not the waker. Each wait is now paired with a reference
SDL_DelayPrecise to the same deadline, on a thread at the waker's QoS;
the waker may be at most 2 ms later at p90 and 10 ms at worst.

That comparison exposed a real 0.5 ms lateness on the Studio. The
waker handed SDL_DelayPrecise under 2 ms, too short for it to learn
the host's oversleep, and a sub-ms macOS sleep oversleeps ~0.5 ms. The
waker now slices until 3-4 ms remain, as the latch's own precise tail
does: 0.002 ms late quiet and under 16 hogs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-21 02:04:14 -04:00
Joe MattielloandClaude Opus 5.5 b63d745a1d docs(app): emulation thread phase 3 plan (#967) (#1056)
* docs(app): emulation thread phase 3 plan and design amendments (#967)

Phase 3 brings back everything still switched off while the emulation
thread runs, then flips the nightly default. The plan splits the work
into four PRs:
- 3a: debug views on the emulation thread (a debug command lane,
  DebugViews requests, DebugCore).
- 3b: the debugger halt and the GDB stub (a bounded halt pump, a
  verdict atomic, call() give-up, a silent gdb host).
- 3c: Neuro on the input snapshot (an AgentPlayer replica that runs one
  step per emulated frame; RAM watches through DebugViews).
- 3d: VJ_BUILD_CHANNEL, so nightlies default the setting On.
A G1 gate runs before 3d and makes "nothing switched off" a test.

The design doc gets dated amendments to sections 5, 6, 8 and 10:
- The pump's wait is bounded, because the core's GDBHalt loop owns the
  socket and the halt timeout.
- haltedFrame stays for inline mode until phase 5.
- While halted, commands are held, refused or detached-first.
- gdb gets a silent host and is locked under hardcore.
- The channel flag.

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): phase 3 plan: headless-only signal handlers; command cp (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): phase 3 plan: fix the negative self-test command (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-20 23:58:38 -04:00
Joseph Mattiello 3c683169ef ci(core): share compile-only release builds and preserve CI artifacts 2026-10-20 18:39:49 -04:00
Joseph Mattiello 2165519e97 ci: allow ref-scoped temporary runners for lightweight checks 2026-10-20 17:34:02 -04:00
Joseph Mattiello f6e2e7b784 docs(release): refresh final-core cartridge and CD boot matrices 2026-10-20 16:55:25 -04:00
Joseph Mattiello b5e4009021 docs(release): correct GT blitter fix in v3.10.0 changelog 2026-10-20 16:29:48 -04:00
Joseph Mattiello 045d2f88cd docs(release): describe community cheat source conversion 2026-10-20 16:24:30 -04:00
Joseph Mattiello 98e1327cf6 feat(cheats): convert community source freezes with revision guards
(cherry picked from commit e6fa96e1d0)
2026-10-20 16:24:29 -04:00
Joe Mattiello abc9054ec2 Merge pull request #1052 from libretro/codex/community-cheat-import
feat(cheats): convert community source freezes with ROM revision guards
2026-10-20 16:22:34 -04:00
Joseph Mattiello 378c15afc9 docs: record DSTA2 GT blitter correction in v3.10.0 notes 2026-10-20 16:22:33 -04:00
Joseph Mattiello 338e2b6527 fix(blitter): follow destination lanes for computed Z with DSTA2
(cherry picked from commit f7249c4d01)
2026-10-20 16:22:16 -04:00
Joseph Mattiello e6fa96e1d0 feat(cheats): convert community source freezes with revision guards 2026-10-20 15:44:09 -04:00
Joseph Mattiello f7249c4d01 fix(blitter): follow destination lanes for computed Z with DSTA2 2026-10-20 14:32:09 -04:00
Joe Mattiello 3cfe437271 fix(risc): persist and wrap the MMULT matrix address counter (#1048)
* fix(risc): persist and wrap the MMULT matrix address counter

* test(risc): keep MMULT harness under the test ABI guard
2026-10-20 13:17:32 -04:00
Joe Mattiello 8287d41676 fix(tom): expose live PIT counters and hardware register readback (#1047)
* fix(tom): expose live PIT counters and hardware register readback

* test(tom): refresh Jagniccc baseline for live PIT readback

* test(tom): keep readback harness under the test ABI guard
2026-10-20 04:14:00 -04:00
Joseph Mattiello 62118c0669 docs: validate combined v3.10.0 emulation fixes 2026-10-20 02:50:30 -04:00
Joseph Mattiello f5db240244 fix(risc): persist and wrap the MMULT matrix address counter
(cherry picked from commit 05d4bd79a3)
2026-10-20 02:20:09 -04:00
Joseph Mattiello 5a2af07e8b fix(tom): expose live PIT counters and hardware register readback
(cherry picked from commit 0d330ad35c)
2026-10-20 02:19:45 -04:00
Joseph Mattiello fcace2d235 fix(app): preserve playlist discs and per-disc state resume 2026-10-20 01:23:51 -04:00
Joseph Mattiello e6094a60bd docs: validate v3.10.0 and regenerate boot matrices 2026-10-20 00:46:19 -04:00
Joseph Mattiello 7e84cf227f chore: prepare v3.10.0 app feature release 2026-10-20 00:20:04 -04:00
Joe MattielloandClaude Opus 5.5 0085c4485d feat(app): emulation thread phase 2 — achievements, netplay and busy 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* docs(app): emulation thread phase 2 -- achievements, netplay, the busy spinner (#967)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
2026-10-19 20:15:25 -04:00
Joe Mattiello 998dee625c docs: make AGENTS.md the shared Codex and Claude guide (#1032) 2026-10-19 17:35:29 -04:00
Joe MattielloandClaude Opus 5.5 b0dc50f871 feat(app): controller navigation of the desktop library, with per-family and custom button profiles (#1021) (#1030)
* docs(app): controller navigation of the desktop library, design (#1021)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(app): controller navigation of the desktop library, plan (#1021)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): NavInput per-pad sources, supplied intents and long-press Accept (#1021)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): controller-family menu-button profiles with a per-model custom layer (#1021)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): a menu-button family change clears the model's overrides; ASCII minus label (#1021)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-19 17:23:35 -04:00
Joe MattielloandClaude Sonnet 5.5 97cb2040d1 fix(app): Neuro follow-ups: release input on drop, bounded stop, hardcore 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 <[email protected]>
2026-10-19 17:16:58 -04:00
biggestsonicfanandClaude Opus 5.5 33446a784c Feature/neuro sdk (#1001)
* 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 <[email protected]>

* docs: Neuro API guide (actions, profile format, testing with Randy)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 20:51:26 -04:00
Joe MattielloandClaude Opus 5.5 0bbe016f90 feat(app): Ctrl/Cmd+Shift+T toggles TV mode; the TV-menu action is labelled 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 <[email protected]>
2026-10-17 20:48:27 -04:00
Joe MattielloandClaude Opus 5.5 7d72ded8c9 docs(app): emulation thread phase 2 plan (#967) (#1022)
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 <[email protected]>
2026-10-17 19:58:03 -04:00
Joe MattielloandClaude Opus 5.5 d97d25a311 fix(app): follow a disc swap in the content identity; keep the .srm on 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 <[email protected]>
(cherry picked from commit b96ce11c4a7499fddf001038beab660b05fa1664)

* docs(app): the content identity a disc swap moves is read from the snapshot

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit c7e7dfc772750dffd25dfe9221c92faa6a980651)

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 19:09:04 -04:00
Joe MattielloandClaude Opus 5.5 cd39de7eef fix(app): register the loaded CD with disk control so the disc menu works (#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 <[email protected]>
(cherry picked from commit 0033152e1f9835b148e41dacffce78202a64dc92)

* 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 <[email protected]>
(cherry picked from commit 98599c6c905e945c5bd2c4421053526e531f3f96)

* 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 <[email protected]>
(cherry picked from commit 08b340f41dc0f30a8a74bc2ea7ed9107f2acdf3b)

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 19:08:26 -04:00
Joe MattielloandClaude Opus 5.5 1b18d49117 fix(app): emulation thread presents as evenly as the single-thread loop (#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 <[email protected]>
(cherry picked from commit 462c14627faf7940f222f87f1947070e590041b0)

* 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 <[email protected]>
(cherry picked from commit bdc878609859e4f719d1377cb0d4afb882c47343)

* 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 <[email protected]>
(cherry picked from commit a01504788087a0d3160242f0ee13a89699517b94)

* docs(app): mode A phase steering in the emulation-thread design (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 133ee01a2f4b51f85f83473a060455808465289c)

* 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 <[email protected]>
(cherry picked from commit 90643d1e44af0b4de36c95576a28618f3c23849d)

* 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 <[email protected]>
(cherry picked from commit 395e255eeb49c69171ccf03a64ebc9bae71b8ddf)

* docs(app): VJ_PERF_TRACE summary script, --pace-steer in the README (#967)

Refs #967

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 1cbe57b59b135a0a280d14ca4545742e24897c6f)

* 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 <[email protected]>
(cherry picked from commit 9adae8e4d64c897a6834f4d6e98f4eed6f048a94)

* 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 <[email protected]>
(cherry picked from commit 469cf23cbabc1e63b5cce10b52d06d1608dd7873)

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 19:08:08 -04:00
Joe MattielloandClaude Opus 5.5 df0f6d271b feat(app): emulation on its own thread, phase 1 (#967) (#1000)
* 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 <[email protected]>

* feat(app): EmuCommands, the emulation thread's ordered command queue (#967)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* test(app): windowed emulation-thread check for the Mac Studio (#967)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* docs(app): the Emulation thread setting (#967)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 19:01:55 -04:00
Joe MattielloandClaude Opus 5.5 e49d5649a9 feat(app): TV shell with Home, Library and Game details (TV mode phase 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 <[email protected]>

* feat(app): TV mode state machine and input ownership (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): TV mode fullscreen hold is state, leaving fullscreen turns TV off (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV shell, Manual TV mode, --tv and the View menu entry (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): TV shell leaves bind capture and text fields their input; clickable section tabs (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV Home screen with shelves and hero panel (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV Library screen with rail, cover grid and letter jump (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(app): TV pad trigger glyphs, held Backspace stops at an empty filter (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV Game details and the TV-mode toggle in the in-game menu (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(app): TV script verbs for windowed screenshots (wait library, shot, quit) (#991)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 16:56:24 -04:00