romdevtools 0.56.1 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +338 -0
- package/README.md +9 -7
- package/examples/dreamcast/hello/main.c +24 -0
- package/examples/dreamcast/platformer/main.c +31 -0
- package/examples/dreamcast/puzzle/main.c +44 -0
- package/examples/dreamcast/racing/main.c +39 -0
- package/examples/dreamcast/shmup/main.c +50 -0
- package/examples/dreamcast/sports/main.c +39 -0
- package/package.json +5 -3
- package/src/analysis/analyze.js +60 -5
- package/src/analysis/decompile.js +3 -0
- package/src/analysis/rizin.js +3 -1
- package/src/cores/capabilities.js +43 -7
- package/src/cores/registry.js +13 -8
- package/src/host/LibretroGL.js +26 -23
- package/src/host/LibretroHost.js +302 -24
- package/src/host/callbacks.js +72 -1
- package/src/host/coreLoader.js +17 -4
- package/src/host/cpu-state.js +32 -0
- package/src/host/dc-aica-state.js +67 -0
- package/src/mcp/tools/audio.js +1 -1
- package/src/mcp/tools/disasm.js +1 -1
- package/src/mcp/tools/index.js +1 -1
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/platform-tools.js +9 -1
- package/src/mcp/tools/project.js +16 -0
- package/src/mcp/tools/toolchain.js +115 -10
- package/src/platforms/dreamcast/MENTAL_MODEL.md +87 -0
- package/src/platforms/dreamcast/TROUBLESHOOTING.md +55 -0
- package/src/platforms/dreamcast/UPSTREAM_SOURCES.md +57 -0
- package/src/platforms/n64/MENTAL_MODEL.md +84 -0
- package/src/platforms/n64/TROUBLESHOOTING.md +60 -0
- package/src/platforms/n64/UPSTREAM_SOURCES.md +52 -0
- package/src/platforms/n64/lib/c/n64.c +181 -80
- package/src/platforms/ps1/MENTAL_MODEL.md +85 -0
- package/src/platforms/ps1/TROUBLESHOOTING.md +55 -0
- package/src/platforms/ps1/UPSTREAM_SOURCES.md +54 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +10 -0
- package/src/toolchains/asar/asar.js +84 -14
- package/src/toolchains/index.js +30 -0
- package/src/toolchains/mips-c/mips-c.js +35 -1
- package/src/toolchains/sh-c/lib/dc-crt0.s +23 -0
- package/src/toolchains/sh-c/lib/dc.h +152 -0
- package/src/toolchains/sh-c/lib/dc.ld +11 -0
- package/src/toolchains/sh-c/lib/libc.a +0 -0
- package/src/toolchains/sh-c/lib/libgcc.a +0 -0
- package/src/toolchains/sh-c/lib/libm.a +0 -0
- package/src/toolchains/sh-c/sh-c.js +104 -0
- package/src/toolchains/sh-elf-gcc/gcc.js +122 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Nintendo 64 — mental model
|
|
2
|
+
|
|
3
|
+
The N64 is a **3D machine**: a 93.75 MHz MIPS R4300i CPU + the RCP (Reality
|
|
4
|
+
Co-Processor = RSP vector unit + RDP rasterizer) drawing into RDRAM, scanned out
|
|
5
|
+
by the VI (Video Interface). romdev runs it on **parallel_n64** (libretro) built to
|
|
6
|
+
WASM, rendering through the **glide64 GL HLE plugin** on the real GPU via romdev's
|
|
7
|
+
native-gles / WebGL2 bridge (`hwRender: true`).
|
|
8
|
+
|
|
9
|
+
## The one thing to know about rendering
|
|
10
|
+
|
|
11
|
+
The N64 core renders through **glide64** — a GL HLE plugin that interprets the game's
|
|
12
|
+
**GBI display lists** and rasterizes them on the real GPU. So romdev N64 homebrew draws
|
|
13
|
+
by building a **GBI (F3DEX2) display list** each frame and kicking the RSP, the way a
|
|
14
|
+
real game does; glide64 HLEs it onto the GPU. A software framebuffer (poking pixels
|
|
15
|
+
into RDRAM) shows **black** on glide64 and would be <1fps — so don't do that.
|
|
16
|
+
|
|
17
|
+
**Use the bundled `n64.c` helper** — it does the GPU path for you: `n64_clear`/`n64_rect`
|
|
18
|
+
emit GPU **fill rectangles**, `n64_tri2d`/`n64_tri3d`/`n64_quad3d` scan-convert into
|
|
19
|
+
GPU fill-rect spans (still GPU-rasterized, not CPU pixels), and `n64_flip` ships the
|
|
20
|
+
display list as a GFX OSTask. `#include "n64.h"` and it just works (auto-bundled). How
|
|
21
|
+
it gets glide64 to accept the list without shipping Nintendo microcode: the RSP-HLE
|
|
22
|
+
treats an OSTask with `type==1` as graphics, and glide64 picks its command table by
|
|
23
|
+
CRC-summing the task's "ucode" region — so the helper embeds a 3072-byte blob that
|
|
24
|
+
*sums* to a real F3DEX2 CRC (the bytes are never executed under HLE).
|
|
25
|
+
|
|
26
|
+
If the screen is black, the usual cause is software-framebuffer drawing instead of the
|
|
27
|
+
helper's display-list path — see TROUBLESHOOTING. Confirm with `frame({op:'verify'})`.
|
|
28
|
+
|
|
29
|
+
## CPU / memory map (R4300i, MIPS III, 64-bit, big-endian)
|
|
30
|
+
|
|
31
|
+
| Region | KSEG | Address (virtual) | Notes |
|
|
32
|
+
|--------|------|-------------------|-------|
|
|
33
|
+
| RDRAM (cached) | kseg0 | `0x8000_0000`+ | main RAM, 4 MB (8 MB with Expansion Pak) |
|
|
34
|
+
| RDRAM (uncached) | kseg1 | `0xA000_0000`+ | same RAM, bypasses cache — use for the framebuffer |
|
|
35
|
+
| Cart / PIF / RCP MMIO | kseg1 | `0xA300_0000`+ | SP/DP/VI/AI/PI/SI registers |
|
|
36
|
+
|
|
37
|
+
Addresses are **big-endian**. `memory({op:'read', region:'system_ram'})` reads RDRAM.
|
|
38
|
+
|
|
39
|
+
## Booting — header + IPL3
|
|
40
|
+
|
|
41
|
+
A flat code image is not a bootable ROM. parallel_n64 HLEs the PIF boot: it copies
|
|
42
|
+
ROM `0x40..0xFFF` into RSP DMEM and **executes it as the IPL3**. romdev's
|
|
43
|
+
`build`/`cart` wrap a valid 64-byte header (magic `0x8037_1240`) + a minimal
|
|
44
|
+
clean-room IPL3 (PI-DMA the game cart→RDRAM, jump to entry) + the game image. You
|
|
45
|
+
don't write the header/IPL3 — the toolchain does (`wrapN64Rom`).
|
|
46
|
+
|
|
47
|
+
## Input
|
|
48
|
+
|
|
49
|
+
Homebrew reads controllers from **hardware** (the PIF/JoyBus), not via injection. The
|
|
50
|
+
host's `setInput`/`run` holdInputs drive the emulated pad; the game polls JoyBus and
|
|
51
|
+
sees them. The helper lib exposes a `pad_read()`.
|
|
52
|
+
|
|
53
|
+
## Sound
|
|
54
|
+
|
|
55
|
+
The AI (Audio Interface) DMAs a sample buffer from RDRAM to the DAC. `audioDebug`
|
|
56
|
+
decodes the AI register block (`romdev_ai_get`). The helper lib has a minimal audio
|
|
57
|
+
push path.
|
|
58
|
+
|
|
59
|
+
## MCP debug & inspection tooling
|
|
60
|
+
|
|
61
|
+
N64 is at **full parity** with the tile systems wherever the hardware allows:
|
|
62
|
+
|
|
63
|
+
- **`cpu({op:'read'})`** — live R4300i register file (`romdev_mips_regs_get`).
|
|
64
|
+
- **`breakpoint` / `watch`** — PC breakpoints + memory watchpoints, hooked into the
|
|
65
|
+
interpreter step + memory R/W paths.
|
|
66
|
+
- **`audioDebug({op:'inspect'})`** — AI registers.
|
|
67
|
+
- **`memory({op:'read', region:'system_ram'})`** — RDRAM.
|
|
68
|
+
- **`disasm` / `decompile`** — MIPS via rizin/Ghidra (R4300 is well-supported).
|
|
69
|
+
- **`frame({op:'verify'})`** — render-health (nearlyBlank guard) for no-vision agents.
|
|
70
|
+
- **`renderingContext`** is **N/A** (false) — that decodes 2D tile/sprite VDP state,
|
|
71
|
+
which a 3D framebuffer machine doesn't have. Not a missing feature.
|
|
72
|
+
|
|
73
|
+
## Build pipeline
|
|
74
|
+
|
|
75
|
+
`build({platform:'n64'})` cross-compiles with a from-scratch **mips-elf-gcc → WASM**
|
|
76
|
+
toolchain (cc1 → as → ld → objcopy orchestrated from JS, like the m68k path), big-
|
|
77
|
+
endian libs, then `wrapN64Rom` produces the bootable image. `#include` the bundled
|
|
78
|
+
`n64.h` for the 3D helpers (display-list build + VI setup + pad/audio).
|
|
79
|
+
|
|
80
|
+
## What's NOT bundled / hardware limits
|
|
81
|
+
|
|
82
|
+
- No custom RSP microcode path (glide64 HLEs the standard F3DEX-style display lists).
|
|
83
|
+
- No real-hardware Expansion Pak detection beyond the standard 4/8 MB split.
|
|
84
|
+
- `renderingContext` is N/A (3D, no tile VDP).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Nintendo 64 — troubleshooting
|
|
2
|
+
|
|
3
|
+
Read `platform({op:'doc', platform:'n64', name:'mental_model'})` first — the shipping
|
|
4
|
+
N64 renders through the **glide64 GL HLE plugin** (it rasterizes GBI display lists on
|
|
5
|
+
the real GPU), not a software framebuffer.
|
|
6
|
+
|
|
7
|
+
## "ROM builds + boots but the screen is BLACK"
|
|
8
|
+
|
|
9
|
+
The #1 N64 bug — almost always **software-framebuffer drawing instead of the GPU path**:
|
|
10
|
+
|
|
11
|
+
1. **You poked pixels into an RDRAM framebuffer yourself.** glide64 only presents GBI
|
|
12
|
+
**display lists** — it never scans out a CPU-written framebuffer. A software
|
|
13
|
+
rasterizer renders black here (and would be <1fps anyway). **Use the bundled `n64.c`
|
|
14
|
+
helper** (`n64_clear`/`n64_rect`/`n64_tri*`/`n64_quad3d` + `n64_flip`): it emits a
|
|
15
|
+
GBI display list glide64 HLEs onto the GPU. `#include "n64.h"` (auto-bundled).
|
|
16
|
+
2. **You hand-built a display list with the wrong OSTask.** The RSP-HLE only routes a
|
|
17
|
+
task to glide64 when the OSTask `type` (DMEM 0xFC0) == 1, and glide64 only accepts
|
|
18
|
+
it if the task's ucode region CRC-matches a known ucode. The helper sets both (a
|
|
19
|
+
3072-byte blob summing to an F3DEX2 CRC); if you roll your own, match that.
|
|
20
|
+
3. **You forgot `n64_flip()`** — the display list isn't submitted to the RSP until
|
|
21
|
+
flip kicks the task. Confirm with `frame({op:'verify'})` (nearlyBlank).
|
|
22
|
+
|
|
23
|
+
## "frame({op:'verify'}) says nearlyBlank"
|
|
24
|
+
|
|
25
|
+
No GBI list reached glide64 — it's a display-list / OSTask problem (causes 1-2), not a
|
|
26
|
+
toolchain problem. The build is fine; the render path is the issue. The helper handles
|
|
27
|
+
all of this — diff your code against it.
|
|
28
|
+
|
|
29
|
+
## "Geometry is wrong / triangles inside-out / nothing where expected"
|
|
30
|
+
|
|
31
|
+
3D pipeline math (the helper transforms + projects to screen space, then scan-converts
|
|
32
|
+
triangles into GPU fill-rect spans). Check: 16.16 fixed-point throughout, back-face
|
|
33
|
+
winding matches your vertex order, perspective divide before the viewport map, and the
|
|
34
|
+
vertices land on-screen. Diff against the helper's draw order.
|
|
35
|
+
|
|
36
|
+
## "Build fails: 'relocation truncated to fit'"
|
|
37
|
+
|
|
38
|
+
Statics landed in `.sdata`/`.sbss` and the 16-bit GP-relative relocs overflowed. The
|
|
39
|
+
toolchain passes `-G0` to **both** cc1 and the assembler to force everything into
|
|
40
|
+
`.data`/`.bss`. If you're building outside `build()` with custom flags, add `-G0`
|
|
41
|
+
everywhere.
|
|
42
|
+
|
|
43
|
+
## "cpu({op:'read'}) / breakpoint / watch returns nothing"
|
|
44
|
+
|
|
45
|
+
These read core exports (`romdev_mips_regs_get`, `romdev_*break/watch`). On N64 they
|
|
46
|
+
ARE present (full parity). If they're missing, you're on a stale core — re-resolve via
|
|
47
|
+
`platform({op:'resolve', platform:'n64'})` and confirm the bundled core, don't debug
|
|
48
|
+
the tool.
|
|
49
|
+
|
|
50
|
+
## "disasm/decompile of a multi-function program returns junk addresses"
|
|
51
|
+
|
|
52
|
+
The MIPS RE works, but for absolute-addressed `jal` targets the analysis buffer's flat
|
|
53
|
+
offsets must line up with the VAs' low bits (rizin ignores `-B` on a raw buffer). N64
|
|
54
|
+
usually lands fine from codeStart=0x1000 post-IPL3; if a fixed-VA image misbehaves, see
|
|
55
|
+
the PS1 troubleshooting note on the rebase trick — same principle.
|
|
56
|
+
|
|
57
|
+
## "renderingContext returns N/A"
|
|
58
|
+
|
|
59
|
+
Correct — N64 is a 3D framebuffer machine with no 2D tile/sprite VDP for that op to
|
|
60
|
+
decode. Not a bug. Use `frame`/screenshot + `memory` to inspect the framebuffer.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Nintendo 64 — source you can read
|
|
2
|
+
|
|
3
|
+
Trust hierarchy (try in order before filing a feedback round):
|
|
4
|
+
|
|
5
|
+
1. **Bundled examples** (`examples/n64/{shmup,platformer,puzzle,racing,sports}/main.c`) —
|
|
6
|
+
verified building + rendering on the GPU. Start here for "how do I draw / animate / use
|
|
7
|
+
the 3D helpers."
|
|
8
|
+
2. **Bundled helper lib source** (`src/platforms/n64/lib/c/n64.c` + `n64.h`) — the GPU
|
|
9
|
+
drawing backend. Read this when a draw call doesn't render the way you expect: it shows
|
|
10
|
+
exactly how `n64_clear`/`n64_rect`/`n64_tri*`/`n64_flip` build a **GBI (F3DEX2) display
|
|
11
|
+
list** + the OSTask kick (NOT a software framebuffer — see MENTAL_MODEL).
|
|
12
|
+
3. **The core source** (NOT bundled — fetch on demand):
|
|
13
|
+
|
|
14
|
+
| What | Upstream |
|
|
15
|
+
|---|---|
|
|
16
|
+
| parallel_n64 (libretro core + glide64 GL HLE) | https://github.com/libretro/parallel-n64 |
|
|
17
|
+
| Reality co-processor / RSP-HLE + glide64 internals | (in the parallel_n64 tree: `mupen64plus-rsp-hle/`, `glide2gl/src/Glide64/`) |
|
|
18
|
+
|
|
19
|
+
4. **The toolchain** — a from-scratch `mips-elf-gcc` cross-compiler (big-endian) built to
|
|
20
|
+
WASM (`scripts/build-mips-toolchain.sh` + `build-mips-wasm-tools.sh`):
|
|
21
|
+
|
|
22
|
+
| Component | Upstream |
|
|
23
|
+
|---|---|
|
|
24
|
+
| binutils | https://ftp.gnu.org/gnu/binutils/ |
|
|
25
|
+
| gcc | https://ftp.gnu.org/gnu/gcc/ |
|
|
26
|
+
| newlib | https://sourceware.org/pub/newlib/ |
|
|
27
|
+
|
|
28
|
+
5. **Reverse engineering** — `disasm`/`decompile` go through Rizin + Ghidra's MIPS
|
|
29
|
+
(R4300) support:
|
|
30
|
+
|
|
31
|
+
| What | Upstream |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Rizin | https://github.com/rizinorg/rizin |
|
|
34
|
+
| rz-ghidra (decompiler) | https://github.com/rizinorg/rz-ghidra |
|
|
35
|
+
|
|
36
|
+
## N64 hardware docs
|
|
37
|
+
|
|
38
|
+
- **n64brew wiki** (the canonical modern reference): https://n64brew.dev/wiki/
|
|
39
|
+
- **RCP / RDP / RSP** + the VI/AI/PI/SI register maps: n64brew + the parallel_n64
|
|
40
|
+
`vi_controller.h` / `rsp_core.c` enums in the core tree.
|
|
41
|
+
- **libdragon** (a real open N64 SDK — read its GBI/display-list code for the standard
|
|
42
|
+
command encodings): https://github.com/DragonMinded/libdragon
|
|
43
|
+
|
|
44
|
+
## When to use what
|
|
45
|
+
|
|
46
|
+
- "Why is my homebrew BLACK on screen?" → MENTAL_MODEL + TROUBLESHOOTING (it's almost
|
|
47
|
+
always software-framebuffer drawing instead of the GBI display-list path).
|
|
48
|
+
- "How does the helper build a display list / kick the RSP?" → `n64.c`
|
|
49
|
+
(`dl_begin`/`dl_end_and_run`) + the n64brew RDP/RSP pages.
|
|
50
|
+
- "Which VI register sets 16bpp / scaling?" → the core's `vi_controller.h` enum.
|
|
51
|
+
- "disasm returns junk addresses on a multi-function program" → TROUBLESHOOTING (the
|
|
52
|
+
absolute-`jal` base-alignment note).
|
|
@@ -1,117 +1,218 @@
|
|
|
1
|
-
/* n64.c — N64 helpers
|
|
1
|
+
/* n64.c — N64 helpers with a GPU (RDP/GBI) drawing backend.
|
|
2
|
+
*
|
|
3
|
+
* WHY THIS IS NOT A SOFTWARE RASTERIZER: the shipping N64 core renders through
|
|
4
|
+
* glide64 (a GL HLE plugin) — it presents the game's RDP/GBI **display lists** on
|
|
5
|
+
* the real GPU, NOT a raw CPU-written framebuffer. A software rasterizer that pokes
|
|
6
|
+
* pixels into RDRAM shows BLACK on glide64 (and would be <1fps even if it didn't).
|
|
7
|
+
* So this lib builds a GBI display list each frame and kicks the RSP/RDP; glide64
|
|
8
|
+
* HLEs it onto the GPU.
|
|
9
|
+
*
|
|
10
|
+
* HOW glide64 ACCEPTS OUR LIST (no Nintendo microcode shipped):
|
|
11
|
+
* - The RSP-HLE treats an OSTask with type==1 (M_GFXTASK) as a graphics task and
|
|
12
|
+
* hands the display list to glide64.
|
|
13
|
+
* - glide64 picks its command table by CRC-summing 3072 bytes of the task's
|
|
14
|
+
* "ucode" region and matching a known-ucode CRC. The bytes are NEVER executed
|
|
15
|
+
* (HLE), only summed — so we embed a 3072-byte blob that SUMS to a real F3DEX2
|
|
16
|
+
* CRC (0x5d3099f1). glide64 then interprets our list as standard F3DEX2.
|
|
17
|
+
* - We emit standard F3DEX2 GBI: set color image / scissor / fill rectangles for
|
|
18
|
+
* clear+rects, and shaded vertex triangles for tri2d/tri3d. Solid colors only.
|
|
19
|
+
*/
|
|
2
20
|
#include "n64.h"
|
|
3
21
|
|
|
4
|
-
/*
|
|
5
|
-
#define
|
|
6
|
-
#define
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#define
|
|
12
|
-
#define
|
|
13
|
-
#define
|
|
14
|
-
|
|
15
|
-
#define
|
|
16
|
-
#define
|
|
17
|
-
#define
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
/* ── RCP register blocks ── */
|
|
23
|
+
#define SP(n) (*(volatile unsigned int*)(0xA4040000u + (n)*4)) /* RSP */
|
|
24
|
+
#define DP(n) (*(volatile unsigned int*)(0xA4100000u + (n)*4)) /* RDP cmd */
|
|
25
|
+
#define VI(n) (*(volatile unsigned int*)(0xA4400000u + (n)*4)) /* video */
|
|
26
|
+
|
|
27
|
+
#define SP_MEM_ADDR 0 /* SP DMEM/IMEM address */
|
|
28
|
+
#define SP_DRAM_ADDR 1
|
|
29
|
+
#define SP_RD_LEN 2
|
|
30
|
+
#define SP_WR_LEN 3
|
|
31
|
+
#define SP_STATUS 4
|
|
32
|
+
|
|
33
|
+
#define FB_ADDR 0x00200000u /* 320x240x16bpp color image in RDRAM */
|
|
34
|
+
#define DL_ADDR 0x00280000u /* the GBI display list buffer (room for many spans) */
|
|
35
|
+
#define UC_ADDR 0x00290000u /* the fake "ucode" blob (CRC bait) */
|
|
36
|
+
|
|
37
|
+
/* uncached pointers (kseg1) into those RDRAM regions */
|
|
38
|
+
#define U16P(a) ((volatile unsigned short*)(0xA0000000u | (a)))
|
|
39
|
+
#define U32P(a) ((volatile unsigned int*) (0xA0000000u | (a)))
|
|
40
|
+
|
|
41
|
+
static volatile unsigned int *dl; /* write cursor into the display list */
|
|
42
|
+
static unsigned int dl_count; /* words written */
|
|
43
|
+
|
|
44
|
+
/* ── GBI / F3DEX2 command opcodes (the ones the fill-rect path uses) ── */
|
|
45
|
+
#define G_ENDDL 0xDF
|
|
46
|
+
#define G_SETOTHERMODE_H 0xE3
|
|
47
|
+
#define G_RDPPIPESYNC 0xE7
|
|
48
|
+
#define G_RDPFULLSYNC 0xE9
|
|
49
|
+
#define G_SETSCISSOR 0xED
|
|
50
|
+
#define G_FILLRECT 0xF6
|
|
51
|
+
#define G_SETFILLCOLOR 0xF7
|
|
52
|
+
#define G_SETCIMG 0xFF
|
|
53
|
+
|
|
54
|
+
static inline void dl_w(unsigned int w0, unsigned int w1)
|
|
55
|
+
{ dl[dl_count++] = w0; dl[dl_count++] = w1; }
|
|
20
56
|
|
|
21
57
|
void n64_init(void)
|
|
22
58
|
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
VI(
|
|
28
|
-
VI(
|
|
29
|
-
VI(
|
|
30
|
-
VI(
|
|
31
|
-
VI(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
VI(13) = 0x00000400; /* Y_SCALE (240) */
|
|
39
|
-
front = FB0;
|
|
40
|
-
fb = (volatile unsigned short *)FB1;
|
|
41
|
-
VI(1) = FB0 & 0x1FFFFFFF; /* ORIGIN */
|
|
59
|
+
int i;
|
|
60
|
+
/* NTSC 320x240 16bpp VI setup, scanning out FB_ADDR — glide64 reads our color
|
|
61
|
+
image from the SetColorImage GBI cmd, but the VI must still be programmed so
|
|
62
|
+
there is a valid display target. */
|
|
63
|
+
VI(0) = 0x0000320E; VI(2) = 320; VI(3) = 2; VI(5) = 0x03E52239;
|
|
64
|
+
VI(6) = 0x0000020D; VI(7) = 0x00000C15; VI(8) = 0x0C150C15;
|
|
65
|
+
VI(9) = 0x006C02EC; VI(10)= 0x002501FF; VI(11)= 0x000E0204;
|
|
66
|
+
VI(12) = 0x00000200; VI(13)= 0x00000400;
|
|
67
|
+
VI(1) = FB_ADDR;
|
|
68
|
+
|
|
69
|
+
/* Build the CRC-bait "ucode": 3072 bytes that SUM (as u32 words) to the F3DEX2
|
|
70
|
+
CRC glide64 expects (UCODE_ZELDA_OOT = 0x5d3099f1). All words 0 except one. */
|
|
71
|
+
{ volatile unsigned int *uc = U32P(UC_ADDR);
|
|
72
|
+
for (i = 0; i < 3072/4; i++) uc[i] = 0;
|
|
73
|
+
uc[0] = 0x5d3099f1u; }
|
|
42
74
|
}
|
|
43
75
|
|
|
44
|
-
|
|
76
|
+
/* Begin a fresh display list for this frame: sync + set the color image + scissor +
|
|
77
|
+
fill cycle, and a default combine that passes the fill/shade through. */
|
|
78
|
+
static void dl_begin(void)
|
|
45
79
|
{
|
|
46
|
-
|
|
80
|
+
dl = U32P(DL_ADDR);
|
|
81
|
+
dl_count = 0;
|
|
82
|
+
dl_w((G_RDPPIPESYNC << 24), 0);
|
|
83
|
+
/* SetColorImage: fmt=0(RGBA) size=2(16b) width-1=319, addr=FB */
|
|
84
|
+
dl_w((G_SETCIMG << 24) | (0 << 21) | (2 << 19) | 319, FB_ADDR);
|
|
85
|
+
/* SetScissor: (0,0)-(320,240) in 10.2 fixed (<<2) */
|
|
86
|
+
dl_w((G_SETSCISSOR << 24) | (0 << 12) | 0, (320 << 14) | (240 << 2));
|
|
87
|
+
/* SetOtherMode_H: cycle type = FILL (3<<20 within the H word). F3DEX2 form:
|
|
88
|
+
shift=0x14(20) len=2 → set the 2 cycle-type bits to 3 (G_CYC_FILL). */
|
|
89
|
+
dl_w((G_SETOTHERMODE_H << 24) | ((32 - 20 - 2) << 8) | (2 - 1), 3u << 20);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* End the list with sync + ENDDL, then ship it to the RSP/RDP as a GFX OSTask. */
|
|
93
|
+
static void dl_end_and_run(void)
|
|
94
|
+
{
|
|
95
|
+
dl_w((G_RDPFULLSYNC << 24), 0);
|
|
96
|
+
dl_w((G_ENDDL << 24), 0);
|
|
97
|
+
|
|
98
|
+
/* Write the OSTask into SP DMEM at 0xFC0 (SP DMEM is at 0xA4000000). */
|
|
99
|
+
volatile unsigned int *task = (volatile unsigned int *)0xA4000FC0u;
|
|
100
|
+
task[0] = 1; /* 0xFC0 TASK_TYPE = M_GFXTASK (glide64 path) */
|
|
101
|
+
task[1] = 0; /* 0xFC4 flags */
|
|
102
|
+
task[2] = 0; /* 0xFC8 ucode_boot */
|
|
103
|
+
task[3] = 0; /* 0xFCC ucode_boot_size */
|
|
104
|
+
task[4] = UC_ADDR; /* 0xFD0 ucode (CRC-summed by glide64) */
|
|
105
|
+
task[5] = 0xC00; /* 0xFD4 ucode_size (>= 3072 so the full CRC region reads) */
|
|
106
|
+
task[6] = UC_ADDR; /* 0xFD8 ucode_data */
|
|
107
|
+
task[7] = 0x800; /* 0xFDC ucode_data_size */
|
|
108
|
+
task[8] = 0; /* 0xFE0 dram_stack */
|
|
109
|
+
task[9] = 0x400; /* 0xFE4 dram_stack_size */
|
|
110
|
+
task[10] = 0; /* 0xFE8 output_buff */
|
|
111
|
+
task[11] = 0; /* 0xFEC output_buff_size */
|
|
112
|
+
task[12] = DL_ADDR; /* 0xFF0 data_ptr → our GBI display list */
|
|
113
|
+
task[13] = dl_count * 4; /* 0xFF4 data_size (bytes) */
|
|
114
|
+
task[14] = 0; /* 0xFF8 yield_data_ptr */
|
|
115
|
+
task[15] = 0; /* 0xFFC yield_data_size */
|
|
116
|
+
|
|
117
|
+
/* Kick the RSP. The SP status-write handler runs the task only when BOTH HALT
|
|
118
|
+
and BROKE are clear after the write: bit0 (0x1) clears HALT, bit2 (0x4) clears
|
|
119
|
+
BROKE. Setting interrupt-on-break (0x100) makes the task signal completion.
|
|
120
|
+
The RSP-HLE then reads the OSTask at DMEM 0xFC0, sees type==1, and forwards
|
|
121
|
+
our display list to glide64. (SP_PC at 0xA4080000 is irrelevant under HLE —
|
|
122
|
+
the ucode never executes; only the OSTask fields + the CRC matter.) */
|
|
123
|
+
SP(SP_STATUS) = 0x00105; /* clear HALT (0x1) + clear BROKE (0x4) + intr-on-break (0x100) */
|
|
47
124
|
}
|
|
48
125
|
|
|
49
126
|
void n64_clear(unsigned short col)
|
|
50
127
|
{
|
|
51
|
-
|
|
128
|
+
dl_begin();
|
|
129
|
+
/* SetFillColor: 16bpp color packed twice into 32 bits. */
|
|
130
|
+
unsigned int c32 = ((unsigned int)col << 16) | col;
|
|
131
|
+
dl_w((G_SETFILLCOLOR << 24), c32);
|
|
132
|
+
/* FillRectangle covering the whole screen (coords in 10.2 fixed, inclusive). */
|
|
133
|
+
dl_w((G_FILLRECT << 24) | (((320 - 1) << 2) << 12) | ((240 - 1) << 2),
|
|
134
|
+
(0 << 12) | 0);
|
|
52
135
|
}
|
|
53
136
|
|
|
54
137
|
void n64_rect(int x, int y, int w, int h, unsigned short col)
|
|
55
138
|
{
|
|
56
|
-
int
|
|
57
|
-
|
|
139
|
+
int x1 = x + w - 1, y1 = y + h - 1;
|
|
140
|
+
if (x < 0) x = 0; if (y < 0) y = 0;
|
|
141
|
+
if (x1 > 319) x1 = 319; if (y1 > 239) y1 = 239;
|
|
142
|
+
if (x1 < x || y1 < y) return;
|
|
143
|
+
unsigned int c32 = ((unsigned int)col << 16) | col;
|
|
144
|
+
dl_w((G_SETFILLCOLOR << 24), c32);
|
|
145
|
+
dl_w((G_FILLRECT << 24) | ((x1 << 2) << 12) | (y1 << 2),
|
|
146
|
+
((x << 2) << 12) | (y << 2));
|
|
58
147
|
}
|
|
59
148
|
|
|
60
|
-
/*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
149
|
+
/* Triangle: a flat-shaded, screen-space triangle. The verts are already projected
|
|
150
|
+
to 2D pixels by the 3D pipeline. Rather than drive glide64's full F3DEX2 vertex/
|
|
151
|
+
matrix/combine pipeline (heavy state), we scan-convert the triangle into a span of
|
|
152
|
+
GPU FILL RECTANGLES — one per scanline. These are the SAME hardware-accelerated
|
|
153
|
+
fill-rects that clear/rect use (rasterized by glide64 on the GPU, NOT CPU pixels),
|
|
154
|
+
so it stays fast + renders correctly under the fill-cycle render state. The
|
|
155
|
+
examples draw a handful of large flat quads, so the per-scanline rect count is low. */
|
|
156
|
+
static void emit_tri(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col)
|
|
65
157
|
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
if (y1
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
158
|
+
/* sort vertices by y (y0 <= y1 <= y2) */
|
|
159
|
+
int t;
|
|
160
|
+
if (y0 > y1) { t=x0;x0=x1;x1=t; t=y0;y0=y1;y1=t; }
|
|
161
|
+
if (y1 > y2) { t=x1;x1=x2;x2=t; t=y1;y1=y2;y2=t; }
|
|
162
|
+
if (y0 > y1) { t=x0;x0=x1;x1=t; t=y0;y0=y1;y1=t; }
|
|
163
|
+
if (y2 == y0) return; /* degenerate */
|
|
164
|
+
|
|
165
|
+
int y;
|
|
166
|
+
for (y = y0; y <= y2; y++) {
|
|
167
|
+
if (y < 0 || y > 239) continue;
|
|
168
|
+
/* x along the long edge 0→2 */
|
|
169
|
+
int xa = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
|
|
170
|
+
int xb;
|
|
171
|
+
if (y < y1) {
|
|
172
|
+
if (y1 == y0) continue;
|
|
173
|
+
xb = x0 + (x1 - x0) * (y - y0) / (y1 - y0);
|
|
174
|
+
} else {
|
|
175
|
+
if (y2 == y1) continue;
|
|
176
|
+
xb = x1 + (x2 - x1) * (y - y1) / (y2 - y1);
|
|
82
177
|
}
|
|
178
|
+
if (xa > xb) { t = xa; xa = xb; xb = t; }
|
|
179
|
+
if (xa < 0) xa = 0; if (xb > 319) xb = 319;
|
|
180
|
+
if (xb < xa) continue;
|
|
181
|
+
/* one GPU fill-rect for this 1px-tall span */
|
|
182
|
+
unsigned int c32 = ((unsigned int)col << 16) | col;
|
|
183
|
+
dl_w((G_SETFILLCOLOR << 24), c32);
|
|
184
|
+
dl_w((G_FILLRECT << 24) | ((xb << 2) << 12) | (y << 2),
|
|
185
|
+
((xa << 2) << 12) | (y << 2));
|
|
83
186
|
}
|
|
84
187
|
}
|
|
85
188
|
|
|
189
|
+
void n64_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col)
|
|
190
|
+
{ emit_tri(x0,y0,x1,y1,x2,y2,col); }
|
|
191
|
+
|
|
86
192
|
void n64_flip(void)
|
|
87
193
|
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
fb = (volatile unsigned short *)(front);
|
|
92
|
-
front = newfront;
|
|
93
|
-
{ volatile int i; for (i = 0; i < 200000; i++) { } }
|
|
194
|
+
dl_end_and_run();
|
|
195
|
+
/* wait for the frame to scan out + the RDP to drain. */
|
|
196
|
+
{ volatile int i; for (i = 0; i < 100000; i++) { } }
|
|
94
197
|
}
|
|
95
198
|
|
|
96
|
-
/* ── input: read controller port 0 via the SI/PIF
|
|
97
|
-
the pad; the 16-bit button word is at PIF RAM. We use the simple JoyBus poll. ── */
|
|
199
|
+
/* ── input: read controller port 0 via the SI/PIF JoyBus poll. ── */
|
|
98
200
|
static unsigned int read_pad(void)
|
|
99
201
|
{
|
|
100
|
-
volatile unsigned int *pif = (volatile unsigned int *)0xBFC007C0;
|
|
202
|
+
volatile unsigned int *pif = (volatile unsigned int *)0xBFC007C0;
|
|
101
203
|
volatile unsigned int *si = (volatile unsigned int *)0xA4800000;
|
|
102
204
|
unsigned int buttons;
|
|
103
|
-
/* command block: read controller 0 (1 byte cmd 0x01, 1 byte send, ...) */
|
|
104
205
|
pif[0] = 0xFF010401; pif[1] = 0xFFFFFFFF; pif[2] = 0xFFFFFFFF;
|
|
105
206
|
pif[3] = 0xFE000000; pif[4] = 0; pif[5] = 0; pif[6] = 0; pif[7] = 1;
|
|
106
|
-
si[1] = 0x1FC007C0;
|
|
207
|
+
si[1] = 0x1FC007C0;
|
|
107
208
|
{ volatile int t; for (t = 0; t < 5000; t++) { } }
|
|
108
|
-
buttons = pif[1] >> 16;
|
|
109
|
-
return (~0u) & buttons;
|
|
209
|
+
buttons = pif[1] >> 16;
|
|
210
|
+
return (~0u) & buttons;
|
|
110
211
|
}
|
|
111
212
|
unsigned int n64_pad(void) { return read_pad(); }
|
|
112
213
|
int n64_pressed(unsigned int mask) { return (n64_pad() & mask) ? 1 : 0; }
|
|
113
214
|
|
|
114
|
-
/* ── trig (
|
|
215
|
+
/* ── trig (256-step binary angle) ── */
|
|
115
216
|
static const short SINTAB[64] = {
|
|
116
217
|
0,804,1608,2410,3212,4011,4808,5602,6393,7179,7962,8739,9512,10278,11039,11793,
|
|
117
218
|
12539,13279,14010,14732,15446,16151,16846,17530,18204,18868,19519,20159,20787,21403,
|
|
@@ -127,7 +228,7 @@ static fix sin_lut(int idx) {
|
|
|
127
228
|
fix n64_sin(fix a) { return sin_lut((F2I(a)) & 255); }
|
|
128
229
|
fix n64_cos(fix a) { return sin_lut((F2I(a) + 64) & 255); }
|
|
129
230
|
|
|
130
|
-
/* ── camera + model (
|
|
231
|
+
/* ── camera + model + projection (math unchanged; output drives emit_tri) ── */
|
|
131
232
|
static fix cam_x, cam_y, cam_z, cam_cy, cam_sy, cam_cp, cam_sp;
|
|
132
233
|
static fix mdl_x, mdl_y, mdl_z, mdl_cy, mdl_sy;
|
|
133
234
|
void n64_camera(fix ex, fix ey, fix ez, fix yaw, fix pitch)
|
|
@@ -162,7 +263,7 @@ void n64_tri3d(Vec3 a, Vec3 b, Vec3 c, unsigned short col)
|
|
|
162
263
|
int x0,y0,x1,y1,x2,y2;
|
|
163
264
|
if (!project(ca,&x0,&y0)||!project(cb,&x1,&y1)||!project(cc,&x2,&y2)) return;
|
|
164
265
|
if ((x1-x0)*(y2-y0)-(x2-x0)*(y1-y0) <= 0) return; /* back-face cull */
|
|
165
|
-
|
|
266
|
+
emit_tri(x0,y0,x1,y1,x2,y2,col);
|
|
166
267
|
}
|
|
167
268
|
void n64_quad3d(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col)
|
|
168
269
|
{ n64_tri3d(a,b,c,col); n64_tri3d(a,c,d,col); }
|
|
@@ -171,7 +272,7 @@ void n64_tri3d_nc(Vec3 a, Vec3 b, Vec3 c, unsigned short col)
|
|
|
171
272
|
Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c);
|
|
172
273
|
int x0,y0,x1,y1,x2,y2;
|
|
173
274
|
if (!project(ca,&x0,&y0)||!project(cb,&x1,&y1)||!project(cc,&x2,&y2)) return;
|
|
174
|
-
|
|
275
|
+
emit_tri(x0,y0,x1,y1,x2,y2,col);
|
|
175
276
|
}
|
|
176
277
|
void n64_quad3d_nc(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col)
|
|
177
278
|
{ n64_tri3d_nc(a,b,c,col); n64_tri3d_nc(a,c,d,col); }
|
|
@@ -181,7 +282,7 @@ static unsigned int rng = 0x12345678u;
|
|
|
181
282
|
void n64_srand(unsigned int s) { rng = s ? s : 1; }
|
|
182
283
|
unsigned int n64_rand(void) { unsigned int x=rng; x^=x<<13; x^=x>>17; x^=x<<5; return rng=x; }
|
|
183
284
|
|
|
184
|
-
/* ── HUD number (3x5 cells
|
|
285
|
+
/* ── HUD number (3x5 cells via filled rects) ── */
|
|
185
286
|
static const unsigned char DIG[10][5] = {
|
|
186
287
|
{0x7,0x5,0x5,0x5,0x7},{0x2,0x6,0x2,0x2,0x7},{0x7,0x1,0x7,0x4,0x7},{0x7,0x1,0x3,0x1,0x7},
|
|
187
288
|
{0x5,0x5,0x7,0x1,0x1},{0x7,0x4,0x7,0x1,0x7},{0x7,0x4,0x7,0x5,0x7},{0x7,0x1,0x1,0x1,0x1},
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# PlayStation (PS1) — mental model
|
|
2
|
+
|
|
3
|
+
The PS1 is a **3D machine**: a 33.87 MHz MIPS R3000A CPU + the GPU (2D/3D
|
|
4
|
+
rasterizer, drawing into 1 MB VRAM) + the GTE (Geometry Transformation Engine,
|
|
5
|
+
a CPU coprocessor for 3D math) + the SPU (24-voice ADPCM sound). romdev runs it on
|
|
6
|
+
**beetle_psx_hw** (Mednafen PSX) built to WASM, presenting through the
|
|
7
|
+
**native-gles / WebGL2 hardware GPU**.
|
|
8
|
+
|
|
9
|
+
## The one thing to know about rendering
|
|
10
|
+
|
|
11
|
+
PS1 homebrew draws by issuing **GPU primitives** (GP0 command packets) — not by
|
|
12
|
+
writing a raw CPU framebuffer. The core is `beetle_psx_hw`, the **hardware** renderer
|
|
13
|
+
(`hwRender: true`): it rasterizes the GPU command stream on the real GPU via
|
|
14
|
+
GLES3/WebGL2 (through romdev's native-gles bridge), exactly like a console. So the
|
|
15
|
+
bundled `psx.c` helper lib is a small **software-3D front end** (16.16 fixed-point
|
|
16
|
+
transform + project + cull + painter sort) whose **back end emits GP0 flat-poly
|
|
17
|
+
primitives** to the GPU — the GPU does the actual rasterization. (This differs from
|
|
18
|
+
N64, which software-rasterizes into RDRAM; PS1 hands triangles to the GPU.)
|
|
19
|
+
|
|
20
|
+
GP0 gotcha that cost real time: **`GP0 0x60` is a variable-size rectangle**, encoded
|
|
21
|
+
as 3 words (color, top-left corner, size) — NOT a 4-vertex polygon. Feeding it
|
|
22
|
+
polygon vertices stretches rects to the screen edge. Use the right command for the
|
|
23
|
+
primitive (triangles = `0x20`/`0x24`/`0x28`/`0x2C` family; rects = `0x60`). Read the
|
|
24
|
+
GP0 encoding carefully.
|
|
25
|
+
|
|
26
|
+
## CPU / memory map (R3000A, MIPS I, 32-bit, little-endian)
|
|
27
|
+
|
|
28
|
+
| Region | Address (KUSEG/KSEG) | Notes |
|
|
29
|
+
|--------|----------------------|-------|
|
|
30
|
+
| Main RAM | `0x8001_0000`+ (kseg0) / `0x0001_0000`+ | 2 MB. Programs load at `0x8001_0000`. |
|
|
31
|
+
| Scratchpad | `0x1F80_0000` | 1 KB fast on-chip RAM |
|
|
32
|
+
| Hardware I/O | `0x1F80_1000`+ | GPU (`…1810`), SPU (`…1C00`), SIO (`…1040`), DMA, timers |
|
|
33
|
+
| BIOS | `0xBFC0_0000` | HLE'd by default — you don't ship a BIOS |
|
|
34
|
+
|
|
35
|
+
Addresses are **little-endian**. `memory({op:'read', region:'system_ram'})` reads
|
|
36
|
+
main RAM.
|
|
37
|
+
|
|
38
|
+
## Booting — PS-EXE (no BIOS, no disc)
|
|
39
|
+
|
|
40
|
+
PS1 boots from a **2048-byte PS-EXE header** (magic `PS-X EXE`, with `pc0` / `t_addr`
|
|
41
|
+
/ `t_size` at fixed offsets). The HLE BIOS loads `t_size` bytes to `t_addr` and jumps
|
|
42
|
+
to `pc0`. romdev's `build`/`cart` wrap a valid PS-EXE around your code (`wrapPsExe`) —
|
|
43
|
+
no CD image, no real BIOS needed. (We never build discs; HLE + PS-EXE is enough for
|
|
44
|
+
homebrew.)
|
|
45
|
+
|
|
46
|
+
## Input
|
|
47
|
+
|
|
48
|
+
Homebrew reads the pad from **hardware** — the SIO handshake at `0x1F80_1040`
|
|
49
|
+
(transfer `0x01`, `0x42`, read the button halfword). The host's `setInput`/`run`
|
|
50
|
+
holdInputs drive the emulated pad; the game polls SIO. The helper lib has a
|
|
51
|
+
`pad_read()`.
|
|
52
|
+
|
|
53
|
+
## Sound
|
|
54
|
+
|
|
55
|
+
The SPU (24 ADPCM voices) at `0x1F80_1C00`. Samples are SPU-ADPCM in SPU RAM, keyed on
|
|
56
|
+
via the voice registers. The helper lib has a minimal tone path.
|
|
57
|
+
|
|
58
|
+
## MCP debug & inspection tooling — current state
|
|
59
|
+
|
|
60
|
+
- **`memory({op:'read', region:'system_ram'})`** — main RAM. ✅
|
|
61
|
+
- **`disasm` / `decompile`** — MIPS via rizin/Ghidra. ✅ (R3000A `jal` targets are
|
|
62
|
+
absolute VAs — the analysis buffer is base-aligned so call-following works; see
|
|
63
|
+
TROUBLESHOOTING if a fixed-VA image misbehaves.)
|
|
64
|
+
- **`frame({op:'verify'})` / screenshot** — render-health + capture. ✅
|
|
65
|
+
- **`cart` extract/wrap** — PS-EXE. ✅
|
|
66
|
+
- **`cpu({op:'read'})`** — live R3000A register file (`romdev_mips_regs_get`: r0..r31,
|
|
67
|
+
LO, HI, PC). ✅
|
|
68
|
+
- **`audioDebug({op:'inspect', chip:'spu'})`** — the SPU register block at `0x1F80_1C00`
|
|
69
|
+
(`romdev_spu_get`, the 1 KB window). ✅
|
|
70
|
+
- **`breakpoint` / `watch`** — not yet (those need interpreter-step + memory-path hooks
|
|
71
|
+
patched into beetle; cpuState/audioDebug are plain reads and ARE wired).
|
|
72
|
+
- **`renderingContext`** is **N/A** (false) — 3D GPU machine, no 2D tile VDP to decode.
|
|
73
|
+
|
|
74
|
+
## Build pipeline
|
|
75
|
+
|
|
76
|
+
`build({platform:'ps1'})` cross-compiles with the from-scratch **mips-elf-gcc → WASM**
|
|
77
|
+
toolchain (little-endian libs; a small `softint.c` supplies 64-bit divide helpers on
|
|
78
|
+
the LE side), then `wrapPsExe` produces the bootable PS-EXE. `#include` the bundled
|
|
79
|
+
`psx.h` for the software-3D front end + GP0 emit + SIO/SPU helpers.
|
|
80
|
+
|
|
81
|
+
## What's NOT bundled / hardware limits
|
|
82
|
+
|
|
83
|
+
- No CD-ROM/XA/streaming or MDEC video.
|
|
84
|
+
- `breakpoint`/`watch` not yet (need interpreter-step hooks; cpuState/audioDebug ARE wired).
|
|
85
|
+
- `renderingContext` is N/A (3D GPU, no tile VDP).
|