mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
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]>
458 lines
27 KiB
Markdown
458 lines
27 KiB
Markdown
# Jaguar GameDrive (JagGD) interface notes — research for #312
|
||
|
||
Status: phases 1+2 of §9 are implemented (`src/core/jaggd.c`, core option
|
||
`virtualjaguar_jgd`, savestate v8, `test/test_jgd.c`); the GD menu, SD/FAT
|
||
filesystem, GPU async reads, encrypted `.jgd` and `.MRQ` remain out of scope.
|
||
This document is the hardware/protocol spec of record for that
|
||
implementation, at the quality bar of `docs/memory-track.md`.
|
||
|
||
**Primary ground truth: RetroHQ's own published homebrew API**
|
||
(<https://github.com/RetroHQ/JagGD>, by SainT, the JagGD author). Unlike the
|
||
Memory Track, we did NOT have to reconstruct this interface from third-party
|
||
behavior — the cart-side 68K source (`gdbios_bindings.s`) is public and contains
|
||
the SPI register addresses, bit definitions, the full handshake protocol, and
|
||
the bank-switching semantics in SainT's own comments. Local copies of
|
||
everything cited live in the research scratchpad
|
||
(`.../scratchpad/jaggd-api/`, `.../scratchpad/jagstudio/`,
|
||
`.../scratchpad/bigpemu_disasm.txt`).
|
||
|
||
Confidence labels used throughout:
|
||
- **established** — stated in RetroHQ source (`gdbios_bindings.s` /
|
||
`gdbios.h` / `README.md`) or directly observable in shipped JagStudio code
|
||
(`RAPTORGD.O`, disassembled and verified identical to the bindings).
|
||
- **established-behavioral** — read out of the BigPEmu 1.2x arm64 binary
|
||
(`/Applications/BigPEmu.app/Wrapper/BigPEmu.app/BigPEmu`, disassembled) or
|
||
its changelog. Tells us what the ecosystem's reference emulator does, not
|
||
necessarily what silicon does.
|
||
- **inferred** — follows from the above but not directly stated.
|
||
- **guessed** — plausible, must be verified before relying on it.
|
||
|
||
---
|
||
|
||
## 1. Hardware model
|
||
|
||
The JagGD is a flash cart with (established, from the API source + product
|
||
materials):
|
||
|
||
- **16 MB of onboard SDRAM** presented to the Jaguar as cartridge ROM.
|
||
- An **ASIC/FPGA** that decodes the cart bus, maps SDRAM into the 6 MB cart
|
||
window, and exposes a small SPI-style mailbox register set.
|
||
- A **microcontroller** ("the GD", "the micro" in SainT's comments) behind
|
||
that SPI link. It owns the SD card, the FAT filesystem, serial numbers, the
|
||
menu, firmware, and it *serves the GDBIOS blob on request*.
|
||
|
||
Communication model: the 68K talks to the micro through three registers in
|
||
JERRY's **GPIO2** decode range. Cart EEPROM already works exactly this way
|
||
through GPIO0/GPIO1 ($F14800/$F15000, see `src/jerry/eeprom.c`), so the GPIO
|
||
chip selects demonstrably route to the cartridge connector; GPIO2 is the next
|
||
one up and is marked "reserved" in our own jerry.c address map (established by
|
||
analogy + the fact that the GD API uses these addresses from cart code).
|
||
|
||
## 2. Register map
|
||
|
||
All from `gdbios_bindings.s` (established) unless noted.
|
||
|
||
| Address | Name (RetroHQ) | Access | Meaning |
|
||
|---|---|---|---|
|
||
| `$F16002` | `ASIC_SPI_STATUS` | R/W 16-bit | Status + control, bits below |
|
||
| `$F16004` | `ASIC_SPI_DATA` | R/W 16-bit | Write = trigger one SPI byte exchange; word read = drain rx latch |
|
||
| `$F16005` | `ASIC_SPI_DATA_BYTE` | R 8-bit | Received byte (low byte of `$F16004`) |
|
||
|
||
`ASIC_SPI_STATUS` bits (established):
|
||
|
||
| Bit | Mask | Name | Direction | Meaning |
|
||
|---|---|---|---|---|
|
||
| 0 | `$0001` | `SLAVE_SELECT` | W | Assert slave select (ack packet start) |
|
||
| 3 | `$0008` | `HAVE_DATA` | R | Handshake bit; see GDWaitData below. Idle/ready state is **clear**; the slave raises it to ack/announce data |
|
||
| 4 | `$0010` | `PACKET_START` | W | Request packet start / block boundary ("process it, GD!") |
|
||
| 5 | `$0020` | `LATCH_FULL` | R | A received byte is sitting in the rx latch (must be drained before starting a transaction) |
|
||
| 15 | `$8000` | (busy) | R | Exchange in progress. All waits are `tst.w $F16002` / `bmi` loops, i.e. spin while bit 15 set |
|
||
|
||
Write of `$0000` to `$F16002` = end of packet / deselect (established — every
|
||
transaction ends with `clr.w ASIC_SPI_STATUS`).
|
||
|
||
**Data register semantics** (established from code shape; exact tx byte order
|
||
inferred): writing **any word** to `$F16004` clocks **one byte exchange**. The
|
||
transmitted byte is the **low byte of the written word** (inferred — see
|
||
`GDExchangeWord` trace below; the bulk-read loop writes its loop counter as a
|
||
don't-care dummy, so the written value is irrelevant when only receiving).
|
||
After bit 15 clears, the received byte is read at `$F16005`.
|
||
|
||
`GDExchangeWord` (send a 16-bit word, receive a 16-bit word — established):
|
||
|
||
```
|
||
move.w d0,$F16004 ; exchange byte #1
|
||
(wait not busy)
|
||
move.b $F16005,d0 ; rx1 -> low byte
|
||
ror.w #8,d0 ; rx1 -> high byte, old high byte -> low
|
||
move.w d0,$F16004 ; exchange byte #2 (tx = old high byte)
|
||
(wait not busy)
|
||
move.b $F16005,d0 ; result = rx1<<8 | rx2
|
||
```
|
||
|
||
`GDWaitData` (the packet handshake — established):
|
||
|
||
```
|
||
1. spin until HAVE_DATA (bit 3) == 0
|
||
2. write PACKET_START|SLAVE_SELECT ($0011) to $F16002
|
||
3. spin until HAVE_DATA == 1 ; slave ack
|
||
```
|
||
|
||
**Consequence for a machine with no GD attached:** step 3 never completes.
|
||
There is no timeout anywhere in the RetroHQ or JagStudio code. See §6.
|
||
|
||
## 3. Micro packet protocol
|
||
|
||
Transaction shape (established, from `_GD_HWVersion` and `_GD_Install`):
|
||
|
||
```
|
||
write $0010 to STATUS ; request packet start
|
||
GDWaitData ; handshake
|
||
GDExchangeWord(command) ; 16-bit command word
|
||
GDExchangeWord(param_size) ; 16-bit param byte count (0 if none)
|
||
[param bytes...]
|
||
write $0010 to STATUS ; "we're done, process it GD!"
|
||
--- response ---
|
||
GDWaitData
|
||
GDExchangeWord / byte loop ; response payload, format per command
|
||
write $0010 to STATUS ; after each ≤512-byte block
|
||
...
|
||
write $0000 to STATUS ; end of packet
|
||
```
|
||
|
||
Known command words (established):
|
||
|
||
| Cmd | Function | Response |
|
||
|---|---|---|
|
||
| `12` (`$0C`) | HW version | u32: high word = **firmware version, BCD** (e.g. `$0111` = v1.11), low word = ASIC version, BCD. Read as two `GDExchangeWord`s, high word first. After reading, the caller busy-waits ~500 dbra iterations "to let the micro finish up" |
|
||
| `$80` | Fetch GDBIOS | u16 blob size, then the blob streamed in ≤512-byte chunks, one `PACKET_START` write between chunks; each byte clocked out by a dummy word write to `$F16004` |
|
||
|
||
Before `$80`, the installer drains the rx latch: while `LATCH_FULL` set, read
|
||
`$F16004` (word) and re-check ("in case of FIFO DMA termination",
|
||
established).
|
||
|
||
All other micro commands (file I/O, serials, LED, reset, page set, …) are
|
||
issued **by the GDBIOS blob**, not by game code, and their SPI encodings are
|
||
**unknown** — and irrelevant for us, because the blob itself comes from the
|
||
emulated micro, i.e. *we get to supply our own blob* (see §7). BigPEmu
|
||
necessarily does the same thing (it cannot ship RetroHQ's copyrighted blob).
|
||
|
||
## 4. GDBIOS blob ABI (what games actually link against)
|
||
|
||
Games do not poke registers for anything except the two commands above. They
|
||
call `GD_Install(buffer)` (a 4 KB, long-aligned RAM buffer), which:
|
||
|
||
1. Calls HW version (cmd 12); **fails if firmware < `$111` (v1.11)** —
|
||
established, both in RetroHQ bindings (`MINVERSION`... actually the FW
|
||
gate) and JagStudio's `RAPTOR_GD_Init` (identical code, verified by
|
||
disassembling `RAPTORGD.O`).
|
||
2. Streams the blob (cmd `$80`) into the buffer, stores the pointer
|
||
(`GDB_Base`).
|
||
3. Checks blob version word ≥ `$100`, then calls blob function 1 (`GD_Init`).
|
||
|
||
Blob memory layout (established):
|
||
|
||
| Offset | Size | Meaning |
|
||
|---|---|---|
|
||
| `+0` | u16 | GDBIOS version, BCD, min `$100` |
|
||
| `+2` | u16 | Highest function number implemented |
|
||
| `+N*4` | 4 bytes | Entry point of function N (a 4-byte slot — enough for one `bra.w`/`jmp` stub) |
|
||
|
||
Call convention (established): `a6` = blob base; caller does
|
||
`cmp.w #N,2(a6)` / `blt fail` then `jsr N*4(a6)`. `d0-d1/a0-a1` scratch,
|
||
everything else callee-saved. Function numbers and register ABI:
|
||
|
||
| N | Name | Args in | Returns (d0) |
|
||
|---|---|---|---|
|
||
| 1 | `GD_Init` | — | — |
|
||
| 2 | `GD_InitGPURead` | a0=GPU RAM buffer (224 bytes), d0=flags (0 preserve / 1 fast) | — |
|
||
| 3 | `GD_BIOSVersion` | — | u16 BCD |
|
||
| 4 | `GD_ROMWriteEnable` | d0=1 enable / 0 disable | — |
|
||
| 5 | `GD_ROMSetPage` | d0 = page<<16 \| bank | — |
|
||
| 6 | `GD_ROMSetPages` | d0 = u32, one bank nibble per page, LSN = page 0 | — |
|
||
| 7 | `GD_GetCartSerial` | a0=16-byte buffer | 0 ok / !0 fail |
|
||
| 8 | `GD_GetCardSerial` | a0=16-byte buffer | 0 ok / !0 fail |
|
||
| 9 | `GD_CardIn` | — | 0 no / 1 yes |
|
||
| 10 | `GD_FileOpen` | a0=path, d0=mode | handle ≥0 / <0 fail |
|
||
| 11 | `GD_FileClose` | d0=handle | 0 / <0 |
|
||
| 12 | `GD_FileSeek` | d0 = flags<<16 \| handle, d1=offset | 0 / <0 |
|
||
| 13 | `GD_FileRead` | d0 = flags<<16 \| handle, a0=buffer, d1=size | 0 / <0 |
|
||
| 14 | `GD_FileWrite` | d0=handle, a0=buffer, d1=size | 0 / <0 |
|
||
| 15 | `GD_FileTell` | d0=handle | offset / $FFFFFFFF |
|
||
| 16 | `GD_FileSize` | d0=handle | size / $FFFFFFFF |
|
||
| 17 | `GD_FileAsyncPos` | — | pos |
|
||
| 18 | `GD_FileAsyncWait` | — | — |
|
||
| 19 | `GD_FileAsyncActive` | — | 0/1 |
|
||
| 20 | `GD_FileInfo` | a0=path, a1=buffer, d0=flags (bit0 = long name) | ≥0 / <0 |
|
||
| 21 | `GD_DirOpen` | a0=path | handle / <0 |
|
||
| 22 | `GD_DirRead` | d0 = flags<<16 \| handle, a0=buffer | ≥0 / <0 |
|
||
| 23 | `GD_DirClose` | d0=handle | 0 / <0 |
|
||
| 24 | `GD_Reset` | d0 = 0 normal / 1 to GD menu / 2 debug | — |
|
||
| 25 | `GD_SetLED` | d0=flags | — |
|
||
| 26 | `GD_DebugString` | a0=C string (→ virtual USB COM port) | — |
|
||
|
||
`GD_HWVersion` is **not** a blob function — it is raw SPI in the binding and
|
||
works pre-install.
|
||
|
||
## 5. Bank switching semantics
|
||
|
||
Verbatim from SainT's comments in `gdbios_bindings.s` (established):
|
||
|
||
```
|
||
ROMSetPage(u16 page, u16 bank)
|
||
Page 0 : $8xxxxx 1 : $9xxxxx 2 : $axxxxx
|
||
3 : $bxxxxx 4 : $cxxxxx 5 : $dxxxxx
|
||
Bank 0-15 is 1MB pages of the onboard 16MB SDRAM
|
||
|
||
ROMSetPages(u32 banks)
|
||
Set all SDRAM banks at once, one per nibble.
|
||
Lowest significant nibble is page 0, upto nibble 5. Data above is ignored.
|
||
```
|
||
|
||
So:
|
||
|
||
- **Granularity:** 1 MB. The 6 MB cart window ($800000–$DFFFFF) is six
|
||
independent 1 MB pages, each mappable to any of sixteen 1 MB banks of the
|
||
16 MB SDRAM. (established)
|
||
- **Window:** whole cart space. Note page 5 covers $Dxxxxx including the
|
||
$DFFF00 BUTCH-overlay tail — for emulation, keep our existing
|
||
$DFFF00–$DFFFFF handling above the banked read, exactly as today's dispatch
|
||
already orders it. (inferred, trivially safe)
|
||
- **Latch behavior:** immediate on the next bus access; a page register is
|
||
plain combinational address-translation state in the ASIC. No evidence of
|
||
any delay or double-buffering. (inferred)
|
||
- **Initial mapping:** identity (page N → bank N) so that a flat ≤6 MB image
|
||
looks like a normal cart, and a >6 MB image exposes its first 6 MB.
|
||
(inferred — required for anything to boot at all; BigPEmu boots >6 MB
|
||
images this way per its changelog)
|
||
- **Writes:** `GD_ROMWriteEnable(1)` makes the SDRAM-backed "ROM" writable
|
||
(the GD menu uses this to load; homebrew can use cart space as extra RAM).
|
||
Granularity global vs per-page unknown; assume global. (established that it
|
||
exists; granularity guessed)
|
||
- **How the real blob performs the switch is unknown** (SPI command to the
|
||
micro vs. a direct ASIC register). Deliberately irrelevant to us: games can
|
||
only reach banking through the blob ABI, and the emulator supplies the
|
||
blob. (established consequence)
|
||
|
||
## 6. GD detection — what a GD-locked title actually does
|
||
|
||
From JagStudio v1.11 (`RAPTORGD.O` disassembled; `buildfiles/template/rapapp.s`;
|
||
`projects/basic/jaguargd/`) — JagStudio is what real GD homebrew (e.g. SKYLAR)
|
||
is built with (established):
|
||
|
||
1. Projects opt in with `useGD=1`; startup then calls `RAPTOR_GD_Init`
|
||
**unconditionally, before DSP setup** ("MUST be before DSP setup").
|
||
2. `RAPTOR_GD_Init` = RetroHQ's `GD_Install` with a detect flag:
|
||
`raptor_GD_detect = 1`, run HW-version (cmd 12); if FW < `$111` →
|
||
`GDB_Base = 0`, `detect = -1`, return. Otherwise install blob + `GD_Init`.
|
||
3. Game code branches on `raptor_GD_detect == 1` ("GD Cart Detected") — and
|
||
every wrapped call re-checks `GDB_Base != 0` and the function-count word,
|
||
returning -1 gracefully when absent.
|
||
|
||
**The failure mode without a GD is a hang, not a refusal**: `GDWaitData`
|
||
step 3 spins forever waiting for `HAVE_DATA` to go high. On our core today,
|
||
`$F16002` reads fall into the `F14000–F1A0FF` catch-all in
|
||
`src/jerry/jerry.c` → `EepromReadWord` → returns `$0000` for non-EEPROM
|
||
offsets — bit 3 never rises, the 68K wedges in `.waitAck`, and `crash_detect`
|
||
will report `video_stall`. BigPEmu with JGD off behaves the same way (its GD
|
||
register special-case is gated on a JGD-enabled flag bit; disabled, the read
|
||
falls through to a flat memory array) — which is exactly why Batocera tells
|
||
users GD-locked games "won't boot" until they turn on Force JGD.
|
||
|
||
So the **detection contract we must implement** is precisely:
|
||
status handshake (bit 3 sequencing + bit 15 busy + bit 5 drain), command 12
|
||
returning FW ≥ `$111` (recommend `$0300`/`$0102`-style current-ish BCD values;
|
||
anything ≥ `$0111` works), and command `$80` serving a valid blob.
|
||
|
||
What we do *not* have to implement for detection: serial numbers can return
|
||
zeros-with-success (titles hard-locked to one physical cart serial are
|
||
distributed as encrypted `.jgd` files we can't load anyway — out of scope,
|
||
same as BigPEmu).
|
||
|
||
## 7. What BigPEmu does (established-behavioral)
|
||
|
||
From its changelog (mirrored at emunations.com) and the arm64 binary:
|
||
|
||
- v1.01 "Added JaguarGD bank switching support." v1.02 "Added an option to
|
||
force Jaguar GD emulation, which enables bank switching even in ROM images
|
||
smaller than 6MB" (config key `ForceJGD`; sits alongside `AttachButch`,
|
||
`AttachMT`, `ShareMT`, `CDSeekSpeed` in `BigPEmuConfig.System`). v1.19
|
||
"Added more Jaguar GD functionality. Filesystem functions are stubbed out…"
|
||
- Its JERRY byte/word read+write handlers special-case addresses ≥ `$F16002`
|
||
behind a JGD-enable flag. The status read is synthesized: `LATCH_FULL` is
|
||
set from a response-FIFO head≠tail comparison, and the read has
|
||
side effects on the stored status bits (read-sensitive handshake
|
||
emulation). `PACKET_START` writes raise a "process packet" flag consumed by
|
||
a command engine that fills the response FIFO. I.e. **BigPEmu emulates the
|
||
GD at the SPI-register level for detection/install**, matching §2–§3.
|
||
- GD *function* services are HLE'd host-side: its file-open paths read the
|
||
filename out of emulated RAM and call host filesystem/`wcslen` code — i.e.
|
||
the blob it serves is a thin trampoline into native handlers. (This is the
|
||
v1.19 "filesystem stubs" layer.)
|
||
- Auto-enable: banking turns on automatically for images ≥ 6 MB; `ForceJGD`
|
||
covers smaller GD-locked images. Our proposed core option
|
||
`virtualjaguar_jgd = disabled|auto|enabled` maps 1:1.
|
||
- `.MRQ` (GD box-art/metadata sidecar) support is pure UI, ignore.
|
||
|
||
## 8. Remaining unknowns, and the cheapest experiment for each
|
||
|
||
| Unknown | Impact | Cheapest resolution |
|
||
|---|---|---|
|
||
| Exact tx byte order of `$F16004` word writes (low-byte-out inferred) | None if we implement to satisfy the binding code shape (we are the only slave); matters only for exotic homebrew doing raw SPI | Write a 20-line probe ROM from the bindings, run under BigPEmu with its scripting/debugger tracing `$F16004`; or ask SainT on AtariAge |
|
||
| Full micro SPI command set beyond 12/`$80` | None for scope of #312 (blob is ours) | Dump the real blob: run `GD_Install` on real hardware and save the 4 KB buffer to SD with `GD_FileWrite` — a ~30-line JagStudio program; also answers the next row |
|
||
| What the real GDBIOS `ROMSetPage` pokes | None (same reason); would be nice for a hardware-faithful mode | Same blob dump, disassemble |
|
||
| Initial page map on boot (identity assumed) | Boot of >6 MB images | Run SKYLAR 1.0 in BigPEmu, break before first `ROMSetPage`, checksum the six pages against file offsets |
|
||
| `ROMWriteEnable` granularity (global assumed) | Writable-cart feature only | Blob dump / SainT |
|
||
| Stock-console (no GD) read value at `$F16002` | Only affects how faithfully "GD absent" hangs; our current `$0000` reproduces the observed hang class | Real-hardware probe; not needed for #312 |
|
||
| Whether any homebrew calls raw SPI beyond `GD_HWVersion` | Compatibility ceiling | Corpus scan for `$F160` word constants in ROM images once we have GD titles locally |
|
||
|
||
None of these block implementation.
|
||
|
||
## 9. Implementation sketch (mapped onto our code)
|
||
|
||
New `src/core/jaggd.c` + `jaggd.h` (mirror `memtrack.c` structure):
|
||
|
||
- **State:** `jgdEnabled` (from core option `virtualjaguar_jgd =
|
||
disabled|auto|enabled`; `auto` = on when image > `0x5FFF00` bytes),
|
||
`uint8_t jgdPage[6]` (init 0..5), full-image buffer `jgdROM` (≤16 MB;
|
||
`jaguarMainROM`/`jagMemSpace` cart region stays as-is for the non-GD path),
|
||
SPI engine: `status`, small state machine (IDLE → HANDSHAKE → CMD → LEN →
|
||
PARAMS → RESPONSE), response byte-FIFO, `rxLatch`.
|
||
- **Register hook:** in `src/jerry/jerry.c`, intercept `$F16000–$F16007`
|
||
in `JERRYReadByte/Word` and `JERRYWriteByte/Word` **before** the
|
||
`F14000–F1A0FF` EEPROM catch-all (which currently swallows these
|
||
addresses), delegating to `JGDControlRead/Write` when `jgdEnabled`.
|
||
Behavior per §2–§3: implement exactly the sequence the bindings perform;
|
||
respond to cmd 12 with FW `$0300` / ASIC `$0102` (any BCD ≥ `$0111` FW),
|
||
cmd `$80` with our blob; unknown commands → zero-filled responses.
|
||
- **The served blob:** ~200 bytes of hand-written 68K, embedded via bin2c
|
||
like `src/bios/*` (source checked in under `src/bios/` with its build
|
||
script, same pattern as the CD boot stub). Header `$0100`, func count 26.
|
||
Implemented: 1 (rts), 3 (version), 4 (write enable), 5/6 (page set),
|
||
7/8 (zero serial, return 0), 9 (return 0), 24 (write to reset backdoor —
|
||
or just rts initially), 25/26 (rts); file/dir/async functions return -1
|
||
(`moveq #-1,d0; rts` — BigPEmu also only "stubs" these; genuinely
|
||
implementing them against a host directory is possible later via the same
|
||
backdoor). Banking mechanism for the blob: functions 5/6 write the packed
|
||
bank value to a **backdoor register we define at `$F16006`** (word write,
|
||
inside the GPIO2 range we already own; nothing else decodes it). This
|
||
keeps the blob 68K-only and trivially small, and keeps all interpretation
|
||
in `jaggd.c`. `retro_deinit` must reset all of this (iOS static-state
|
||
rule).
|
||
- **Cart read path:** in `src/core/jaguar.c` `JaguarReadByte/Word/Long`
|
||
cart branches (and the DMA/`JaguarRead*`-alike helpers at lines ~659-690),
|
||
when `jgdBankingActive`:
|
||
`off = address - 0x800000; return jgdROM[((uint32_t)jgdPage[off >> 20] << 20) | (off & 0xFFFFF)]`
|
||
— one table lookup + shift, same cost class as the existing MEMTRACK
|
||
gate. Word/long reads that could straddle a 1 MB boundary: only possible
|
||
for the odd-address/boundary cases; handle by composing bytes (straddling
|
||
reads are already rare/degenerate on real carts). `GD_ROMWriteEnable`
|
||
makes the same translation apply in `JaguarWriteByte/Word` cart branches
|
||
(write into `jgdROM`).
|
||
- **Loader:** `src/core/file.c` `JaguarLoadROM`/callers currently cap at
|
||
the cart window; accept up to `0x1000000` (16 MB), copy first 6 MB into
|
||
`jaguarMainROM` for the flat path, keep full image in `jgdROM`, set
|
||
auto-enable. (Alignment/padding to 1 MB multiple.)
|
||
- **Savestate:** serialize `jgdEnabled(active)`, `jgdPage[6]`, SPI engine
|
||
state + FIFO; single version bump per release policy
|
||
(`docs/savestate-compat.md`). The 16 MB image itself is NOT serialized
|
||
(same as cart ROM today) — but pages written via `ROMWriteEnable` are
|
||
dirty state; simplest correct v1: serialize a dirty flag and, if any
|
||
write occurred, the full written-page set. (Defer: most titles won't
|
||
write.)
|
||
- **Memory map / RA:** `test/tools/test_memory_map.c` expects the current
|
||
descriptor layout; cart descriptor stays the 6 MB window (it describes the
|
||
68K address space, which is unchanged).
|
||
- **Out of scope** (match BigPEmu): GD menu, SD/FAT filesystem semantics,
|
||
`GD_InitGPURead` GPU async reads (function 2 = rts; `GD_FREAD_GPU` modes
|
||
are documented to not function without it, so returning -1 from FileRead
|
||
keeps the contract honest), `.jgd` encrypted images, `.MRQ` sidecars.
|
||
|
||
Suggested implementation order: (1) register hook + detection handshake +
|
||
blob install — gets "GD Cart Detected" on the JagStudio example; (2) banking
|
||
+ >6 MB loader — gets SKYLAR; (3) savestate + core option plumbing + tests.
|
||
|
||
## 9b. SD-card file API (#783), as implemented
|
||
|
||
Blob functions 9-16, 20-23 and 26 are answered from a **host folder acting as the SD card**. This supersedes the "file/dir functions return -1" line in §9.
|
||
|
||
- **The card folder:** `jaggd-sd` inside the frontend's save directory, or the system directory if there is none. It is created on first use.
|
||
- Core option `virtualjaguar_jgd_sd` sets access: Read/write (default), Read-only, or Disabled.
|
||
- Frontends that bypass `libretro.c` call `JGDFSSetRoot()` / `JGDFSSetAccess()`.
|
||
- **Call path:** each of those blob slots is one word write, `$C000 | function`, to the `$F16006` backdoor.
|
||
- The core services the call *during that write*: `JGDHostCall` in `jaggd.c` reads the 68K's d0/d1/a0/a1, and the result goes back 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 afterwards.
|
||
- Only a 68K write triggers it; GPU, DSP and blitter writes to `$F16006` are ignored.
|
||
- The call completes inside one write, so no half-finished call can land in a savestate.
|
||
- **Emulated memory:** paths and buffers move through `JaguarReadByte`/`JaguarWriteByte` as the 68K, so watchers see the writes.
|
||
- Paths are capped at 255 characters plus NUL; a path with no terminator fails.
|
||
- Buffer ranges are checked against the 24-bit bus.
|
||
- **The ABI, from RetroHQ's published `gdbios.h` / `gdbios_bindings.s`** (facts only; their files are not in this tree):
|
||
- **Open modes** are FatFs `FA_*`: READ `$01`, WRITE `$02`, CREATE_NEW `$04`, CREATE_ALWAYS `$08`, OPEN_ALWAYS `$10`, OPEN_APPEND `$30`. Any write or create flag needs a writable card (the FatFs rule).
|
||
- **FileRead:** `d0 = flags<<16 | handle`. Flags other than `GD_FREAD_CPU` (0) need `GD_InitGPURead`, which is not emulated, so they return -1. Returns 0 on success. A short read at end of file is success, as with FatFs `f_read`; the program finds the count with FileTell.
|
||
- **FileSeek:** `d0 = whence<<16 | handle` (SET/CUR/END = 0/1/2), `d1 = offset`.
|
||
- **FileWrite** returns 0 on success. **FileTell** and **FileSize** return `$FFFFFFFF` on failure.
|
||
- **FileInfo / DirRead records** are big-endian with no padding, matching FatFs `FILINFO`:
|
||
- offset 0: u32 size
|
||
- offset 4: u16 date `(year-1980)<<9 | month<<5 | day`
|
||
- offset 6: u16 time `hour<<11 | min<<5 | sec/2`
|
||
- offset 8: u8 attributes (`$10` dir, `$20` file, `$01` read-only card)
|
||
- offset 9: `char[13]` 8.3 name
|
||
- offset 22: `char[256]` long name, long form only
|
||
- Short record 22 bytes, long 278. The date/time comments in `gdbios.h` are swapped; this is the FatFs encoding.
|
||
- **End of directory:** not documented. DirRead returns -1 *and* writes a record with zeroed names, so a `>= 0` loop and a `name[0] != 0` loop both stop.
|
||
- **Errors:** every failure is the full 32-bit -1, so `.w` and `.l` sign tests both see it.
|
||
- **Sandbox:**
|
||
- Accepted: a leading `0:` drive, and both `/` and `\` as separators.
|
||
- Refused: `..`, any other drive letter, FAT-invalid characters, a file used as a directory, and (on POSIX hosts) any symlink under the root.
|
||
- Each component is matched case-insensitively, since FAT is.
|
||
- **Limits:**
|
||
- 8 open files and 4 open directories.
|
||
- The 8.3 alias of an over-long name is `BASENA~1.EXT`, FatFs's first alias; no collision numbering.
|
||
- On non-POSIX hosts the timestamp is fixed at 1980-01-01.
|
||
- **Run-ahead / netplay:** writes to the card are host side effects. A run-ahead replay of a positional write rewrites the same bytes, so it converges. A `CREATE_NEW` replayed after rollback finds the file already present and fails, so avoid run-ahead for software that creates files. Netplay peers each have their own card folder; differing contents desync.
|
||
- **Savestate:** see `docs/savestate-compat.md` v17 ("JGF1").
|
||
- **Still out of scope:** GPU and async reads (functions 2, 17-19), GD_Reset (24), and GD_SetLED (25).
|
||
|
||
## 10. Test cases
|
||
|
||
| Title | What it exercises | Local availability |
|
||
|---|---|---|
|
||
| JagStudio `jaguargd` example | `GD_Install`, detect flag, serials, LED, file I/O — prints "GD Cart Detected"/"NOT Detected" on screen | **Yes** — prebuilt `build/jaguargd.bin` (BJL/RAM-load format) in the scratchpad JagStudio tree; rebuild to `.rom` with JagStudio (Windows/Wine or the shipped `jaggd-x64` tooling) for a cart image |
|
||
| SKYLAR 1.0 preview (JagFest 2024) | The real thing: 16 MB image, live `ROMSetPage` banking + GD file access | No — download from the AtariAge thread ("SKYLAR 1.0 - 16MB BankSwitching GameDrive ROM", topic 371072; needs the SKYLAR directory on "SD card" — file calls will return -1 under our stub, so expect partial function: boot+banking yes, streamed assets no |
|
||
| Own probe ROM (recommended) | Deterministic harness ROM built with rmac + the RetroHQ bindings: install, `ROMSetPages`, checksum each page window, report via screen color / RAM flag readable by `harness_dlsym` | Build ourselves — best regression asset, no distribution problem, drop into `test/roms/` |
|
||
| GD-locked commercial homebrew | The ForceJGD path for <6 MB images | None in the local corpus (scanned `test/roms/private` — no GD titles, nothing >4 MB unpacked). Names of specific GD-locked retail homebrew were not pinned down by public sources during this research; Batocera/BigPEmu docs confirm the class exists without naming titles. Acquire via AtariAge when needed; the probe ROM covers the mechanism in the meantime |
|
||
|
||
The local corpus check: `test/roms/private/ROMS` is a classic-era set (139
|
||
retail entries + PD folder); no GD-aware or >6 MB images. The two >4 MB files
|
||
are 7z archives (Ultra Vortek, PD collection).
|
||
|
||
## 11. Source index (provenance)
|
||
|
||
- `github.com/RetroHQ/JagGD` — `gdbios.h`, `gdbios_bindings.s`, `README.md`
|
||
(SainT/RetroHQ, official). Registers, bits, protocol, blob ABI, page/bank
|
||
semantics, GPU-read notes. Cloned to scratchpad `jaggd-api/`.
|
||
- JagStudio v1.11 (`reboot-games.com/jagstudio/releases/jagstudio-v1.11.zip`)
|
||
— `buildfiles/raptor/RAPTORGD.O` (disassembled with capstone; byte-for-byte
|
||
the same logic as the bindings + `raptor_GD_detect`), `RAPTORGD.INC`,
|
||
`template/rapapp.s` (`useGD=1` init ordering), `projects/basic/jaguargd/`
|
||
example. Scratchpad `jagstudio/`.
|
||
- BigPEmu macOS arm64 binary — JERRY handler special-cases at
|
||
`0x1001d0a0c`/`0x1001d1f88`/`0x1001d22f8` (GD SPI status/data emulation,
|
||
gated on a mode flag), GD filesystem HLE cluster near `0x10014b150`
|
||
(host-side file ops on emulated-RAM filenames), config key block
|
||
(`ForceJGD` next to `AttachButch`/`AttachMT`). Disasm at scratchpad
|
||
`bigpemu_disasm.txt` (2M lines).
|
||
- BigPEmu changelog v1.01/v1.02/v1.19 via emunations.com mirror; BigPEmu user
|
||
manual (richwhitehouse.com) for MRQ note.
|
||
- AtariAge topic 371072 (SKYLAR 1.0) — existence/shape of a real 16 MB
|
||
banking title; JagStudio credit "Rik, CJ and SainT for implementing …
|
||
Bankswitching / GameDrive files … in JagStudio".
|
||
- Batocera wiki `systems:jaguar` — user-facing confirmation that GD-locked
|
||
games need Force JGD Emulation.
|
||
- This repo: `src/jerry/jerry.c` GPIO map comment (F16000-F16FFF = GPIO2
|
||
reserved; F14000-F1A0FF catch-all currently swallowing GD addresses via
|
||
`EepromReadByte` → `$0000`), `src/core/jaguar.c` cart dispatch + MEMTRACK
|
||
overlay pattern, `src/jerry/eeprom.c` GPIO0/1 precedent that GPIO decodes
|
||
reach the cart connector.
|