Commit Graph
78 Commits
Author SHA1 Message Date
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 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
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
Joe MattielloandClaude Opus 5.5 744e9a3860 feat(site): serve the Jaguar CD art as one zip and list the CD database (#1013) (#1016)
build_site.py now writes "thumbnails/Atari - Jaguar CD.zip" next to the
mirror. It holds only Named_*/*.png under an "Atari - Jaguar CD/" folder,
so it unzips straight into RetroArch's thumbnails directory and can be
offered to libretro-thumbnails#141 as-is. Entries are sorted with a fixed
timestamp, so the same images always give the same bytes.

The core info now lists "Atari - Jaguar CD" alongside "Atari - Jaguar",
so once libretro-super builds that rdb, RetroArch offers this core for
discs a scan matches. The mirror README records what still has to land
upstream: the thumbnail repo, the rdb build line, and a RetroArch
scanner fix (every Jaguar CD track is AUDIO, and the scanner only hashes
data tracks).

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 17:54:03 -04:00
Joe MattielloandClaude Opus 5.5 5aafd86cb8 fix(site): honest cart-matrix labels for boot-ROM rejects, crashes and RAM programs (#988)
The compatibility page printed "Not yet verified: BIOS_REJECT" for the
real boot ROM's red reject screen, "Not yet verified: ? (trap_loop)" for
titles whose 68K crashed at start (#975), and "Boots and runs" in the
real-BIOS column for RAM-loaded programs that never touch the boot ROM.

classify_cart_stage now maps:
- BIOS_REJECT -> plain "Rejected by the boot ROM" (authentic, #852/#744)
- ? (trap_loop) -> bad "Known issue: crashes at start"
- ? (bios_trap) -> bad "Known issue: crashes after the boot ROM"
- GAME_CODE in the BIOS column with no handoff in its notes
  (cart_classify.sh bios_ran=0) -> "Boot ROM not used; runs"
- GAME_CODE + black video -> "Runs; no picture in the test"

The legend now explains these labels instead of quoting a stale count
("14 rows ... v3.8.0").

On the current matrix this is 202 "Boots and runs", 81 "Boot ROM not
used" (all homebrew/BJL/Alpine images), 14 "crashes at start", 9
"Rejected by the boot ROM" and 4 "no picture". Splitting rejects into
bad dump / unverifiable / unsigned, and merging .jag/.rom twins, needs
new probe fields (crc32, header byte 0, filedb flags) and stays on #976.

Refs #976

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 22:45:04 -04:00
Joe MattielloandClaude Opus 5.5 3d9401f035 feat(site): per-page share cards and a favicon set from the app icon (#985)
* feat(site): per-page share cards and a favicon set from the app icon

Every page shared one og:image, the Cybermorph true-color comparison,
which reads as a small low-contrast strip in a link preview; the site had
no favicon at all.

- scripts/make_share_cards.py (Pillow + rsvg-convert) composes two
  1200x630 cards: home.png, eight game captures from site/assets/home/
  over the icon and wordmark, and app.png, zoomed crops of the desktop app
  captures (library, Rayman under the Lottes CRT shader, palette debugger,
  light theme). It refuses to write a card that is not exactly 1200x630 or
  is 1 MB or more. It also renders favicon.svg/.ico, icon-192/512.png and
  an opaque full-bleed apple-touch-icon from
  app/packaging/icons/virtualjaguar.svg. Outputs are committed; the
  stdlib-only site build only copies them.
- build_site.py: SHARE_DEFAULT plus a per-page SHARE_IMAGES map replace
  the global SHARE_IMAGE; each page gets its own og:image, twitter:image,
  alt, type, width and height (read from the PNG), and the home JSON-LD
  image follows. Every page head links the favicon set; favicon.ico is
  also copied to the output root.
- check_site.py: twitter:image must equal og:image, the width/height tags
  must match the PNG, the card must be under 1 MB, and every page must
  carry rel=icon and rel=apple-touch-icon links.

Closes #984

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

* fix(site): opaque touch icon without the outline ring; list the new checks in check_site's docstring

Refs #984

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 18:41:09 -04:00
Joseph MattielloandClaude Opus 5.5 edeaa81051 docs(site): app page download picker; lightbox and red hover on every screenshot
The app page's Get it window is now the Download page's widget, limited to
the app: Nightly / Latest / older releases that ship it, platform and arch
icons, file sizes, SHA256SUMS link, and the one-line installer preset to
--what app (download.js composes --what and --channel, and finds its root by
the .dl class so it runs on both pages). The hard-coded 3.9.0 file table is
gone; the per-package notes move to an About the packages window.

Captured frames (figure.shot) on the enhancements and app pages open in the
gallery lightbox and take the same red hover outline as the app gallery
cards. gallery.js reads a shot's window title and figcaption for the
lightbox caption.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 01:41:56 -04:00
Joe MattielloandClaude Opus 5.5 a0e79a5d9e docs(site): bring the site up to v3.9.0 and generate the release log (#964)
* docs(site): generate the release log from docs/RELEASE_NOTES_v*.md

The home-page release log was hand-written and stopped at v3.7.0. It is now
the {{RELEASE_LOG}} placeholder: build_site.py reads every v3+ notes file
(summary = the file's opening tagline paragraph), shows the newest five rows
and folds the rest, and pages.yml rebuilds when a notes file changes. New
releases appear with no page edit. Also refreshes the home page for v3.9.0
(app feature list, savestate v17, RetroAchievements, debug tools, GameDrive
SD card) and drops the stale 'until the first release with the app exists'
installer sentence.

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

* docs(site): bring compatibility, enhancements, performance and why-this-core up to v3.9.0

Compatibility: current as of v3.9.0; drop known problems that v3.7.1 and
v3.8.0 fixed or closed (Club Drive, #800 carts, Music Demo BIOS, Hover Strike
real-BIOS run-ahead, #737, #633); add the v3.7.1 and v3.8.0 fixes (older
ones folded); add JaGol, Ruiner Pinball and Doom link-play notes; replace the
'matrices predate v3.7.0' caveats with the current ones (14 BIOS_REJECT rows).
Enhancements: compatibility-class presets and load notice (v3.8.0), clock
percentages and idle-skip at any RISC scale (v3.9.0), DSP runaways in the
watchdog. Performance: GPU/DSP Pipeline Timing name. Why this core: the
desktop app, app RetroAchievements modes, GameDrive SD card.

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

* docs(site): hide release-log rows newer than the latest release; trim home page duplication

Release notes land before the tag exists, so with release data in hand the
generated log drops versions above releases/latest (offline builds show all).
The home page keeps the app feature list in one place and shortens the
installer paragraph, so it is no taller than before despite three new rows.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 00:19:20 -04:00
Joe MattielloandClaude Opus 5.5 8ea18fde7b docs(site): redesign the Download page around a release picker (#958)
* docs(site): redesign the Download page around a release picker

One Downloads window instead of five sections. A Nightly / Latest / Older
releases picker (buttons plus a select, progressive enhancement) switches
between per-release panels rendered by build_site.py; without JS the latest
release shows with links to the GitHub release list. App and core builds are
grouped by platform then arch with generic inline-SVG glyphs; the core's long
tail is collapsed per platform. The installer one-liners become a compact tabbed
widget that adds --channel for the chosen release, and tools get a proper table.

fetch_releases also fetches the last 10 tagged releases; --releases accepts an
optional older[] list. Adds scripts/check_downloads.py, an offline fixture under
scripts/testdata and a CI step that builds from it.

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

* docs(site): end the releases.json fixture with a newline (EditorConfig)

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 00:04:42 -04:00
Joe MattielloandClaude Fable 5.1 58c6aa835a site(download): label the Flatpak bundle and the Windows arm64 packages (#947)
v3.9.0 added VirtualJaguar-X.Y.Z-linux-x86_64.flatpak and Windows arm64
installer/zip; the Download page would have listed them under "Other" by
file name. The asset filter admits .flatpak and the three get rows like the
rest.

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-14 03:11:59 -04:00
Joseph Mattiello 05c0d7f422 Merge master (v3.9.0) back into develop
# Conflicts:
#	README.md
#	site/pages/app.html
2026-10-14 01:44:05 -04:00
Joe MattielloandClaude Fable 5.1 7ceacba7e2 site: Download page with direct links to every release file (#930)
* site: Download page with direct links to every release file

A new Download page, second in the nav, lists the desktop app packages,
the RetroArch core for every platform, the tools and checksums, and the
nightly, each as a direct link to the asset. The home page's Download button
and the app page's "Get it" point at it; the home page no longer hardcodes
"latest: v3.7.0".

build_site.py takes the release data as --releases FILE or fetches it with
--releases-from-github (stdlib urllib, GH_TOKEN/GITHUB_TOKEN sent when set),
and dies on a failed fetch so a publisher keeps its previous deployment.
With neither flag the page links the release pages instead, so local builds
and check_site.py keep working offline. pages.yml uses the fetch flag and
gains a 6-hourly schedule, since a new release changes the app package names.

The v3.8.0 release carries no app packages, so until the next tag the app
section links the nightly's, with a note saying so.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* site: signing text matches CI: macOS notarized, Windows installer not signed yet

The Download page and the app page say the macOS app is Developer ID-signed
and notarized (the release workflow's notarize step now runs and passes),
with no right-click > Open caveat. The README and app page from #929 said
the Windows installer is signed as well; the SignPath steps are still
skipped in every release run, so both now say it is not code-signed yet and
SmartScreen may warn. The "SIGNING: confirm" markers are resolved and gone.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-14 00:59:54 -04:00
Joseph MattielloandClaude Sonnet 5.5 a3c50fecd7 docs(site): add privacy policy and terms of service pages
Plain-language Privacy Policy and Terms of Service for the Virtual Jaguar
app and core, with footer links. The pages are built and in the sitemap
but stay out of the primary nav (LEGAL_PAGES in build_site.py).

Refs #833

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 21:41:06 -04:00
Joseph MattielloandClaude Sonnet 5.5 69bc186110 feat(site): host the Jaguar CD thumbnail set under /thumbnails/
Mirrors the libretro-thumbnails URL layout (<System>/<Type>/<Name>.png) so the
desktop app can use the same URL builder with a different base. libretro-thumbnails
has no Jaguar CD repository yet (libretro-thumbnails#141). build_site.py copies
site/thumbnails verbatim.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 17:28:23 -04:00
Joseph MattielloandClaude Sonnet 5.5 c1a712cd5c docs(site): add the app page with screenshots; README app section and install options
The desktop app ships in v3.8.1.  README: "Two ways to play", install
bullets for the app and the install.sh / install.ps1 one-liners
(--what core|app|both, nightly channel), and a "The desktop app" section
with screenshots, features and the package table.  app/README.md gains a
Netplay section.  Site: new App page (Get it, library, shaders, controllers,
cheats / achievements / states, netplay), nav entry, home-page hero button
and "Two ways to play".  Ten screenshots in site/assets/ (1280x860 PNG8,
1.1 MB total) captured from the app on real carts; the netplay shot has the
host's address blanked.

Refs #833

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 11:48:39 -04:00
Joseph MattielloandClaude Opus 5.5 dc34531117 feat(jaggd): GameDrive SD-card file API against a host folder (#783)
The GameDrive's file/dir functions were stubs returning -1, so software
that keeps its data on the SD card (JagOS 'Spot', streamed assets,
screenshots) could not run. Blob functions 9-16, 20-23 and 26 now work
against <save dir>/jaggd-sd (system dir fallback), per the new core
option virtualjaguar_jgd_sd (Read/write default, Read-only, Disabled).

- Call path: each of those blob slots is one backdoor write, $C000 |
  function. The core services the call during that write: it reads the
  68K's d0/d1/a0/a1 and leaves the result in d0 via m68k_set_reg. Safe
  because the 68K's JERRY writes are synchronous and the stub's
  move.w #imm,abs.l never touches d0; only a 68K writer triggers it.
  Paths and buffers move through JaguarRead/WriteByte, bounds-checked.
- src/core/jaggd_fs.c: FatFs semantics, matching RetroHQ's published
  gdbios.h (facts only, none of their files vendored): FA_* open
  dispositions, write/create needs a writable card, short reads succeed,
  FILINFO records big-endian (22/278 bytes). End of directory returns -1
  with a zeroed record. Sandbox: .., drive letters, FAT-invalid chars
  and symlinks refused; components matched case-insensitively.
- Savestate v17 (first bump of the v3.8.0 cycle): trailing "JGF1" chunk
  with card-relative path, access and position per handle. Restore
  reopens with the original access only (never create/truncate/append),
  re-seeks a handle already open on the same file, closes deleted ones.
- Blob grows 208 -> 338 bytes (limit 510).

Tests: test_jgd's probe now drives the file API through the real blob
(size 7 and the read buffer are values only the host can produce;
sandbox escape returns -1); new host test test_jaggd_fs covers every
disposition, read-only cards, sandbox refusals, FILINFO layout,
directories, the handle limit and savestate restore.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 00:39:47 -04:00
Joseph MattielloandClaude Opus 5.5 ef04767b03 ci: keep GitHub's error text when the link mutation fails
The exit-3 path redirected the mutation's stderr to /dev/null, so the
job summary said 'see above' with nothing above it -- the PR #725 case
("exceeds manual reference limit") would have lost its only clue.

Refs #730

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 00:39:53 -04:00
Joseph MattielloandClaude Haiku 4.5 5471000d3d ci: make pr-link-issue.sh and release guard use explicit signals
Task #730: scripts/pr-link-issue.sh now signals distinct exit codes for its two
failure modes, allowing the pr-issue-link workflow to classify failures properly
without pattern-matching GitHub's error prose:

  - exit code 2: no link tag found in the PR body (author action needed)
  - exit code 3: tag found but GitHub API mutation failed (API error)

Updated .github/workflows/pr-issue-link.yml to match on exit codes instead of
error message text. This decouples workflow logic from GitHub's wording, making
it robust to future API message changes. Human-readable error messages are still
displayed to users; they are now inputs to the message selection rather than
the selection criterion.

Task #2: Fixed the build target count in the release.yml comments and guard job.
The guard job and release notes incorrectly stated "16 targets" when the matrix
actually builds 24 targets (22 build configs + vita-build + switch-build). Updated
all references to match the real count, with a comment noting the calculation.

- pr-link-issue.sh: exit codes (2 for no tag, 3 for API failure)
- pr-issue-link.yml: check exit codes instead of pattern-matching errors
- release.yml: 16 -> 24 targets (guard message, comments, release body text)
- All YAML syntax validated; script syntax validated with bash -n

Closes #730

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
2026-10-07 00:38:10 -04:00
Joseph MattielloandClaude Sonnet 5.5 ad371077da docs(site): audit pages for v3.7.0, add known-problems list, new hero
Compatibility leads with current known problems and recent fixes; the
generated boot matrices are collapsed and caveated. Remove stale claims
(16 platforms, savestate v12, Team Tap/Pro Controller unsupported, idle-skip
off by default, no frameskip, tier-3 textures unimplemented, Iron Soldier
Fast-blitter wireframe). Tighten prose on every page. Replace the 326x240
Cybermorph hero with a 2x2 mosaic of real core frames (2x resolution, true
colour), 127 KB WebP.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-06 02:39:38 -04:00
Joseph MattielloandClaude Opus 5.5 e671501f85 ci(release): hook + release guide cover the fallback header; checker nits
Review on #795:
- Copilot: the pre-commit hook ran check-info-version.sh only for
  dist/info/ or Makefile changes; version_fallback.h now triggers it too.
- Copilot: docs/release-process.md still said to bump only the Makefile
  and check only .info; both places now name all three copies.
- Kimi: missing-file check for all three inputs, set -eu, and the version
  parse stops at the first closing quote (a trailing comment on the
  #define no longer leaks into the value -- tested with a mismatch).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 09:22:23 -04:00
Joseph MattielloandClaude Opus 5.5 b563b2c38b ci(release): check-info-version.sh checks all three version copies
The version lives in the Makefile, dist/info and src/core/version_fallback.h.
The script compared only the first two; v3.6.1's stale fallback header was
caught by a different CI job ('duplicated lists in sync') instead. Now it
fails naming the fallback file when that one drifts. Verified: passes on
develop (all v3.6.1), fails with exit 1 when the fallback says v3.6.0.
docs/agent/github.md's release step lists all three.

Fixes #734

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 05:59:36 -04:00
Joseph MattielloandClaude Fable 5.1 cd74a2f6d8 site: link the Provenance family in the footer and declare the publisher
Adds a "More from Provenance" footer line (Provenance, iCube, iFly,
wiki) and a publisher Organization reference in the SoftwareApplication
JSON-LD, pointing at the Organization node published on
provenance-emu.com. jaguar.provenance-emu.com is hosted by Provenance,
so the sibling sites now link both ways.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-07 23:23:35 -04:00
Joe MattielloandClaude Opus 5 a40b202abd ci(build): guard the MSVC job's source list against Makefile.common drift (#679) (#721)
* ci(build): guard the MSVC job's source list against Makefile.common drift (#679)

scripts/check-package-sources.sh already does this for Package.swift.
Nothing did it for the msvc-check job, whose list lives inline in the
workflow, is maintained by hand, and is validated only by pushing and
waiting for Windows. That gap cost real time three times in one day
(#624, c89-lint.sh's missing -Isrc/debug, and #662 -- where a brand-new
src/debug/ directory built everywhere except MSVC x64/x86 while ~35 other
checks passed).

New scripts/check-msvc-sources.sh, wired into `make lint` (so drift fails
before a push) and into the "duplicated lists in sync" CI job next to its
sibling. Parses only the FIRST cl.exe step; the second compiles
blitter.c + blitter_simd_sse2.c alone to prove the SSE2 shape and is
narrow on purpose, so flagging it would make the check cry wolf.

Two lists, deliberately kept apart:

- EXCLUDED -- design, each with a reason. Only the two arch-gated
  blitter_simd variants qualify.
- KNOWN GAP -- debt, not design: 30 sources the job has never compiled
  despite its step being named "Compile all sources". Folding these into
  "excluded" would launder them, which is exactly what #679 warns
  against; the rule is that this list may shrink, never grow. A NEWLY
  added source lands in neither list and fails the check, which is the
  #662 case and the whole point.

Closing that 30-file gap means adding them to a Windows-only job that
cannot be tested from here, days before a release. Doing it in the same
change would make a red MSVC job ambiguous -- guard first, then the gap.

Acceptance from the issue, all three verified by actually breaking things:
- Removing src\debug\gdbstub.c from the job: FAILS, naming the file.
- Adding a source to Makefile.common only: FAILS, naming it (exit 1).
- The SSE2-only second cl.exe step: not flagged.

One bug found in this script by its own review: the include-dir
comparison was VACUOUS on first write. Makefile.common spells them
-I$(CORE_DIR)/src/..., the regex matched a bare path, mk_inc came out
empty, and comm against an empty list passes on nothing -- the same class
of failure this file exists to prevent. mk_inc is now in the emptiness
guard, and the parse is scoped to the unconditional `INCFLAGS :=` block
because the later `+=` is msvc2003-gated and must NOT be demanded of the
modern job. 14 include dirs are now really compared.

Verified: make lint clean, make TEST_EXPORTS=1 test exit 0, zero FAILs.

Refs #679

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

* ci(build): guard both cl.exe step markers, not just the first (#679)

From the Kimi review on PR #721, and it is right.

`sed -n '/start/,/end/p'` whose END pattern never matches runs silently to
EOF. So renaming only the second step's name would have folded that step
into the parse and quietly broken the "ONLY the first cl.exe step"
invariant the check depends on -- with no error, because the `[ -s ]`
guard only catches the START pattern failing. The error message already
said "the step names this parser keys on", plural, while checking one.

Benign today purely by luck: blitter_simd_sse2.c is in Makefile.common
(so it never trips the stale branch) and excluded (so it never trips the
missing branch). Luck is not an invariant.

Now symmetric: both markers must be present, and the extracted range must
TERMINATE on the end marker rather than run off the end of the file.

Verified by breaking each one separately:
- rename the END marker only  -> exit 1, names the missing marker
- rename the START marker only -> exit 1, names the missing marker
- clean tree                   -> exit 0

Shell-only change; `make lint` clean (all three guards) and the three
issue acceptance criteria re-verified.

Refs #679

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

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-31 20:31:32 -04:00
Joe Mattiello b43fb0dc55 perf(voicechat): SSE2 saturating mix for VoiceChatMixInto (#695)
Port the NEON fast path from #666 to x86 SSE2. New
src/jerry/voicechat_simd_sse2.h mirrors voicechat_simd_neon.h:
_mm_adds_epi16 is the exact vqaddq_s16 counterpart (per-lane s16
saturating add), applied to 4 interleaved stereo pairs per step via
unaligned movdqu load/store. Dispatch in voicechat.c is an #elif on
BLITTER_SIMD_HAVE_SSE2 after the NEON branch, with the identical
mixed-fits-in-int16 gate (the SIMD sat-add only matches the scalar
(int)add + clamp when both addends are int16).

c89-lint.sh: skip the new header in the default pass and add
voicechat.c to the per-arch pass, which is the only host-side syntax
coverage the non-native fragment header gets. The clang-tidy
exclusion regex from #687 already covers src/jerry/*_simd_sse2.h.
2026-08-27 05:08:09 -04:00
Joe Mattiello 8d2828ce27 perf(shadowfb): NEON-vectorize hi-res Stage-1 store and line resolve (#692)
ShadowHiresStoreCry's box-replication fill and ShadowHiresLineFromRAM's
2x2 block scatter ran as runtime-N scalar loops with per-subpixel
branching (blk / rdst / rblk re-tested every iteration); on Missile
Command 3D at 2x the pair was ~8.5% of profile samples.  New
include-only header src/tom/shadowfb_simd_neon.h (BLITTER_SIMD_HAVE_NEON
capability guard, ARMv7-portable intrinsics only -- struct-returning
vzip_u16, no A64-only forms) replaces them at N=2 with straight-line
16-byte stores; the {value16, frac16} pattern is built as uint16 lanes
so byte layout is endian-correct by construction.  Scalar loops stay as
the fallback for non-NEON builds and any future N != 2.

Byte-identical: frame_hash_ab CSVs vs develop are identical for 600
frames of yarc.j64 and Missile Command 3D at 2x; hires_box_check passes
on yarc (MC3D presents 652x480 in the headless harness on develop too,
so its box gate is inapplicable there -- pre-existing, unchanged).
2026-08-27 04:42:08 -04:00
Joe Mattiello efc4c7d8af perf(simd): SSE2 ports of OP phrase stores and TOM scanline converters (#687)
Port the NEON fast paths merged in #670/#672/#682 to x86 SSE2 so Intel
Linux/Windows/macOS builds take the same wins:

- src/tom/op_simd_sse2.h: 16bpp and 24bpp OP phrase stores (movq
  load/store, cmpeq+and/andnot/or select for TRANS lane masking).
- src/tom/tom_scan_simd_sse2.h: 16bpp-direct, 24bpp and RGB16
  scanline converters plus the 2x hi-res gather/expand kernels.
  The 24bpp converter needs no vld4-style deinterleave: x86 is
  little-endian, so a raw dword load plus mask/shift reassembles
  XRGB8888 in three ops.

Dispatch in op.c/tom.c mirrors the NEON branches (#elif on
BLITTER_SIMD_HAVE_SSE2 from blitter_simd_arch.h) with the guard
conditions copied verbatim, so exactly one (or neither) fast path
compiles. SSE2 only -- no SSSE3/SSE4; the buildbot x86 baseline
stays intact. No build-flag changes needed: x86_64 has SSE2 as
baseline and Makefile.common already appends -msse2 wherever it
selects the SSE2 blitter (i686 CI rows included); a generic i386
build without SSE2 stays scalar via the capability macro.

c89-lint.sh: skip the new include-only headers in the default pass
(same as the NEON ones) and extend the per-arch pass to compile
op.c/tom.c per arch, which is the only host-side syntax coverage
the non-native fragment headers get.

c-cpp.yml clang-tidy: broaden the exclusion regex from
blitter_simd_* to all src/{tom,jerry}/*_simd_{neon,sse2,scalar}.{c,h}
fragments -- include-only files that cannot be linted standalone.
2026-08-27 04:02:41 -04:00
Joe Mattiello 6264f554e6 feat(debug): GDB remote stub Phase 2/3/4 -- breakpoints, GPU/DSP threads, docs (#685)
Phase 2: RSP ack/nak byte handling (the gap that stalled every real
m68k-elf-gdb on qSupported), Z0-Z4 breakpoints/watchpoints, c/s/vCont,
register and memory writes, the blocking halt loop (GDBHalt() freezes
retro_run() in place until continue/step/disconnect), a client
disconnect that disarms everything and resumes, and the
virtualjaguar_gdb_wait / virtualjaguar_gdb_halt_timeout core options.

Per-target armed counters (gdbArmed68K/GPU/DSP) plus a 256-entry
direct-mapped PC cache gate the three per-instruction hooks
(M68KInstructionHook, GPUExec, DSPExec) -- one load and one
never-taken branch when nothing is armed, never patching emulated
memory. Memory watchpoints ride jaguar.c's pre-existing (but
previously dead) bpmActive/bpmAddress1 pair via the new
GDBMemWatchHit(), single-slot by design.

Phase 3: GPU and DSP as GDB threads 2/3 with a custom
org.atari.jaguar.risc target description (R0-R31=0-31, PC=32,
FLAGS=33, fixed forever) served over qXfer:features:read, thread
enumeration/selection (qfThreadInfo/Hg/Hc/qC), and monitor commands
(disasm/regs/halt/trace/watch) via qRcmd. The RISC disassembler is
shared, not duplicated: src/debug/gdbdisasm.h is used by both
gdbtarget.c (live) and the refactored test/tools/gpu_disasm_dump.c
(offline).

Phase 4: docs/gdb-stub-guide.md, a user-facing guide covering attach,
the frontend-freeze halt model, the RISC disassembly gap, and the
symbol-level (not source-level) debugging ceiling.

New accessors added or un-gated from VJ_TRACE-only in gpu.c/dsp.c
(GPUGetControl, GPUSetReg/PC/Flags, DSPGetPC/Control,
DSPSetReg/PC/Flags, DSPGetReg/GPUGetFlags un-gated) so the stub can
read and write GPU/DSP state in every build.

Verification: make -j4 clean; make TEST_EXPORTS=1 test green (0
failures, 75 suites, including 45/45 gdbstub protocol tests);
c89-lint clean on every touched src/ file; savestate digests
byte-identical across stub-disabled / enabled-unattached /
enabled-attached-zero-breakpoints (test/tools/gdb_determinism_probe);
a live RSP client verifies the ack byte and no-ack transition end to
end (test/tools/gdb_attach_probe.py) and a real Z0 breakpoint halting
real (synthetic, injected) 68K execution at the exact armed PC
(test/tools/gdb_breakpoint_probe.py).

Perf A/B NOT measured this pass: the host stayed under sustained
unrelated load (other processes, load average 10-26 over repeated
checks) for the whole session. VJ_GDB_STUB_DISABLE_HOOKS is wired at
all three hook sites so the A/B can be run later on an idle machine --
see the PR body.

Refs #652
2026-08-27 04:02:25 -04:00
Joe Mattiello baf682de80 perf(voicechat): NEON saturating mix (#666)
Vectorize VoiceChatMixInto's add+clamp over each 6-pair upsample group
with vdupq_n_s16 + vqaddq_s16 so far-end mix matches the scalar
[-32768, 32767] clamp. Scalar path remains for non-NEON builds, tail
pairs, and mixed values that do not fit in int16.
2026-08-27 03:18:22 -04:00
Joe Mattiello 6edac105d4 perf(op): NEON fast path for 16bpp fixed-bitmap phrases (#672)
Store a full 4-pixel phrase with vld1/vst1/vbsl when the scalar loop
would have done four hi/lo byte stores with no RMW, REFLECT, or
shadow-FB hooks. Byte-identical to the existing walk on LE hosts.
2026-08-27 02:28:45 -04:00
Joe Mattiello 94b99cd687 perf(tom): NEON scanline converters for direct/24bpp/RGB16 modes (#670)
Vectorize the pwidth_scale==1 hot paths with ARMv7-portable NEON
(vld1q/vrev16q/vshrq/vmovl and vld4_u8), keep scalar tails, and add
VJ_RESTRICT so autovec still applies. BGEN stays compiler-vectorized.
2026-08-27 02:27:36 -04:00
Joe Mattiello 0d451c4e03 feat(debug): GDB remote stub Phase 1 — protocol engine, 68K target, loopback transport (#662)
* feat(debug): RSP packet framing for the GDB stub

Refs #652

* feat(debug): RSP run-length decode and command dispatch

Refs #652

* feat(debug): bounds-checked m packet for the GDB stub

Every guest address is validated by the target op before any read; the
engine turns refusal into E01 and never guesses a range.

Refs #652

* feat(debug): 68000 register and memory target ops

Refs #652

* feat(debug): loopback-only TCP transport for the GDB stub

Binds 127.0.0.1 only, never INADDR_ANY. Verified by observation:
netstat shows 127.0.0.1.<port> LISTEN, never *.<port>.

Refs #652

* feat(debug): core options and lifecycle for the GDB stub

Two Restart-gated core options (virtualjaguar_gdb_stub, virtualjaguar_gdb_port)
read raw via environ_cb in retro_load_game, matching the enhancement-hook
gate's reasoning: a developer-facing option must not be settable by a
per-title DB row. On enable, opens the loopback listener, inits the RSP
session against the Jaguar target ops, and shows a one-shot SET_MESSAGE_EXT
banner. Serviced once per frame from the top of retro_run() -- one poll()
and two comparisons when disabled or idle. All new statics (including the
packet accumulation buffer) are reset in both retro_unload_game() and
retro_deinit(), since iOS cannot dlclose the core.

Verified: savestate digests are byte-identical across stub-disabled and
stub-enabled-idle runs of test/tools/hires_state_digest (three checkpoints,
SHA-identical output). netstat confirms the live core binds
127.0.0.1.<port>, never *.<port>.

Also fixes two build-system gaps this change exposed: scripts/c89-lint.sh
was missing -Isrc/debug (libretro.c now includes gdbstub.h), and
Package.swift's coreSources/headerSearchPath arrays had drifted from
Makefile.common per the exact class of bug scripts/check-package-sources.sh
exists to catch -- it now reports clean. `swift build` itself still fails
in this environment on an unrelated, pre-existing manifest error
(defaultLocalization not set) reproducible on the unmodified manifest, so
full SPM compilation could not be verified end-to-end here.

Refs #652

* test(debug): end-to-end GDB attach probe

Scripted RSP client (test/tools/gdb_attach_probe.py) since the #581
toolchain does not ship a GDB. Run live against the actual core (not
just the fake-target unit tests): attach, qSupported, halt reason, 144-hex
register read, memory read, and bounds refusal on a wild address -- all
passed against ./virtualjaguar_libretro.dylib with the stub enabled.

Answers both of docs/gdb-stub-design.md's open questions, recorded there
with evidence:
- #581 toolchain ships no m68k-elf-gdb (checked the built tree after
  `make jaguar-toolchain-build`; PIN lists five tools, none a debugger).
- rln emits no DWARF: its output is classic mc68k COFF (not ELF), `-g`
  produced a byte-identical link to the same build without it, and no
  toolchain on this host can even parse the format as an object file.
  Every "source-level debugging" claim in the design doc is corrected to
  symbol-level (rln -m's load map) accordingly.

Refs #652

* fix(debug): correct overclaims and harden the GDB stub's Phase 1 edges

Post-review accuracy pass, three fixes:

- gdbtarget.c's non-perturbing comment overclaimed: it verified every
  who-gated side effect but said nothing about unconditional MMIO read
  side effects (e.g. CD BUTCH+2's DSCNTRL ack, reached only via the
  16/32-bit path this file's byte-granular reads never take). Narrowed
  to state exactly what was checked and what a debugger read through
  MMIO can still legitimately perturb, same as any processor touching it.
- docs/gdb-stub-design.md: the scripted RSP probe passes without ever
  sending the low-level +/- ack byte real GDB requires before
  QStartNoAckMode is negotiated -- a real m68k-elf-gdb would stall on its
  first qSupported. Documented as a named Phase 2 gap rather than left
  implicit in a probe that doesn't check for it.
- gdbsock.c: added the same #pragma comment(lib, "ws2_32.lib") under
  _MSC_VER that jlink_tcp.c (the file this one deliberately mirrors)
  already carries, for parity on a raw MSVC compile outside the
  Makefile's own -lws2_32 link line.
- libretro.c: a hand-edited config with a non-numeric gdb_port value
  used to silently ask the OS for an ephemeral port while the startup
  banner kept reporting the bogus requested number; now rejected in
  favour of the documented default.

Also confirmed jni/Android.mk and the MSVC buildbot targets both build
through Makefile.common's own SOURCES_C/INCFLAGS rather than a third
hand-maintained list, so neither needed the src/debug update Package.swift
did in the previous commit.

No behavior change to the wire protocol or the packet-framing engine.
Determinism re-verified byte-identical after these edits.

Refs #652

* ci(msvc): compile src/debug in the MSVC check

libretro.c now includes gdbstub.h, but the MSVC job's hand-maintained
source list and include flags had no src\\debug -- so cl.exe failed to
open the header on both x64 and x86 while every other target built fine.

Same class of drift as the SIMD blitter include-path miss: Makefile.common
carries the real source list, and these by-hand CI compiles silently fall
behind it. Only the first cl.exe invocation needs it; the second compiles
just the blitter for SSE2.

Refs #652
2026-08-27 01:40:06 -04:00
Joseph Mattiello f119191031 test(tools): audio_wav_dump -- raw WAV capture from a savestate, plus sample cadence
Every audio tool in the tree reduces the stream to a statistic: RMS, peak,
saturation density, non-silent fraction. Those catch loud-broken and
silent-broken audio, and they are all blind to a *spectral* complaint. The
AvP report in #633 -- "a strange parasitic noise running during gameplay" --
is a tone sitting under otherwise-correct audio, and test_audio_clipping
passes it cleanly (0 saturated samples, RMS 1652.7, no hot frames).

This writes the samples out instead, so they can be listened to or run
through an FFT. It reuses the shared harness for its CLI, which is what
gets it to the scene: --load-state reaches gameplay from a RetroArch save,
where these reports come from and where no other audio tool can currently
go. The harness installs its own audio callback during harness_load_rom(),
so ours is installed after and the core keeps whichever was set last.

It also reports the per-frame sample cadence, because the other half of a
"noise on Windows/Linux but not here" report is the frontend resampler: a
core emitting a jittery sample count per frame can whine on a strict
resampler and sound clean on a forgiving one, from an identical stream.
Ruling that out is a one-line answer instead of a theory. (AvP measures
min=max=800, spread 0 -- rigid, so not that.)

Used to produce the #633 evidence: 92% of energy below 200 Hz, 0.001% above
8 kHz, no discrete peak above 2 kHz -- i.e. no macOS repro, which is what
moved that issue from "patch the DAC" to "find out the reporter's platform".

Hand-built like its siblings, so it joins the c89-lint exempt list for the
same stated reason (builds against test/harness/, which is outside
$INCLUDES; C99 harness) and .gitignore's hand-built-binary block:

  cc -O2 -Wall -std=c99 -I. -I./test/harness -I./libretro-common/include \
     -o test/tools/audio_wav_dump test/tools/audio_wav_dump.c \
     test/harness/harness.c -ldl -lm

Refs #633
2026-08-26 23:51:00 -04:00
Joe Mattiello 7b66c02018 ci(pr-issue-link): create the link from a body tag instead of demanding a manual step (#644)
* ci(pr-issue-link): create the link from a body tag instead of demanding a manual step

Every new PR here went red on creation for a step that cannot be performed
until after the PR exists: the linked-issue check failed, and the fix was a
manual click in the Development panel that does not re-trigger the job.

The premise behind that was stale. This job's own comment -- and
docs/agent/github.md -- asserted "there is no public API to create that
link, so it has to be done in the UI" and "don't burn time on a gh flag or
GraphQL mutation". The mutation exists:

  addCloseIssueReferences -- "Adds one or more pull requests as manually
  linked closing references on an issue."

That is exactly the Development-panel relationship, and it is idempotent
(verified against PR #641/#632, already linked: re-running changed nothing;
and end-to-end against #642/#633 by unlinking, re-linking from a body tag,
and confirming the result).

So the job now makes the link rather than complaining that nobody did:

  * scripts/pr-link-issue.sh <pr> [issue] [--repo o/n] does the work and is
    runnable by hand. No issue argument -> scan the PR body for a tag.
    Already linked -> no-op, so it is safe to run unconditionally.
  * The workflow runs it and only fails when there is no tag to act on. Its
    failure summary now says to edit the description, because `edited`
    re-runs the job and it links then -- no push required.

Accepted tags, case-insensitive:

  Closes #123 / Fixes #123 / Resolves #123 / Refs #123
  <!-- link-issue: #123 -->      invisible in the rendered body

The hidden form wins over the keywords, so a PR can discuss one issue in
prose and link a different one deliberately. The keywords still do nothing
on GitHub's own side here -- GitHub honours them only when the PR targets
the default branch, which is `master` while PRs target `develop`. They are
markers for this job.

Security: the job runs pull_request_target with a write token, so it checks
out the BASE ref (actions/checkout's default for that event) and never the
PR head -- the script it executes is the trusted copy from the target
branch, not one the PR could have edited. The only untrusted input is the
body, parsed to an integer and validated to be an existing issue (not a PR)
before use. Permissions widened by exactly one: issues: write, which is what
the mutation needs.

Refs #643

* fix(pr-link-issue): ignore link tags inside code blocks

Caught by the feature's own PR. #644 documents the accepted tag syntax with
a fenced example:

    Closes #123    Fixes #123    Resolves #123    Refs #123
    <!-- link-issue: #123 -->

The scan matched that example before reaching the real `<!-- link-issue:
#643 -->` at the bottom of the same body, and linked the PR to issue #123 --
a closed OP performance spike with nothing to do with it. Any PR that
explains this feature would have done the same, which makes it exactly the
kind of body the parser has to survive.

Strip fenced blocks and inline code spans before scanning. Anything inside
code is a sample, not an instruction.

Verified: re-running against #644 now yields "PR #644 -> linked to #643",
and a synthetic body with an inline `Refs #111`, a fenced `Closes #222` and
`<!-- link-issue: #333 -->`, plus a real `<!-- link-issue: #444 -->` outside
any code, selects 444.

Refs #643
2026-08-26 22:04:04 -04:00
Joseph MattielloandClaude Opus 5 5f0925f058 build(spm): drop unsafeFlags -- they make the package unconsumable by version
Kimi review on #625, and it is a real defect that defeats the point of the PR.

SwiftPM REFUSES to build a VERSIONED dependency (`from: "x.y.z"`) on any
package whose targets carry unsafe flags:

    error: 'app': the target 'virtualjaguar-libchdr' in product
           'VirtualJaguar' contains unsafe build flags

Only path and branch/revision dependencies are exempt. So a frontend doing the
natural thing -- pinning a release tag of this repo -- could not build, which
is exactly the "depend on it as a package instead of vendoring it" workflow
this manifest exists to enable.

My verification could not have caught it: `swift build` inside the repo builds
this as the ROOT package, and the root is exempt from the restriction. The
spm-build CI job had the same blind spot. Same shape as the boolean.h
correction earlier on this branch -- a test that structurally cannot observe
the failure it is cited as evidence against.

The flags only silenced unused-function/variable warnings in libchdr's unity
TU. Not worth making the package unusable; removed.

Added a CI job that consumes the package the way a frontend actually would --
a throwaway tagged clone plus a tiny consumer depending on it by version -- so
the restriction is tested rather than remembered. Two things about that job
were established by experiment rather than assumed:

  - It must run `swift build`, NOT `swift package resolve`. Resolve SUCCEEDS
    with unsafe flags present; the graph is only validated for a build.
  - The clone goes in a directory named after the repo, because SwiftPM derives
    package identity from the URL's last path component and not from `name:` in
    the manifest.

Also from the same review: check-package-sources.sh now uses `set -eu` rather
than `set -e`, and `command rm` in its trap. An unset variable expanding to
empty is precisely how a drift guard starts silently passing on nothing, and
this repo's shells alias rm to `rm -i`, which blocks forever with no tty. These
guard scripts get copied from, so the precedent matters.

  swift build (root)                 clean
  scripts/check-package-sources.sh   66 core + 13 libretro-common agree

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-26 20:33:15 -04:00
Joseph MattielloandClaude Opus 5 534ded45f9 build: ship a Package.swift so Apple frontends stop vendoring this repo
Closes the last item on #614.

Apple consumption of this core currently requires a fork branch plus a
downstream manifest that re-declares every source file. That second copy of
the file list is the problem: the Provenance wrapper kept one and drifted by
27 files over seven releases before anyone noticed, and a missing
-DBLITTER_SIMD_<ARCH> in it meant every arm64 build inlined the SCALAR
blitter while the linked vtable was NEON (#612).

This manifest builds the C core only -- no Swift, no frontend glue -- so a
frontend depends on it as a package instead of vendoring it. It is inert for
every other build system: make, jni/Android.mk and the MSVC projects never
read it, and nothing here changes a flag they use.

Two guards, because a second copy of a list that changes every release is
what rots:

  scripts/check-package-sources.sh diffs Package.swift's arrays against
  Makefile.common, which stays the single source of truth. It fails loudly
  and names the files, never auto-fixes -- same posture as the
  cd-boot-matrix and version_fallback guards. Verified it catches both
  directions: a file added to Makefile.common but forgotten here, and a
  stale entry here that Makefile.common dropped.

  A new spm-build CI job actually runs `swift build`. Nothing else in CI
  reads this file, so without it a manifest that does not even parse would
  merge unnoticed and only break the frontend. That job also asserts exactly
  one blitter_simd_ops definition survives when all three SIMD variants are
  compiled together -- the invariant #617's arch guards exist to hold, which
  no Makefile target can check because it compiles only one variant.

Target names are prefixed (virtualjaguar-libretro-common,
virtualjaguar-libchdr). SPM requires them unique across the consuming app's
whole package graph, and generic names collide: a frontend carrying two
cores that each vendor CHD cannot build if both call the target "libchdr".
That is not hypothetical -- it broke the Provenance build this week.

path "." for the core target because libretro.c sits at the repo root and
SPM requires sources under the target path. The explicit sources list means
only named files compile, so docs/, test/ and scripts/ are ignored; the two
subdirectories owned by other targets are excluded so SPM does not reject
overlapping paths.

HAVE_COCOATOUCH and __GCCUNIX__, which the downstream manifest defines, are
deliberately absent: neither appears anywhere in the core sources.

  swift build                          clean, 80 files
  scripts/check-package-sources.sh     66 core + 13 libretro-common agree
  make -j                              0 errors (unaffected)
  make TEST_EXPORTS=1 test             0 test(s) failed
  one vtable, and it is NEON on arm64  verified via nm on the SPM objects
  blitter hot path                     47 NEON vector instructions

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-26 20:33:15 -04:00
Joseph MattielloandClaude Opus 5 f7ba81eef0 tools: add a cross-platform core installer, and link it from the site
Users are hand-picking .so files off the releases page and getting it wrong.
The failure is silent: RetroArch says only "Failed to open libretro core".

The specific trap is that builds are named by board AND word size -- rpi4 is
32-bit armhf, rpi4_64 is aarch64 -- and `uname -m` is the wrong way to choose.
Raspberry Pi OS shipped a 64-bit kernel with a 32-bit userland for years, so
uname reports aarch64 while every binary on the system is 32-bit. The core has
to match RETROARCH, not the kernel.

tools/install.sh reads the word size and machine out of the frontend's own
binary (ELF class + e_machine on Linux, the running slice on macOS) and aborts
before writing anything if the downloaded core does not match.

Finds the core directory by reading RetroArch's own libretro_directory out of
retroarch.cfg rather than carrying a per-distro table of paths. That is
authoritative everywhere, including the read-only-root distributions where a
guessed path would simply be wrong, and it does not rot when a distro moves
things. RetroPie is the one special case, because it keeps each core in its own
package directory that emulators.cfg points at instead.

POSIX sh, safe to pipe into a shell. Offers latest/nightly/pinned-tag, an
optional systemd-timer or cron auto-updater, --dry-run, --list, and --yes.
Prompts read /dev/tty because stdin is the script when piped; with no tty it
takes defaults rather than hanging.

Testing found two bugs worth recording:

  - Under zsh -- macOS's default shell, so a likely invocation -- `for a in
    $archs` does not word-split, so the universal-binary slice picker never
    matched and silently chose x86_64 on an Apple Silicon Mac. Verified: sh
    said macos-arm64 and zsh said macos-x86_64 on the same machine. Now a
    split-free `case`, and all four shells agree.
  - Piped into a shell, $0 is the shell name and not a file, so --help ran
    `sed` on "sh" and the re-run hints printed "sh --dest ...". Both now go
    through a SELF that falls back to the curl one-liner.

Verified: ELF detection agrees with file(1) on all 6 real Linux builds across
i386/arm/x86_64/aarch64; all 14 board+wordsize combinations map to assets that
exist in the release; syntax clean under sh, bash, zsh and dash, which all
choose the same build; discovery finds the real RetroArch on this machine by
config; install, re-install with backup, and the post-install load check all
work end to end -- RetroArch loaded the freshly downloaded core.

build_site.py gains {{RAW_URL}}, derived from REPO_URL so the curl one-liner on
the site cannot drift from the repository the rest of the page points at.

Supersedes the RetroPie-only script this replaces, which was never released.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-26 19:44:41 -04:00
Joseph MattielloandClaude Opus 5 96d0b1852f build: committed version.h fallback for build systems that can't run a script
src/core/version.h is generated by scripts/gen-version-h.sh and gitignored:
it carries the short git rev, so committing it would dirty the tree on every
build. The Makefile and jni/Android.mk both run the generator at parse time,
so for them the header always exists.

Build systems with no hook to run a shell script before compiling have no
version.h at all and failed on libretro.c's #include outright. The
Provenance SwiftPM manifest is the case this exists for; it is the last of
the four SPM-compat fixes tracked in #614.

libretro.c now prefers the generated header and falls back to a committed
src/core/version_fallback.h. The __has_include test is nested rather than
`#if defined(__has_include) && __has_include(...)`: the standard says #if
short-circuits, but older preprocessors that lack __has_include can still
choke parsing the call in the right-hand operand. Every compiler without it
here is Makefile-driven and takes the #else, where the generated header
always exists.

CORE_BASE_VERSION is a deliberate copy; the Makefile stays the source of
truth. Nothing rewrites the fallback at build time, on purpose: the
generator runs at Makefile *parse* time, on every build including
`make lint`, so a generator that edited a tracked file would dirty the
working tree on every build -- feeding a false "-dirty" into
scripts/build-id.sh and the VJ_EXPECT_BUILD guard, and recreating the
silent-overwrite footgun this replaces.

The copy is checked instead of fixed, the same shape as the
docs/cd-boot-matrix.md drift guard: a new c-cpp.yml job fails loudly if the
two diverge, and version-bump.yml now edits and commits both on a release
bump (it previously did `git add Makefile` alone, which would have dropped
the new file).

GIT_VERSION falls back to " unknown-rev", spelled so it cannot be mistaken
for a real rev in a bug report. It cannot reach the VJ_EXPECT_BUILD guard,
which only runs against Makefile builds.

Verified:
  libretro.c -fsyntax-only with version.h absent   0 errors (the SPM case)
  fallback expands to                              "v3.5.1" " unknown-rev"
  built dylib reports                              v3.5.1 d86659d-dirty
                                                   (generated wins, not the fallback)
  drift guard, fallback forced to v9.9.9           correctly reports DIVERGED
  make -j                                          0 errors
  make TEST_EXPORTS=1 test                         0 test(s) failed
  scripts/c89-lint.sh libretro.c                   passed

scripts/c89-lint.sh keeps generating the header before linting -- that is now
a deliberate choice, not a workaround, since the Makefile build is what the
lint models. Its comment says so, because deleting those two lines would
silently switch the lint to the fallback path.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-26 02:15:43 -04:00
Joseph MattielloandClaude Opus 5 2b180cb6d4 fix(ocgrid): stop on INT/TERM, refuse a saturated stock cell as baseline
Two review findings, both in the measurement guards the script exists to
enforce.

The trap cleaned up but never stopped. On Ctrl-C the handler ran rm and the
sweep carried on: the signal killed only the current cells frame_hash_ab, that
cell was recorded FAILED, and six more multi-minute cells followed. TERM from
timeout was ignored the same way. Split into an EXIT trap that cleans up and an
INT/TERM trap that exits 130.

A saturated stock cell still became the baseline. The doc is emphatic that
SATURATED means unmeasured and arms must not be compared -- but any
non-saturated cell would still print an authoritative-looking percentage
against a ceiling-pinned number. Only a measured stock cell can set the
baseline now; otherwise every row reports NO USABLE STOCK BASELINE.

Super Burnout hit exactly this case in the same run that prompted the fix:
1799/1800 in all seven cells, stock included.

Also records gate 1 complete across all eight candidates. One passes.

  Cybermorph          -48.7% / +44.7% / +72.8%   PASSES
  I-War               -18.2% / +6.3%  / +1.1%    noise, capped at stock
  Missile Command 3D   -0.2% / -14.3% / -11.2%   overclock actively harmful
  Doom, Club Drive, Iron Soldier                 flat locks
  Super Burnout                                  saturated at stock
  Val D Isere                                    still needs a gameplay save

The structural-null hypothesis holds as the general rule but not as a law:
Cybermorph disproves the strong form. Doom reads 593/1800 = 19.8 fps in
gameplay against real hardwares ~20 renders/s, which is a lead for #401 --
the 2x-too-fast figure came from the attract demo, a different code path.

Refs #378

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 23:38:37 -04:00
Joseph MattielloandClaude Opus 5 ac7a8e0e3f fix(ocgrid): honour the output argument, always run the 0.5x control, drop bare rm
Four review findings, all real.

The documented invocation passed an output filename that the script consumed
and threw away -- results only ever reached stdout. It now writes the file and
echoes, and CORE= overrides the hardcoded macOS library name.

The checklist table spliced a 900-frame 0.5x control into a table labelled as a
2400-frame window, and quoted it against a 1x baseline of 513 that appeared
nowhere in the document. Re-measured: all seven cells now come from one
2400-frame window, and the control reads -48.7% against 1.5x gaining +44.7%.
Near-linear in both directions, which is what makes the rest of the table
trustworthy.

Cleanup used a bare rm, against the house rule, and leaked the mktemp dir on
any early exit. Now trap + command rm -rf.

Beyond the findings: the 0.5x control is no longer something the operator has
to remember. It is a fixed cell in the grid, because a flat 1x/1.5x/2x row
cannot distinguish a real frame cap from a measurement that never responded --
the direct cause of both retracted tables on #378. Reporting is two-phase so
the stock cell is known before anything is quoted against it.

Kimi asked whether intl option arrays reference the deleted "timing" category
key: libretro_core_options_intl.h is a 91-line stub with no option_cats_/
option_defs_ arrays and no reference to any category, so no locale regresses.

Refs #378

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 23:06:01 -04:00
Joseph MattielloandClaude Opus 5 ed06c6998d feat(options): split Timing into Speed and Hardware Timing; add overclock gate-1 tooling
The Timing category held three different intents at once: two overclocks that
change what the game computes, two fast-forwards that change only how much work
the host does, and three accuracy models that deliberately make things slower.
One flat list gave a user no way to tell "this makes it faster" from "this makes
it slower on purpose".

Split into Speed and Hardware Timing (Experimental), and order Speed by what to
try first: the two free bit-exact fast-forwards, then the two overclocks that can
break a game. blit_memo moves here from Video -- it is the same free-speedup
class, and it silently disables idle-skip, which was undiscoverable from a
different category. Option keys are unchanged, so no user config breaks.

Also corrects the M68K text, which cited AvP and Checkered Flag as games
overclocking can smooth. Both were measured under #378 and both failed: AvP is
locked to one frame per 5 fields, Checkered Flag caps itself in software.

Adds scripts/ocgrid.sh (gate-1 grid), scripts/rzip_extract.py (RetroArch
compressed states are otherwise unreadable by the harness) and the #378
checklist, which records the first title to clear gate 1: Cybermorph, +44.7% at
RISC 1.5x and +72.8% at 2x, with an underclock control proving the metric live.

Refs #378

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 22:29:38 -04:00
Joseph MattielloandClaude Opus 5 b4d1f2a668 fix(site): enforce the copy standard, and three review fixes
Follow-ups from reviewing the redesign, none of them cosmetic:

- check_site.py now FAILS on any em or en dash in the built output, title
  attributes and <title> included. The repo's own rule in
  site-maintenance.md is "add a check there when you add a guarantee here",
  and a copy standard whose only proof is a grep someone ran once survives
  exactly until the next page edit. Negative-tested: injecting one dash into
  a built page trips it with the surrounding context quoted.
- Document the standard in site-maintenance.md, including why the fix for
  generated matrix text lives in normalize_dashes() rather than in the .md
  files: those are never hand-edited.
- The --font-display comment described a --font-display-weight token that
  does not exist, so the documented fallback protection was fictional. The
  h1 now sets font-stretch: condensed, which is the real (partial) fallback,
  and the comment says plainly what happens on a host with no condensed face.
- Wide A/B figures scroll horizontally on a phone, but iOS Safari hides
  overflow scrollbars until touched: a reader would see panel one of three
  and no reason to think the others exist, on the platform this design is
  most likely to be read on. They now get an always-visible chunky beveled
  scrollbar, which is both the honest affordance and the period-correct one.

Also swept the repo for references to the deleted fx pipeline (the earlier
sweep silently matched nothing: zsh ate the glob). Only the deliberate
historical note in site-maintenance.md remains. Confirmed the page copy
contains none of the marketing vocabulary the brief called out.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 19:45:05 -04:00
Joseph MattielloandClaude Opus 5 9e4879d206 fix(site): normalize em-dashes coming from the generated boot matrices
The zero-em-dash copy standard was holding for prose but leaking through
the data: cart_boot_matrix.sh writes notes like "black video (headless --
undetermined)", which reach the reader as a title= tooltip on the badge.

Normalized in the two matrix parsers rather than by editing
docs/cart-boot-matrix.md, because the compatibility page's entire claim is
that nothing on it was typed by hand. Punctuation only; no cell's meaning
moves, and the rule now holds however upstream writes the string.

Built output is at zero em/en dashes across all five pages.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 19:45:05 -04:00
Joseph MattielloandClaude Opus 5 22f7a31084 feat(site): 1994 window chrome, and a copy pass that stops selling
The maintainer's read on the old site was that it "looks very AI LLM made",
and he was right about the specifics: neon glow on every heading and stat,
rounded cards, a twelve-card auto-fit grid on the home page, scanlines laid
over body text, and a WebGL VHS filter on the hero.  That is 2020s dark-tech
in a retro costume, not 1994.

DESIGN.  The page is a desktop and each section is a WINDOW: striped title
bar, hard 2px bevels, solid drop shadow, zero border-radius, zero glow.
Windows 3.1 / GEM chrome drawn in CSS.  Red is structural (title bars,
rules, hazard stripes) instead of decorative, and three measured reds do
three jobs so every red/text pair clears WCAG AA with margin rather than
squeaking past at 4.5:1 on one brand colour.  Dark is primary, the console
being a black box; light is not a fallback but the same bevel system
rendered as a period system-gray desktop.  One display "shout" per page
(the h1, Impact, the register Jaguar box art used); all chrome, tables,
badges and numbers are mono, the readme register.  Scanlines exist only on
the hero CRT -- never over body text, and never over an A/B figure, because
those PNGs are measurements and the design frames them rather than filters
them.

STRUCTURE.  The twelve-card wall on the home page becomes a manifest: mono
key, prose value, evidence line.  why-this-core.html was the second card
wall; it becomes a claim/receipt spec sheet.  Screenshots get window chrome
with a real metadata slot in the title bar.  Wide three-panel diffs cancel
the window body padding to reach full window width, and scroll inside their
well on a phone instead of shrinking three panels to 110px each.

COPY.  Rewritten toward flat and declarative on all five pages.  Headings
name a thing instead of making a claim ("Overclocking that doesn't break the
game" -> "Clock scaling"; "The defaults are already the right answer" ->
"Leave the defaults alone").  Editorialising removed ("The striking part:"
before a measured figure).  Sixty-eight em- and en-dashes are gone, down to
zero sitewide including the <title> tags and the generator's own markup:
that is both the loudest LLM tell and an anachronism, since the voice being
aimed at is 7-bit ASCII.

Every factual claim, measured number, evidence link and screenshot is
preserved; link sets and load-bearing figures were diffed per page before
and after.

Verified with build_site.py + check_site.py, and rendered at 1400px and
390px in both colour schemes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 19:45:05 -04:00
Joseph MattielloandClaude Opus 5 7ddd1d75cd refactor(site): drop the WebGL hero effects and their deploy-time fetch
The hero ran two committed canvas engines plus a third-party Canvas UI
VHS/particle bundle that scripts/fetch_site_fx.py downloaded, hash-pinned
and compiled with esbuild on every Pages deploy.  That is a lot of moving
parts, a network dependency and ~600 lines of JS in front of a five-page
evidence site, and a decorative WebGL VHS filter is the loudest possible
"generated" signal on a page whose whole argument is measured data.

The 1994 chrome that replaces it is pure CSS.  The site now ships no
JavaScript at all beyond its JSON-LD blocks.

- delete site/assets/js/{crt-fx,hero-fx}.js and scripts/fetch_site_fx.py
- drop the fetch step from pages.yml (and, while in its paths filter, add
  the two inputs that were missing: check_site.py and cart-boot-matrix.md,
  which parse_cart_matrix already reads)
- strip the .fx-* canvas scaffolding from the index hero fragment
- rewrite the site-maintenance.md script policy; keep the deferred-local
  <script src> allowance in check_site.py as the contract for future pages

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 19:45:05 -04:00
Joseph Mattiello 6d9348f1c4 docs: settings and performance tuning guide, plus a site page
The core ships ~40 options and nothing explained which ones matter. This
adds the map: what the defaults do, what the visual enhancements cost,
what to change when a game runs slow, and which settings quietly cancel
each other out.

The load-bearing content is the performance section, and it is sourced
rather than folklore:

- Where frame time actually goes (DSP 50-67%, blitter 5-17% fast /
  ~34% AvP accurate, OP ~1%, 68K 0.7-2.6%), from
  docs/perf-audit-2026-08.md and docs/op-perf-profile.md. Figures are
  labelled as host arm64 profiles, NOT device measurements, because no
  on-device A10X capture exists yet (#601).
- The compounding trap: DSP idle-skip disables itself under RISC clock
  scale, dram_timing, gpu_pipeline_timing and blit_memo -- and NOT under
  the M68K clock scale or blitter timing, which is the counterintuitive
  half and was verified against the gate in src/jerry/dsp.c rather than
  taken from any issue text (both #595's body and earlier notes had it
  wrong in exactly that way).
- An ordered "what to try first" list, ending at overclocking rather
  than starting there, since on a DSP-bound title the RISC overclock
  trades the bigger win for the smaller one.

Also adds site/pages/performance.html (condensed web version, wired into
PAGES so it lands in the nav and sitemap) and two README pointers -- the
docs index and a "Running slow?" line under Known limitations, which is
where someone with a slow game will actually be looking.
2026-08-25 19:45:05 -04:00
Joseph MattielloandClaude Opus 5 4fa775d5cb feat(test): add a GitHub release-asset backend to the ROM store
At 22 MB a private GitHub repo is simpler and free, and the earlier "avoid
GitHub" note did not apply to it: that was about LFS, whose bandwidth is sold
in $5/50 GB packs. Regular git operations and release assets are not metered
that way, and nothing here is near a limit (100 MB/file, 2 GB/asset).

A release ASSET rather than files committed to a repo: git keeps every version
of a binary forever, so re-dumping a 4 MB ROM would add 4 MB to every clone
permanently, while an asset is replaced in place. The whole Tier-1 set travels
as one zip -- nine assets would be nine round trips and nine chances to end up
half-updated, and at this size partial transfers buy nothing.

Backend is inferred: VJ_ROM_GH_REPO set -> github, else s3. Both stay
supported, because Tier 2 (GB-scale CD images) will want R2's economics where
asset and repo ceilings bite first.

CI passes both secret groups; whichever is configured wins. VJ_ROM_GH_TOKEN
must be a fine-grained PAT with read-only Contents on the ROM repo -- the
built-in GITHUB_TOKEN is scoped to THIS repository and cannot read another
private one, which surfaces as a 404 rather than a permission error, so it is
worth stating in the workflow rather than leaving someone to debug it.

Docs also record the non-cost risk: these are commercial ROMs plus a BIOS
image, a takedown strike lands on the account or org rather than the file, and
Atari SA does act on Jaguar IP -- so prefer a personal account over the org
that hosts the emulator itself.

Verified against a stubbed `gh` implementing the release verbs over a
directory: publish creates the release and uploads a 17.5 MB zip; cold fetch
unpacks and verifies all 9; warm fetch never contacts the release; a missing
token exits 77 naming the GITHUB variables (the first version printed the S3
ones, which sends a GitHub user hunting a credential that has no bearing on
the skip); and a tampered archive is rejected with "fetched archive does not
match the manifest", exit 1, rather than being left where the next run's fast
path would trust it.

That tamper case initially "passed" because the packed ROMs carry mode 444
from the read-only corpus, so the dd that was meant to corrupt one failed with
Permission denied and the test proved nothing. Chasing the wrong result to its
cause is what turned it into a real check.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 20:48:15 -04:00
Joseph MattielloandClaude Opus 5 28326f0ad6 feat(test): Tier-1 private ROM store -- manifest, fetch, publish, CI wiring
Most checks worth running here need a commercial ROM that cannot be
committed: the audio pair, the clipping sentinels, the #406 DRAM-scale gate,
blit memoisation, the lightgun probe, and any cachegrind A/B on a real game
rather than a GPU-heavy demo. Without them `make test` exits 0 having skipped
all of them -- the Skyhammer sentinel looked for "Skyhammer_(1999).jag",
matched nothing, and reported success for months.

The full corpus is ~24 GB. The subset the suite actually asks for is 9 files,
22 MB. test/roms/tier1.patterns is the source of truth and every entry in it
is copied from a real find-rom.sh call site in the Makefile or test/tools, so
the list cannot drift from what the tests request; adding a title without a
test that wants it just grows the download. 22 MB fits GitHub's 10 GB Actions
cache with four orders of magnitude spare, so steady-state egress is ~zero and
only a manifest change re-downloads.

scripts/rom-store.sh has four verbs -- manifest / publish / fetch / verify --
over any S3-compatible store (R2 recommended for zero egress; nothing is
R2-specific). Behaviour chosen against this repo's silent-skip history:

  * exit 77 = SKIP (no store configured), deliberately not 0, so a caller can
    tell "no store" from "store is broken". CI renders those as a notice and a
    warning respectively.
  * verify-before-rename: a download lands at <file>.part and is size- and
    SHA-256-checked before it takes the real name, so a corrupt transfer can
    never be reused as a valid cached copy.
  * fetch is idempotent and offline-clean: all-present-and-correct never
    contacts the store.
  * manifest and publish share one resolver over tier1.patterns. The first
    version had publish re-glob the manifest basename, which made
    "[BIOS] Atari Jaguar (World).j64" unfindable by its own name, because
    `find -iname` reads "[BIOS]" as a character class matching one of B/I/O/S.
    Caught by the round-trip test, not by review.

CI fetches into test/roms/tier1 (gitignored), caches on the manifest hash, and
sets ROMS_PRIVATE_ROOT only when the fetch produced a verified tree -- pointing
it at an empty directory would make find-rom.sh miss everything and turn every
ROM-gated check into a silent skip. Fork PRs have no secrets and keep skipping
exactly as they do today.

No credentials are committed and the script never creates a bucket, sets an
ACL, or prints a secret. The bucket must be private.

Verified end to end against a local MinIO S3 server in podman: bucket created,
all 9 files published, cold fetch + verify clean, warm fetch contacts nothing,
a deliberately corrupted file detected and re-fetched, missing credentials
exiting 77 rather than 0, every Makefile find-rom.sh pattern resolving against
the fetched tree, and finally the real tests passing against it --
test_audio_presence PASS on Iron Soldier and test_audio_clipping PASS on
Skyhammer (RMS 3083.3, 0.000% saturated), both loaded from the fetched copy.

Setup, daily use and the Tier-2/3 plan: docs/test-rom-store.md

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 20:40:59 -04:00
Joseph MattielloandCursor 6453b22baa feat(cd): load Jaguar CD CHDs that carry CHSE session tags (#322)
Refuse pre-15886 rips, vendor libchdr, and ship pinned chdman helpers
on tagged releases and nightlies so users can convert CUE/BIN themselves.

Co-authored-by: Cursor <[email protected]>
2026-08-16 01:29:50 -04:00
Joseph MattielloandClaude Fable 5 2d1625cd17 docs(op): correct REMAINDER bit range, pin HSCALE/VSCALE direction
docs/jtrm-object-processor.md listed the scaled-object REMAINDER field as bits
16-31. The Jaguar Technical Reference Manual Revision 8 ("Scaled Bit Mapped
Object", Software Reference p.20) says bits 23-16, eight bits — so op.c's
`(p2 >> 16) & 0xFF` was correct and the doc was wrong.

Also records, verbatim from the same table, the thing the doc never stated and
which is easy to invert: HSCALE/VSCALE are DESTINATION PER SOURCE — "the number
determines how many pixels are written into the line buffer for each source
pixel" — so a value below $20 SHRINKS. Worked example added (IWIDTH=40 phrases
@ 8bpp with HSCALE=$10 renders 160px, not 640px), plus the REMAINDER algorithm
that pins the vertical direction and, by analogy, the horizontal one.

This is not cosmetic. While investigating #354 the bad REMAINDER entry sent me
looking for a nonexistent 16-bit field, and the absent direction note
independently led a reviewer to propose an inverted-HSCALE "fix". Both readings
are falsified by the manual; neither would have been proposed had the doc been
right.

Adds test/tools/op_list_dump.c, the OP display-list decoder built for that
investigation: dumps TYPE/YPOS/HEIGHT/XPOS/IWIDTH/DWIDTH/DEPTH/PITCH/HSCALE/
VSCALE/REMAINDER/DATA, raw phrases, and BRANCH link/cc. It separates "the OP is
misbehaving" from "the OP is faithfully drawing bad data the game wrote" —
opposite fixes — and on #354 established the latter. Indexed in the vj-debug
skill under "Pixels wrong". Skip-listed in c89-lint.sh alongside every other
harness-based tool (they build against test/harness/, outside $INCLUDES).

No src/ change; emulation behaviour is unaffected.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-12 19:51:54 -04:00