91 Commits
Author SHA1 Message Date
Joe Mattiello 6072a8ca5e ci(release): use quick downloads for tagged releases (#1187) 2026-11-06 22:53:41 -05:00
Joe Mattiello 5d0a42c027 ci(release): simplify nightly downloads and signing notes (#1185) 2026-11-06 21:27:34 -05:00
Joe MattielloandClaude Opus 5.5 a314dd3aa4 ci: GL qualification: windowed boot + every shader preset on Linux, Windows x64/arm64 and macOS (#1155) (#1160)
* feat(app): --shader PRESET|off for one run, and a [shader] result line per chain

`--shader PRESET.slangp` runs this one run through that preset and `--shader off`
through none; neither is written back to config.ini (like --theme).  A missing or
malformed preset is a usage error (exit 2) instead of a plain picture with a toast.

shaders.cpp now logs one stderr line when a chain is built or cannot be:
  [shader] <preset> built: GLSL 410 on OpenGL 4.1
  [shader] <preset> refused: ... (the GL_ARB_arrays_of_arrays limit, macOS GL 4.1)
  [shader] <preset> failed: ... (librashader's message)
so a CI run has a record of what the chain did (#1155 GL qualification).

Refs #1155

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

* ci: GL qualification workflow: windowed boot + every shader preset on Linux, Windows, macOS

One reusable workflow, gl-qualification.yml, boots the desktop app windowed with a
real OpenGL context on hosted runners and renders every bundled .slangp through
librashader, keeping the screenshots.  Legs: Linux (ubuntu-24.04, Xvfb + llvmpipe),
Windows x64 (hosted, no vendor GL: the automatic bundled-Mesa relaunch of #1138/#1143
must fire), Windows arm64 (folds in windows-gl-test.yml, which is retired), macOS
arm64 (Apple OpenGL 4.1; the 15 arrays-of-arrays presets are allowlisted as refused).

Per leg: one build, cmake --install into stage/, vj_shader_tests gl run directly
(exit 77 = skipped = failure here), then app/test/gl-qualification/gl_qualify.py
launches --shader off and every preset with --screenshot and asserts exit 0, the
last [gl] renderer line, a non-black PNG, a [shader] built line and a picture that
differs from the plain one.  Artifacts upload pass or fail; the job summary lists
failures with the [gl]/[shader] log tail.  Goldens are designed
(app/test/golden/gl/<os>/<preset>.png, refresh_goldens input) but not gated.

Triggers: pull_request on the GL/shader/packaging paths, workflow_call (release.yml
requires it on tags and release/**), nightly schedule, workflow_dispatch.

Closes #1155

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

* ci(gl-qualification): judge shaders_gl after the preset loop, fail fast, time the x64 fallback

First run (41565368188) findings: shaders_gl fails on llvmpipe and on Apple's software
renderer (crt-royale renders black in the 128x64 test picture) and the step stopped the
leg before any screenshot existed; the x64 automatic-fallback boot hit `timeout 120`
where arm64 took 3 s; the arm64 shaders_gl could not load the upstream prebuilt
librashader.dll (eager D3DX9_43.dll import, #1153/#1161).

- shaders_gl runs with continue-on-error and a "shaders_gl verdict" step fails the job
  after the preset loop, so the screenshots and logs upload either way.
- gl_qualify.py --fail-fast N (default 8): the loop stops after N consecutive failed
  presets and the summary says how many were not run.
- Windows fallback step: timeout 300 s, elapsed seconds logged (automatic-seconds.txt).

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

* ci(gl-qualification): empty-array expansion safe for macOS bash 3.2 (set -u)

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

* ci(gl-qualification): Windows leg records the boot, runs timing diagnostics, gates the loop

Second run (41567744247): the x64 automatic-fallback boot did not return within 300 s
although the screenshot was written (frame 20 reached); arm64 does the same in 2 s.

- the loader probe (context create/draw/destroy through the bundled Mesa) runs before
  the app boot, timed;
- the boot step records exit code, seconds and each side condition (automatic-seconds.txt)
  instead of aborting at the first failed check; a verdict step fails the leg later;
- when the boot did not come back, three bounded experiments (the Mesa-side child run
  directly, --emu-thread off, video.vsync=0 seeded) go to timing.txt;
- the preset loop runs only once the plain boot works (500 launches of a boot that does
  not return are hours of nothing); per-launch timeout 150 s.

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

* ci(gl-qualification): known-broken allowlist for presets librashader rejects everywhere

The second Linux run (41567744247) built and rendered 494 of the 500 bundled presets
in ~13 minutes of wall time; the six nedi bilateral-variant presets (all but variant2)
fail reflection with UnknownSemantics("play"): they chain vhs/shaders/vhs.slang, whose
`play` sampler is an LUT the presets never declare.  A preset bug, not a GL one, the
same on every leg, so it gets the same treatment as the macOS refused set: an allowlist
with the reason that may only shrink (a listed preset that builds is an error).

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

* ci(gl-qualification): verdict steps run after an earlier failed step too (always())

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

* test(app): shaders_gl renders the bundled presets at 640x480, not the 128x64 pixel-check picture

On llvmpipe (Linux) and Apple's software renderer (hosted macOS) crt-royale came out
black in shaders_gl: its mask passes scale by 0.0625 of the viewport and the preset says
"Safe for >= 341.333 horizontal triads at viewport size", so at 128x64 they are 8x4 px.
The app at 960x760 renders it (gl-qualification run 41567744247, 76% of pixels differ
from the plain picture).  The pixel checks keep their 128x64 (an exact 4x of the 32x16
source); the bundled loop and VJ_TEST_PRESETS render at a real-world 640x480.

Refs #1155

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

* ci(gl-qualification): every Windows Mesa launch bounded; llvmpipe single-thread and softpipe probes

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

* ci(gl-qualification): own librashader artifacts, macOS from source and sized down, release staging excludes qualification artifacts

Review (Codex P1/P2 on #1160) and run 41571727219:
- librashader.yml gains an `artifact_suffix` input; this workflow calls it with `-glq`, so a
  release run (app-package.yml + gl-qualification.yml) uploads no colliding artifact names.
- release.yml's two staging sweeps exclude artifacts/gl-qualification-* (screenshots, logs and
  the arm64 test zip are not release assets).
- macOS leg: the from-source aarch64 runtime the package ships, not upstream's prebuilt 0.12.0
  (crt-black_crush-koko.slangp fails to parse on the prebuilt, parses on the pinned source);
  3 launches at once in a 480x360 window (small-window.ini via gl_qualify.py --seed-config):
  Apple's software renderer managed 205 of 500 presets in 150 min at 960x760 and 2 jobs.
- refused-<os>.txt also covers presets the leg's GL cannot COMPILE (macOS: fsr and smaa+fsr need
  packHalf2x16, GLSL 4.20); a listed preset that builds is an error, the set only shrinks.

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

* test(app): gl_context_integration prints unbuffered stage markers (where does the x64 Mesa probe stop?)

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

* ci(gl-qualification): tracked known failures; the x64 exit hang counted, not masked; loop before shaders_gl

app/test/gl-qualification/known-failures.txt lists one failure per line (<leg> <kind> <reference>);
the workflow reads it and turns exactly that failure into a recorded pass with a note. Nothing else
on the leg is forgiven, and removing the line makes the leg fail again. First entry: windows-x64
exit-hang (#1167), the app never exits on the bundled MSYS2 MINGW64 Mesa 26.2.3 llvmpipe
(SDL_DestroyWindow/SDL_Quit hang after "context destroyed"; LP_NUM_THREADS=0 and softpipe exit
at once, arm64 Mesa exits in 2 s).

gl_qualify.py: --known-exit-hang <ref> polls the launch, kills its process tree 5 s after the
screenshot is complete (IEND present) and counts the preset, noting the known failure in
summary.md and results.json; a launch without a complete screenshot still times out and fails.
The Windows boot step accepts exit 124 when the screenshot and the [gl] lines are there, the
loaders step accepts it on PASS, and shaders_gl passes without its final OK line only when
nothing failed or skipped and its last check (the broken shader) ran.

Every leg runs the preset loop before shaders_gl: on the macOS software renderer a SIGALRM-killed
GL process left the next window creation hanging. shaders_gl's bundled loop and VJ_TEST_PRESETS
render at 384x288 (crt-royale's mask passes need >= 341 px; 640x480 exceeded a 600 s alarm on
Apple's software renderer), the macOS alarm is 1200 s. gl_context_integration prints unbuffered
stage markers so a log shows where a probe stops. docs/agent/testing.md describes the lists.

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

* ci(gl-qualification): capture waits for core frames; unbuffered shaders_gl; macOS 640x480 + capture diagnostics

Run 41727418224 (3eef3305) read per leg:
- Windows x64: the preset loop passed under the tracked exit hang (494 built, 501 launches killed 5 s after
  their screenshot, #1167) but the shaders_gl allowance failed: the test's stdout verdict lines were lost when
  the hung process was killed (a pipe is fully buffered; only the stderr [shader] lines survived).
  vj_shader_tests writes stdout unbuffered from main() on.
- Windows arm64: 493 built, CuNNy-8x32-nvl-2x-rgb timed out at 150 s on the 128-bit llvmpipe. The Windows
  per-launch timeout is 240 s like the other legs.
- macOS: shaders_gl passes at 384x288 (10 min). In the seeded 480x360 window every capture, --shader off
  included, came back a uniform white picture at 1.9 s a launch; not the game (yarc is black at core frame
  1-2 and shows its picture from frame 10 on), not the theme. The leg seeds 640x480 (fits the hosted display,
  keeps crt-royale above 341 px) and a "Capture diagnostics" step launches `off` once per window
  configuration (default, 480x360, +frame 120, +emu frames, no viewports, no vsync, 480x362, 640x480,
  960x760) and prints each picture's statistics to the step summary, so the artifact names the variable.

App: --screenshot-emu-frames M (dev): the capture also waits until the core has shown M frames, with a line
and a capture anyway 600 UI frames later if it never does; gl_qualify.py --emu-frames passes it, every leg
uses 60. VideoOut::emuFrames() exposes the counter.

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

* ci(gl-qualification): even indentation in the x64 shaders_gl allowance (editorconfig-checker)

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

* ci(gl-qualification): macOS captures after 4 UI frames, 240 min budget; 480x360 white = ImGui viewports

Run 41760278887 (6afaae1a): Linux, Windows x64 (loop and shaders_gl under #1167) and Windows arm64
green. macOS: the capture diagnostics named the white picture's variable. With ImGui's extra OS
windows on (ui.viewports, the default) a 480x360 or 480x362 window captures uniform white; with them
off, or at 640x480 and up, it captures the game. The 640x480 leg then built every chain it reached but
ran out of time: 172 built, 57 timeouts at 240 s (49 of them had built the chain; Apple's software
renderer spends ~12 s a frame on the heavy ones), fail-fast after 250 presets, and the job hit its
150 min limit right after the upload.

- The macOS loop captures after 4 UI frames instead of 20; --emu-frames 60 already makes the capture
  the game's picture. The other legs keep 20.
- macOS timeout-minutes 240 (build ~4 min, shaders_gl 10-18 min), shaders_gl alarm 1800 s.
- The diagnostics step keeps four launches (default, 480x360 with and without viewports, 640x480).

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

* ci(gl-qualification): shard the macOS leg 3 ways, track presets over 120 s as macos-arm64 slow

Apple's software OpenGL 4.1 renderer on hosted Macs cannot render ~500 presets in one job
(runs 41760278887 and 41776934638 each ran out of time).  The macOS job becomes a 3-shard
matrix (gl_qualify.py --shard I/N: sorted preset index modulo N, every preset once, each
shard with its own `off` baseline, 2 launches at once), the GL probe, capture diagnostics and
shaders_gl in shard 0 only, and a macos-summary job joins the shards with --merge, failing
when a shard is missing or the shards did not run every preset exactly once.  Only the
joined table goes to the run summary.

known-failures.txt gains a `macos-arm64 slow <measured>@<run> <preset>` kind: 68 presets slow
in every run that measured them.  A listed preset is stopped at --slow-threshold (120 s) and
counted as a known failure, not a checked picture; an unlisted one over the limit fails and
the summary prints its line to paste; a listed one that renders in time is flagged for
removal.  A launch with no [gl] line is retried once and never counts as slow; POSIX kills
send SIGTERM first (--graceful-kill) and report a surviving process group.  Every run writes
times.tsv and a "Slowest launches" table.  macOS captures after SHOT_FRAME UI frames again.

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

* ci(gl-qualification): no exit allowance for app launches (#1172); test programs under #1179

#1172 ends the app's bundled-Mesa child without unloading Mesa, so the `windows-x64 exit-hang
#1167` entry and every app-launch allowance go: the automatic-fallback boot step and the preset
loop (--known-exit-hang) fail again if an app launch does not exit on its own.

The two test programs, vj_gl_context_integration and `vj_shader_tests gl`, load the bundled Mesa
in-process and still hang in its DllMain after SDL_Quit; their allowances stay, unchanged in what
they require (the probe's `PASS GL loader` line; shaders_gl with no FAIL/SKIP and its last check
run), now keyed to `windows-x64 test-exit-hang #1179`.

The macOS summary states plainly how many presets are listed `macos-arm64 slow` and therefore
not rendered, with their measured times (finished median and range, timeouts counted apart) and
how many this run stopped.

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

* ci(gl-qualification): macOS slow list from the first sharded run; ntsc-blastem refused on GL 4.1

Run 41891214865 (3 shards, all 500 presets, every one run once): Linux, Windows x64 and
Windows arm64 green; macOS 384 built, 57 listed presets stopped at 120 s, 36 failures.

- 31 presets took over 120 s with the chain built (12 timed out at 240 s): listed as
  `macos-arm64 slow` with their measured times.  7 listed presets that rendered in under 100 s
  are dropped; 4 that took 102-114 s stay listed (a listed preset that renders in time is still
  checked).  92 presets are listed.
- ntsc/ntsc-blastem fails to link on Apple's GL 4.1 ("Input of fragment shader
  'LIBRA_VARYING_2' differs in type/qualifiers"); it builds on GL 4.6: refused-macos.txt.
- 4 consecutive shard-0 launches (and their retries) never got a window for ~15 min, then the
  runner recovered, as in run 41760278887: on macOS a launch that times out with no [gl] line
  now leaves a `sample` stack of where the app was stuck.

The summary sentence now says a listed preset stopped at the limit is not rendered or checked,
and counts how many this run stopped and how many rendered in time.

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

* ci(gl-qualification): fail a slow macOS preset only if it never renders; reset Apple's GL compile service; no Windows exit allowances

Gate change (option b): an unlisted preset now FAILS only if it does not render within the
240 s timeout.  One that renders but takes over SLOW_LIMIT (120 s) is a ::notice:: with the
line to paste into known-failures.txt and a count in the summary ("Unlisted, rendered but over
120 s").  Hosted-Mac times swing ~2x between runs (sabr-hybrid-deposterize: 131 s, 91 s,
229 s), so the hard 120 s line failed a few new presets every run.  Listed presets keep the
120 s cap; the summary sentence states both rules.

Run 41957577821 (fd76515e): Linux, Windows x64, Windows arm64 green; macOS 18 failures:
- 10 rendered in 121-205 s: notices under the new gate, not listed.
- 2 built and timed out at 240 s: listed (vectorscale-exact-energy-single-aa, xbr-lv2-sharp).
  94 presets are listed; fd76515e's three lines (all rendered within 240 s) are not.
- 6 never got a window, again in the vectorscale/xbr stretch (third run).  The new stack
  samples all wait in CGLChoosePixelFormat -> cvms_connection_create on an XPC reply from
  CVMServer (com.apple.cvmsServ, Apple's GL compile service) that never came.  --reset-cvms
  (macOS leg) kills CVMCompiler/CVMServer with sudo -n before the no-[gl] retry; launchd starts
  a fresh one; the summary counts the resets and how many retries then got a window.

#1181 fixed #1179 (the GL test programs now exit without unloading the bundled Mesa): the
`windows-x64 test-exit-hang #1179` entry, the Known failures step and both test-program
allowances are gone, so any Windows process that does not exit on its own fails the leg.

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

* ci(gl-qualification): list the 2 macOS presets that timed out with a chain on run 42110580090

crt-yah/presets/pure-lcd (204.9 s on 41957577821, over 240 s now) and vectorscale (no window on
41957577821, the CVMServer stall; chain built but no picture in 240 s now) are added as
macos-arm64 slow, 96 listed.  The 15 unlisted presets that rendered in 121-205 s on that run are
notices under the current rule and are not listed.

Also corrects the SMOKE_PRESETS comment (Kimi review): PR runs render all presets; only
workflow_dispatch's preset_set=smoke selects the subset.

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

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-05 20:04:02 -05:00
Joe Mattiello e64e1bebee Merge pull request #1173 from libretro/claude/1150-azure-artifact-signing-50ffc2
ci(release): Authenticode for the Windows core DLLs, cue2cdi and the jagcd tools (#1150)
2026-11-04 03:21:52 -05:00
Joseph MattielloandClaude Opus 5.5 92b6fa382d cart identity: backfill hdr_byte0 as the loader maps it; fail unchecked audits (#976)
Review fixes for PR #1170.

* cart_matrix_identify.py recorded the file's first byte for images the
  loader maps elsewhere, so --check would mismatch fresh probe-stamped
  rows (Codex).  It now follows ParseFileType's order: ABS/COF/JAGR/DRI
  headers are RAM programs (cart slot untouched, $00); a $400-marker image
  is a cartridge (its own byte 0); an Alpine image ((size + 8192) a whole
  MiB, or the $802000 header-stub shape, ported) reads the loader's $FF
  fill.  The two shapes that depend on the raw-binary scorer (exact MiB
  without the marker, e.g. Rayman Demo #973, and headerless images) take
  the mapping from the row's own BIOS column: a run where the boot ROM ran
  was a cartridge mapping, one without a boot phase was not; when the
  column says neither, hdr_byte0 is left out for the probe to fill and
  --check skips only that field.  Re-stamped the committed matrix: 85 rows
  change byte 0 only (Rayman Demo and BadCode4 (rom) to $FF, the RAM
  programs to $00); no reject label changes; cells byte-identical;
  --check clean against the private corpus.
* --check exited 0 when a row's file was missing or empty, or when it
  verified nothing (Codex).  It now exits 1 on any of those, and the
  backfill exits 1 when it could not stamp a row.  --check compares field
  by field instead of the whole stamp string.
* The probe's identity path dlsyms FindRomIdentifier and
  FindVerifiedRomVariant, which were not in the wide test ABI, so a real
  sweep would have stamped filedb=0 on every row (found while checking
  Kimi's live-core concern).  Added to link-test.T and exports-test.list,
  and cart_boot_matrix.sh now refuses a core that lacks any identity
  symbol.  Kimi's ordering concern itself does not hold: harness_load_rom
  runs retro_load_game, which loads the image (JaguarLoadFile) before
  probe_identity reads it; the probe now says so.
* The $F6..$FE byte-0 range cites its source (JaguarCartNeedsBIOS and
  LooksLikeBjlRamImage in src/core/file.c, #784/#785 corpus survey).

Tests: scripts/test_cart_matrix_identify.py (byte-0 mapping per loader
path, row-driven decisions, stamp format, field compare, end-to-end
backfill + check, and failures on mismatch / missing / empty / nothing
checked).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 00:03:43 -05:00
Joe MattielloandClaude Fable 5.1 7b46632305 site: derive BIOS_REJECT and black-video labels by cause (#976) (#1170)
The compatibility page printed "Not yet verified: BIOS_REJECT" for the
real boot ROM's red "Jaguar" reject screen (#852/#744: an observed,
authentic outcome, not a testing gap) and "video undetermined" for
GAME_CODE + black, which read as broken.  Every cell label is now derived
from row data, nothing per title:

* BIOS_REJECT: filedb FF_BAD_DUMP, or a verified same-title sibling
  (filedb FindVerifiedRomVariant, or another dump of the title that boots
  under the real BIOS at the same size) -> "altered or bad dump (a
  verified dump boots)" (badge `info`); byte 0 outside $F6..$FE ->
  "Unsigned image: the real boot ROM does not accept it", "(works in HLE)"
  only when the HLE column is GAME_CODE; byte 0 in $F6..$FE with no
  sibling -> "(dump cannot be verified)"; no identity data -> the bare
  fact.  Never "Not yet verified".
* GAME_CODE + black: "Runs; no picture in the test (may be audio-only or
  need input)"; in the BIOS column with no boot phase (bios_ran=0): "Boot
  ROM not used (RAM-loaded executable)".
* ? (trap_loop): "Known issue: crashes at start (illegal instruction)"
  when the notes name the $1000 ILLEGAL guard, else "(68K exception loop)".
* Rows with the same CRC32 and verdicts collapse into one (.jag/.rom
  twins and same-byte copies filed under two names), listing every name;
  filedb names are preferred for display with the file titles beneath.

Data path: cart_boot_probe.c prints the image identity after the run
(size, the core's jaguarMainROMCRC32, byte 0 at $800000 as the boot ROM
sees it, the filedb row and its verified sibling); cart_classify.sh turns
it into a per-row `<!-- id: ... -->` stamp that cart_boot_matrix.sh writes
beside the build stamp; build_site.py and matrix_diff.py strip it like the
build stamp.  test/tools/cart_matrix_identify.py backfills or --checks the
stamp from the files alone (ports the copier-header and Alpine rules), and
is what stamped the committed matrix: 155 rows, --check clean, cells and
build stamps byte-identical, matrix_diff "no backward moves".

Tests: scripts/test_build_site_cart.py (one case per label, twin collapse,
display names, end-to-end parse/render; discovered by make test and the
two site workflows via test_*.py), test_matrix_diff.sh gains the identity
stamp cases.  Site rebuilt from the fixture data: 146 rows, 0 "Not yet
verified", 4 bad-dump / 2 unverifiable / 2 unsigned reject labels.

Closes #976

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-04 00:02:22 -05:00
Joseph MattielloandClaude Fable 5.1 8e7fc92296 ci(release): Authenticode for the Windows core DLLs, cue2cdi and the jagcd tools (#1150)
The same .github/actions/windows-sign composite action app-package.yml uses, dropped into
the two remaining Windows jobs, both already on windows-latest (x64):

- release.yml `build`, Windows rows: after strip and before the upload, dist/ is signed
  (virtualjaguar_libretro-windows-{x86_64,i686}.dll, cue2cdi-windows-x86_64.exe; the
  -debug.tar.gz is not a PE file).  The job grants id-token: write; a Windows sidecar
  records the outcome like the macOS rows do.
- chdman-tools.yml: dist-jagcd/ is signed before the zip (chdman.exe, jagcd-chd-check.exe);
  the three AZURE_* secrets join the workflow_call list (release.yml inherits), the job
  grants id-token: write, and a Windows sidecar is uploaded next to the macOS one.
- Gate: release.yml now passes WINDOWS_EXPECT=signed under the same six-value condition as
  WINDOWS_APP_EXPECT, so every Windows file is expected signed once the Azure values exist.
- Docs: README table rows for cores/tools, quota figures (~40 per app-package run, ~45 per
  release run), release-process bullets; the composite's `signed` output description.

After this the only remaining step for #1150 is the maintainer's Azure enrollment.

Refs #1150

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-04 00:00:55 -05:00
Joe Mattiello fcef4ca28e Merge pull request #1169 from libretro/ci/1150-azure-artifact-signing
ci(app): Azure Artifact Signing for every Windows binary; retire the SignPath stub (#1150)
2026-11-03 23:52:36 -05:00
Joseph MattielloandClaude Fable 5.1 d7482ba3ec ci(app): Azure Artifact Signing for every Windows binary; retire the SignPath stub (#1150)
Every exe and dll in the Windows app packages (virtualjaguar.exe, librashader.dll,
the software-opengl/ copy and the bundled Mesa DLLs, x64 and arm64) and both Inno
Setup installers are Authenticode-signed with Azure Artifact Signing: SHA-256,
RFC 3161 timestamp from timestamp.acs.microsoft.com, OpenID Connect login
(azure/login), no certificate material or client secret anywhere.

- .github/actions/windows-sign: composite action (azure/login v3.1.0 and
  azure/artifact-signing-action v2.0.0, both pinned by commit) that decides from
  its inputs whether it can sign, prints a ::notice:: and does nothing otherwise,
  signs, then re-reads every file with Get-AuthenticodeSignature and fails when
  one is not Valid and timestamped.
- app-package.yml: the Windows leg becomes build (native runner per arch, install
  tree -> intermediate windows-stage-<arch>) -> windows-package (ONE x64 job for
  both architectures: the signing action does not run on Windows ARM runners,
  its dlib is x64/x86 only) -> windows-smoke (native runner per arch, Authenticode
  re-checked on the DOWNLOADED artifact, portable zip, silent install/uninstall).
  SignPath stub (inputs, secrets, env, steps) removed; secrets are now
  AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_SUBSCRIPTION_ID plus the variables
  AZURE_SIGNING_ENDPOINT / AZURE_SIGNING_ACCOUNT / AZURE_SIGNING_PROFILE.
- app.yml and release.yml grant id-token: write to the package job; app.yml
  passes the three secrets so same-repo PR packages are signed like a release.
- Release gate: a second expectation, WINDOWS_APP_EXPECT, for the app zips and
  installers; release.yml passes `signed` when all six Azure values exist. The
  core DLLs, cue2cdi and the jagcd tools keep EXPECT_WINDOWS (unsigned) until
  their jobs sign too. The zip check now covers every PE file inside, summarised
  in one cell. stage.sh skips the windows-stage-* intermediates.
- app/packaging/windows/verify-signatures.ps1: the shared check (files, folders,
  zips; -Expect signed|unsigned|report; table in the job summary).
- package.sh: the local sign hook signed the x64 installer name for both arches.
- Docs: app/packaging/README.md (secrets/variables, layout, Mesa decision, quota,
  the one-time Azure setup incl. roles and federated-credential subjects),
  docs/release-process.md.
- release-packages.cjs: the three new jobs join the completeness list (15).

Refs #1150

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-03 19:48:49 -05:00
Joseph MattielloandClaude Fable 5.1 498199a51d ci(app): Flatpak aarch64 leg with the shader runtime (#1156)
The `flatpak` job of app-package.yml becomes a matrix over the CPU:
x86_64 on ubuntu-24.04 (unchanged: same bundle name, same artifact
`app-package-flatpak`) and aarch64 on GitHub's hosted ubuntu-24.04-arm
runner, producing VirtualJaguar-<ver>-linux-aarch64.flatpak as
`app-package-flatpak-aarch64`.  Both legs are native: the Flathub CI image
ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08 is a
multi-arch index (amd64 + arm64), and the freedesktop 25.08 runtime and
the rust-stable SDK extension ship for aarch64 on Flathub, so the Rust
librashader module is compiled by the aarch64 Rust toolchain, not under
QEMU.  A step refuses a non-native host (uname -m must match the leg) so
emulation can never creep in silently.

The manifest needs no change: cargo-sources.json vendors the whole
Cargo.lock (every crate of every target), `cc`, `libc`, `getrandom` and
`cfg-if` are the only platform-sensitive ones and all are pure-Rust or
host-compiled, and the tree has no `ring`.

Per leg, the job now writes the wall time of the build, the runner and
whether the flatpak-builder cache was warm to $GITHUB_STEP_SUMMARY (the
issue's acceptance item), and the smoke test gains an arch check on the
installed ref and on platform.machine() inside the sandbox before the
same ctypes ABI check (librashader.so loads by bare name, ABI 2) that
the x86_64 leg already ran.

Registered the new asset everywhere the x86_64 bundle was named: the
nightly release body table (release.yml), the PR package comment
(app.yml), the Download page classification (build_site.py), the
package tables in app/packaging/README.md and docs/release-process.md,
the site's app page, and the release signing gate's hermetic test
(test/release/test_release_scripts.sh now stages both bundles and
asserts each is a `flatpak`-class row).  verify.sh itself classifies
`*.flatpak` by extension, so the gate expects the aarch64 bundle with no
further change.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-03 18:52:48 -05:00
Joseph MattielloandClaude Fable 5.1 1773c623dd fix(release): skip per-artifact .info copies; never publish a dry run; bound the gate
- stage.sh ignores the virtualjaguar_libretro.info every build artifact
  carries (the Windows ones differ by CRLF) and writes the shipped .info
  from the checkout, as before.
- The release and nightly jobs refuse to publish when the run is a dry_run
  dispatch, whatever the branch (Codex P1).
- verify-signatures and stage get timeout-minutes: 30, so a hung check cannot
  hold the develop nightly, which waits for the gate to end but ignores its
  verdict (Codex P2).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-03 00:27:35 -05:00
Joseph MattielloandClaude Fable 5.1 4d217a3d45 fix(release): stage.sh accepts byte-identical duplicate assets
Every build job uploads its dist/ tree, and dist/info/virtualjaguar_libretro.info
is in the repository, so each artifact carries the same .info file.  The
duplicate-name guard tripped on that in the first dry run; identical copies
are now skipped and only differing files under one asset name fail.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-02 23:53:30 -05:00
Joseph MattielloandClaude Fable 5.1 313a18d70b ci(release): attest, sign and gate every release asset (#1151, #1152)
One `stage` job in release.yml now flattens the artifacts, writes
SHA256SUMS.txt, signs it with the project minisign key
(SHA256SUMS.txt.minisig; public key docs/release-signing.pub once the
maintainer generates it) and attests build provenance for every file with
actions/attest-build-provenance.  A `verify-signatures` gate on a macOS
runner then checks each asset against its expected signing state
(scripts/release/verify.sh: macOS Developer ID + notarized, Windows
`unsigned` until #1150 flips one line, AppImage and checksum signatures once
their public keys are committed, attestation on everything), strict on tags
and release/**, warn-only on develop so an outage cannot withhold the core
nightly.  Its per-artifact table (signed, notarized, attested) replaces the
signing prose in the release body and the job summary.  The macOS and
AppImage signing steps write signing-status sidecars for the facts the gate
cannot re-derive from a file (Apple's ticket on a bare Mach-O).

AppImages: make-appimage.sh passes --sign --sign-key to appimagetool when
VJ_APPIMAGE_SIGN_KEY is set (app-package.yml imports APPIMAGE_GPG_PRIVATE_KEY
into a throwaway GNUPGHOME; the passphrase goes through
APPIMAGETOOL_SIGN_PASSPHRASE).  appimagetool 1.9 has no verifier, so
app/packaging/linux/verify-appimage.py re-hashes the image with the
signature sections zeroed and runs gpg --verify.

Installers: tools/install.sh and install.ps1 verify SHA256SUMS.txt's
minisign signature (pinned key) or, with gh, its attestation before
trusting it; a bad one aborts, a release that predates signing warns;
--no-verify / -NoVerify skips the step.  Both hermetic suites cover it with
fake minisign/gh.  New release-scripts.yml tests the stage/sign/verify
scripts (Linux also signs a real AppImage).  A `dry_run` dispatch input on
release.yml runs everything up to the gate on any branch and publishes
nothing.

Docs: key generation and secret commands in app/packaging/README.md
(SignPath "dormant" text removed), release-process.md, SECURITY.md, the
site's Download page (public key once committed), the nightly tracker text,
and the stale "signing service" known-issue line in the v3.11.0 notes.

Closes #1151
Closes #1152

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-02 21:36:57 -05: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 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