9 Commits
Author SHA1 Message Date
Joseph MattielloandClaude Sonnet 5.5 e873883e33 feat(debug): core side of the app debug interface (vjdebug.c) with socket-less halt pump
src/debug/vjdebug.c implements struct vj_debug_iface: side-effect-free peek
(JaguarReadByte DEBUGGER), policy-checked poke, both RISC register banks, RISC
disassembly, attach/halt-pump/breakpoints on the existing GDB halt machinery
(a host can attach without a socket; GDBHalt pumps it and never blocks when
nothing could resume it), a one-walk OP display-list snapshot taken before the
OP rewrites HEIGHT/DATA, a blitter launch log ring, TexReplaceReload, and the
frame counter.

The interface is handed over once via VJ_ENV_SET_DEBUG_IFACE, only to a
frontend that set VJ_CAP_DEBUG (RetroArch never does).  Detached cost: one
global test per OPProcessList call and one per blitter launch.  Statics reset
on unload and deinit.

test/test_debug_iface.c drives it on yarc.j64: who gets the interface, peek ==
jaguarMainRAM, the poke policy, halt/step/continue/detach through a scripted
host, breakpoint re-hit, OP snapshot, blit log, and an identical frame + RAM
hash with everything switched on.

Refs #652 Refs #833

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 20:23:06 -04:00
Joseph MattielloandClaude Sonnet 5.5 8b9065d86e feat(core): free-form clock scale, gdb timeout clamp, private frontend event channel
Parser-only emulation change: virtualjaguar_{m68k,risc}_clock_scale also accept
a percentage ("175" or "175%", clamped 25..400) after the listed values
(0.5x/1.5x/2x/3x), which map exactly as before; anything else the old ladder
did not know still resolves to stock. virtualjaguar_gdb_halt_timeout is clamped
to 3600 s.

New src/core/vj_frontend_ext.{h,c}: a RETRO_ENVIRONMENT_PRIVATE HELLO/EVENT
channel (ABI 1). retro_init asks once; a frontend that does not answer (RetroArch)
latches it off so no event is ever sent; retro_deinit resets the latch. Events
(LINK_UP/DOWN/STATS, WIRE_SPEED, CRASH_WATCHDOG) are emitted from retro_run next
to the existing log lines, which are unchanged. JLinkStatsLast() exposes the
numbers behind the stats line.

A typed link host that was never a picker row no longer draws the 'dropped off
the LAN' warning.

Refs #833 Refs #888

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 16:42:39 -04:00
Joseph MattielloandClaude Sonnet 5.5 958d859ba2 fix(spm): exclude app/ from the SwiftPM core target
The target path is "." and SPM scans it for resources even with an explicit
sources list. The standalone app's CMake build unpacks SDL3 and Dear ImGui
under app/build*/_deps, which carry .metal shaders, .storyboard files and an
.lproj, so `swift build` at the repo root would fail on any checkout that has
built the app in-tree (same class as the tools/vendor .lproj exclusion).
app/ holds no core sources; check-package-sources.sh still passes.

