Commit Graph
2460 Commits
Author SHA1 Message Date
dependabot[bot] 2e676d167a ci: Bump actions/setup-python from 5 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-26 11:11:43 +00:00
Joseph Mattiello d604c4ade6 add codex xcrun cmd
Signed-off-by: Joseph Mattiello <[email protected]>
2026-10-26 02:04:28 -04:00
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 Mattiello ee4952328d feat(cheats): convert read-only integer lookup tables (#1097) 2026-10-26 00:53:41 -04:00
Joe MattielloandClaude Opus 5.5 1fb655012e fix(tom): derive HC readback from emulated time so AvP marines attack (#1094)
HC ($F00004) free-runs 0..HP once per half-line on hardware (JTRM), so
its low bits at a CPU read are effectively random.  Alien vs Predator
uses them as its marines' fire roll -- `move.w $F00004,d0 / and #7 /
cmp #2` at 68K $015ADA -- while its Predators roll the game's software
RNG.  The core returned a per-read counter reset to 0 / (HP+1)/2 every
half-line, so a once-per-AI-tick read always saw $0000 or $05A6: low
bits 0 or 6, never 2.  Marines walked up to an Alien player and never
fired (30 of 30 rolls measured from the corridor savestate).

The readback is now the elapsed fraction of the current half-line:
time to the next half-line event at slice start, less how far the 68K
has run into the slice (JaguarSliceElapsedUs, stall and clock scale
included), scaled to HP+1 ticks.  Byte reads of $F00004/5 use it too.
tomRam8[HC] is unchanged (the OP's second-half-line test reads bit 10),
and the read-phase counter is gone; nothing new is serialized.

Measured after: the same marine rolls all eight HC&7 values (2 on 12
of 111), crouches, fires, and the Alien's health drops.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-25 23:08:14 -04:00
Joe Mattiello c1653da97b feat(cheats): expand bounded integer settings into freeze choices (#1091)
* feat(cheats): expand bounded integer settings into freeze choices

* fix(cheats): retain cartridge identity in integer analysis failures
2026-10-25 22:23:21 -04:00
Joe MattielloandClaude Opus 5.5 98212bf95d fix(app): don't block in the GL swap when the display was asleep at launch (#1081) (#1089)
On macOS SDL emulates vsync with a CVDisplayLink: SDL_GL_SwapWindow waits on a
condition variable that the link's callback signals each refresh. The link is
made from the active displays when the GL context is created. With the display
asleep there are none, the link never ticks (not even after the display wakes)
and the first swap blocks forever, the emulation with it when it runs inline.
A display that falls asleep after launch does not block: the link survives.

SwapGuard (pure) decides whether the swap interval may be 1: never for a run
whose context was made with the display asleep, and (defensively) not while it
sleeps later. App::guardSwap() asks the platform layer (CGDisplayIsAsleep /
an empty CGGetActiveDisplayList) every 250 ms and before the swap's timestamps,
and every UI swap goes through it. Interval 0 is the existing vsync-off path,
so the emulation keeps its own clock while nothing can be seen.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-24 07:21:33 -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
Joe MattielloandClaude Opus 5.5 287bb30190 site, installer: fetch the install scripts from master, not develop (#1087)
The site's one-liners pointed at develop, so any installer change went live
to users the moment it merged, before a release. Point every installer URL at
master: the site's RAW_URL and source links, the README, the scripts' own usage
text, the nightly notes, and install.sh's RAW, which --auto-update re-fetches.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 22:25:40 -04:00
Joe Mattiello 4fdca5e8a4 Merge pull request #1086 from libretro/chore/backmerge-master-1083
Merge master into develop after the v3.10.0 installer hotfix (#1083)
2026-10-23 21:13:34 -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 aa5b73bd2b ci: use RUNNER_TEMP for scratch files the self-hosted Studio runner would keep (#1084)
The Studio runner keeps /tmp between jobs, so regression-macos-arm64's
miniretro clone failed on the next run ('destination path /tmp/miniretro-src
already exists', exit 128; #1082). Clone into a fresh RUNNER_TEMP dir, and
move the EEPROM lifecycle test's scratch files there too.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 17:23:45 -04:00
Joe Mattiello 4c9e0fdd42 Merge pull request #1083 from libretro/hotfix/v3.10.0-installer
fix(installer): backport qualified portable data preservation to v3.10.0
2026-10-23 16:29:42 -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 f460ab44e1 test(installer): invoke Windows RetroArch directly for qualification (#1065) 2026-10-23 15:39:53 -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 617fe83e15 test(app): fonts_system_unit reports a slow host instead of failing (#1078) (#1080)
* test(app): fonts_system_unit reports a slow host instead of failing (#1078)

The real-machine check waited a fixed 30 s for the full system font scan and failed
`list != nullptr` when it was not done. GitHub's hosted Windows runners take 32-53 s
for that scan (about 2 s elsewhere), so it failed unrelated PRs (#1068, #1074).

Every correctness assertion (names, weight/italic/fixed pitch, collections, dedupe,
sort, directory selection) already runs on synthesised fonts, so the machine scan is
only a smoke check of the worker-thread plumbing. If it has not finished within the
bound it now prints "slow host" and passes; FONTS_STRICT=1 keeps the hard check with
a 120 s bound. No production change: the scan runs on a worker thread, has no time
cap, and the font picker draws whatever it has meanwhile.

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

* test(app): fonts_system_unit's CTest timeout covers the strict 120 s scan (#1078)

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 06:13:22 -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
Joe MattielloandClaude Opus 5.5 10642e5ade fix(app): TV Settings follow-ups from the Phase 5 final review (#1077)
- The Shader row now steps on a press only. A held Right/Left repeats every
  80 ms, and each step parses and compiles a preset and saves the config
  (a broken preset toasts for 6 s per step); the repeats are ignored for
  that row, with a smoke test that a held Right applies exactly one step.
- The library panel's "BIOS & boot..." button goes through
  App::openBiosSettings() like the menu item, so it follows TV mode.
- F8 behind hardcore's run-on menu toasts "The menu has the controller"
  (nothing is paused there); a menu-held pause keeps "Close the menu to
  resume".
- A slider's "(custom)" label keeps the unit the grid labels use
  ("55.5 % (custom)", not "55.5 (custom)").
- App::testDenyPause is private; only the script runner (a member) sets it.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 00:42: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
Joe MattielloandClaude Opus 5.5 80be53f43e fix(app): CI: wait-hook test waits for its hooks; a failed database download is a warning (#967) (#1070)
emu_thread_unit's "a debugCall behind a slow frame runs the wait hook like call()" held the frame for a fixed 450 ms and expected three hooks (kBusyAfter, then ~every 16 ms). GitHub's hosted macOS VM oversleeps enough that only two fit, which failed develop's App macOS arm64 and Packages / macOS universal jobs. The frame is now held until the hook has run three times, with a 5 s bound, so the coverage is kept.

The ThreadSanitizer job's configure failed because file(DOWNLOAD ... EXPECTED_HASH) turns a failed download of 'Atari - Jaguar.dat' (an HTTP error from raw.githubusercontent.com) into "cannot compute hash on failed download", a configure error, instead of the warning the code already prints. The download now runs without EXPECTED_HASH and the SHA-256 is checked by hand: a failed download or a mismatch removes the file and warns, as intended.

Refs #967

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 19:38:02 -04:00
Joe MattielloandClaude Opus 5.5 163f3aff17 fix(info): savestate_features = "deterministic" (#1064)
RetroArch reads savestate_features as a name ("basic", "serialized",
"deterministic"; core_info.c). "3" matched none of them and only got the
deterministic level because it is RetroArch's default. libretro-super
already ships "deterministic" for this core.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 02:28:09 -04:00
Joe Mattiello b66a6282ee test(installer): invoke Windows RetroArch directly for qualification (#1065) 2026-10-22 02:26:54 -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 MattielloandClaude Opus 5.5 3e5afec7ba test(app): latch_waker_unit reports lateness where the host cannot pace (#1062) (#1063)
On GitHub's hosted macOS VM the reference SDL_DelayPrecise is itself 8.3 ms late at p90. The waker's 12 ms then failed the "at most 2 ms later than the reference" check. That broke the develop nightly's macOS universal package: the native arm64 slice's smoke tests fail. The x86_64 (Rosetta) slice runs only the shader tests.

The test now judges lateness only when the reference's own p90 is at most 5 ms. Above that, the host's timers are too coarse for a 2 ms comparison to mean anything, so it prints a "note:" with the measurements instead, as emu_thread_unit does for coarse MinGW timers. Every wait must still deliver its wake event and never wake early. The disarm, re-arm, event-first and stop checks are unchanged.

Closes #1062

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-21 23:41:20 -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
Joe Mattiello e7db53678b Merge pull request #1042 from libretro/release/v3.10.0
release: v3.10.0 — desktop features and emulation fixes
v3.10.0
2026-10-20 21:25:22 -04:00
Joseph Mattiello 304867bd48 fix(ci): scope checkout ownership trust for core artifact staging 2026-10-20 19:01:25 -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 2e46ca4492 ci(app): share exact-head release packages with matching PRs 2026-10-20 18:05:38 -04:00
Joseph Mattiello b8164d81b9 ci: share source validation runner and ensure Ruby manifest checks 2026-10-20 17:55:51 -04:00
Joseph Mattiello 444ee32ead fix(app): reject truncated Neuro profile addresses and CRCs 2026-10-20 17:55:51 -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 8ab7448d78 test(cheats): exercise pinned community sources in CI
(cherry picked from commit 939f41b62f)
2026-10-20 16:24:29 -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