Refs #833

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-11 20:11:21 -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
Joe MattielloandClaude Opus 5 5656c1516f feat(hooks): user-supplied hook file + one patching router doc (#637) (#716)
* feat(hooks): user-supplied hook file + one patching router doc (#637)

We shipped three overlapping ways to modify a game with no single door,
and the question that prompted this ("can users put their own patch files
in?") came from someone who works on this core -- so discoverability was
the bug. Answer: yes, two ways already (cheats, soft patching); the one
thing a user could NOT author was a hook. Both halves fixed.

docs/patching-a-game.md routes the four cases -- cheat code, IPS/BPS/UPS,
byte patch, "different emulator settings for one game" (which is a
per-title default, not a patch at all). Cross-linked from README and the
top of rom-patches.md / enhancement-hooks.md so any entry point reaches
the router.

src/core/hookfile.c parses <system_dir>/vj_hooks.txt:

    crc=DC187F82
    hook=my-patch 0x0012A4 4E71 4E75

Line-based, NOT JSON: libretro-common vendors no JSON parser, and
hand-rolling one adds a large parsing surface to a feature whose whole
job is writing bytes into a ROM. Same shape as vj_netlink.txt, which this
core already reads. Field splitting is hand-rolled rather than strtok_r --
that is POSIX, and msvc05/msvc10 on the GitLab buildbot is exactly where
our C89 lint is structurally blind.

Every fence from docs/enhancement-hooks.md is inherited, none relaxed:
- Same gate, read raw via environ_cb BEFORE the file is parsed, so a file
  can never switch on its own gate.
- expect[] mandatory; all-or-nothing per title in the applier.
- Additionally all-or-nothing per FILE: any malformed line discards every
  hook parsed. A half-understood patch set is worse than none, and the
  applier's own all-or-nothing rule would otherwise be enforced over
  hooks we only partly parsed.
- Applied once at content load, so nothing enters the savestate blob --
  no new field, no version bump.
- User file wins over a built-in row for the same CRC, with a LOG_WRN
  naming both, matching the DB's "user-set values always win" rule.
- Nothing allocates. Every bound is a compile-time constant, so a file
  cannot drive allocation or overrun by declaring a count.
- Malformed or absent file never refuses to load the game.

13 new assertions in test/test_titlehook, including the ones that matter:
a malformed line discards the whole file, expect/patch length mismatch is
refused, over-long hex does not overrun the arena, and >TITLEDB_MAX_HOOKS
refuses the file. The parser is pure, so they drive it directly and then
feed the parsed set through the real applier against a real buffer.

Verified: make TEST_EXPORTS=1 test exit 0, zero FAILs; c89-lint clean on
all four touched .c files.

Refs #637

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

* fix(build): add hookfile.c to Package.swift; guard test fopen results (#637)

Three CI failures on the first push, all mine:

- Package.swift repeats Makefile.common's source list (SPM cannot read a
  Makefile), and I updated only one side -- which is exactly what the
  "duplicated lists in sync" guard exists to catch, and what broke the
  SwiftPM build job too. One line, both green.
- clang-tidy flagged four clang-analyzer-nullability warnings in the new
  test block: fopen results used without a NULL check. Every fixture
  write is now guarded, and the first one bails out rather than
  pretending to test a file it could not create.

Verified: check-package-sources.sh OK (70 core sources agree), clang-tidy
clean on all four changed files, make TEST_EXPORTS=1 test exit 0.

Refs #637

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

* fix(hooks): bounded name copy, not strcpy (#637)

clang-analyzer-security.insecureAPI.strcpy on the CI runner. The length
was already validated one branch earlier, so this was safe in fact -- but
the check is right that strcpy is unbounded at the call site regardless of
what the caller checked, and a parser reading an attacker-editable file is
the last place to argue with that.

Local clang-tidy (llvm 21) did not raise it; the CI runner's did. Worth
knowing the two disagree.

Verified: test_titlehook OK (0 failures), make TEST_EXPORTS=1 test exit 0,
c89-lint clean.

Refs #637

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

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-29 01:13:25 -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 MattielloandClaude Opus 5 9439e50947 fix(spm): exclude tools/ so a fetched Jaguar toolchain doesn't break the manifest
`swift build --target virtualjaguar` failed with

    manifest property 'defaultLocalization' not set; it is required in the
    presence of localized resources

but only on machines that had run tools/jaguar-toolchain/setup.sh.  The fetched
toolchain lands at

    tools/vendor/jaguar-toolchain/new_bjl/macos_helper/launcher.app/
        Contents/Resources/English.lproj

and SPM reads that .lproj as a localized resource, which hard-errors the
manifest before a single file compiles.

The load-bearing fact, because the comment this replaces got it wrong: an
explicit `sources:` array does NOT stop SPM's resource scan.  SPM walks the
entire target `path` for resources independently of `sources:`, and `exclude:`
is the only thing that stops it.  With `path: "."` that scan covers the whole
repo, so any .lproj anywhere outside the exclude list breaks the build.

Fixed by excluding tools/ rather than by adding `defaultLocalization: "en"`.
Verified that the resource scan does respect `exclude`, which makes
defaultLocalization not merely redundant but worse: it would make SPM *accept*
the toolchain's InfoPlist.strings as a resource of the C library instead of
ignoring it, giving the core a resource bundle it has no business carrying.
Kept to the one demonstrated offender -- a sweep of docs/, site/, test/ etc.
would add unexplained entries without closing the class anyway, since a .lproj
under src/ would still break it and src/ cannot be excluded.

Verified on macOS with the Xcode toolchain, each run with a fresh
--cache-path/--scratch-path so nothing came from a cached package graph:

  - offending path present, before fix -> the defaultLocalization error
  - offending path present, after fix  -> build complete
  - offending path absent,  after fix  -> build complete
  - scripts/check-package-sources.sh   -> OK, 66 core + 13 libretro-common

Note that CI cannot catch this class: /tools/vendor/ is gitignored and the
spm-consumer job resolves the package by version from a clean tree, so the
directory that triggers it never exists there.  It surfaces only locally, and
only for developers who have fetched the assembler toolchain.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-27 00:57:58 -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