romdevtools 0.70.0 → 0.71.1

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/examples/dreamcast/platformer/main.c +31 -0
  3. package/examples/dreamcast/puzzle/main.c +44 -0
  4. package/examples/dreamcast/racing/main.c +39 -0
  5. package/examples/dreamcast/shmup/main.c +50 -0
  6. package/examples/dreamcast/sports/main.c +39 -0
  7. package/package.json +3 -3
  8. package/src/cores/capabilities.js +13 -9
  9. package/src/host/LibretroHost.js +242 -23
  10. package/src/host/callbacks.js +68 -1
  11. package/src/host/cpu-state.js +32 -0
  12. package/src/host/dc-aica-state.js +67 -0
  13. package/src/mcp/tools/audio.js +1 -1
  14. package/src/mcp/tools/disasm.js +1 -1
  15. package/src/mcp/tools/index.js +1 -1
  16. package/src/mcp/tools/platform-docs.js +1 -1
  17. package/src/mcp/tools/platform-tools.js +9 -1
  18. package/src/mcp/tools/platforms.js +46 -10
  19. package/src/mcp/tools/toolchain.js +140 -10
  20. package/src/platforms/dreamcast/MENTAL_MODEL.md +87 -0
  21. package/src/platforms/dreamcast/TROUBLESHOOTING.md +55 -0
  22. package/src/platforms/dreamcast/UPSTREAM_SOURCES.md +57 -0
  23. package/src/platforms/gba/MENTAL_MODEL.md +22 -1
  24. package/src/platforms/n64/MENTAL_MODEL.md +84 -0
  25. package/src/platforms/n64/TROUBLESHOOTING.md +60 -0
  26. package/src/platforms/n64/UPSTREAM_SOURCES.md +52 -0
  27. package/src/platforms/n64/lib/c/n64.c +181 -80
  28. package/src/platforms/ps1/MENTAL_MODEL.md +85 -0
  29. package/src/platforms/ps1/TROUBLESHOOTING.md +55 -0
  30. package/src/platforms/ps1/UPSTREAM_SOURCES.md +54 -0
  31. package/src/platforms/snes/TROUBLESHOOTING.md +10 -0
  32. package/src/toolchains/asar/asar.js +84 -14
  33. package/src/toolchains/mips-c/mips-c.js +35 -1
  34. package/src/toolchains/sh-c/lib/dc.h +65 -15
  35. package/src/toolchains/sh-c/sh-c.js +6 -3
@@ -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 + software 3D pipeline (see n64.h). Framebuffer backend. */
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
- /* Two 320x240x16bpp framebuffers in RDRAM (double-buffered), cached kseg0. */
5
- #define FB0 0xA0100000u /* uncached kseg1 writes go straight to RDRAM */
6
- #define FB1 0xA0120000u
7
- static volatile unsigned short *fb; /* the back buffer we draw into */
8
- static unsigned int front;
9
-
10
- /* VI registers (0xA4400000). */
11
- #define VI(n) (*(volatile unsigned int*)(0xA4400000u + (n)*4))
12
- #define VI_STATUS 0
13
- #define VI_ORIGIN 1
14
- #define VI_WIDTH 2
15
- #define VI_V_CURRENT 4
16
- #define VI_H_START 9
17
- #define VI_V_START 10
18
- #define VI_X_SCALE 12
19
- #define VI_Y_SCALE 13
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
- /* NTSC 320x240 16bpp — register indices per the N64 VI map:
24
- 0=STATUS 1=ORIGIN 2=WIDTH 3=V_INTR 4=CURRENT 5=BURST 6=V_SYNC 7=H_SYNC
25
- 8=LEAP 9=H_START 10=V_START 11=V_BURST 12=X_SCALE 13=Y_SCALE. */
26
- VI(0) = 0x0000320E; /* STATUS: 16bpp(type=2) + AA + dither + pixel_advance */
27
- VI(2) = 320; /* WIDTH */
28
- VI(3) = 2; /* V_INTR */
29
- VI(4) = 0; /* CURRENT */
30
- VI(5) = 0x03E52239; /* BURST (NTSC) */
31
- VI(6) = 0x0000020D; /* V_SYNC = 525 */
32
- VI(7) = 0x00000C15; /* H_SYNC */
33
- VI(8) = 0x0C150C15; /* LEAP */
34
- VI(9) = 0x006C02EC; /* H_START: 108..748 */
35
- VI(10) = 0x002501FF; /* V_START: 37..511 */
36
- VI(11) = 0x000E0204; /* V_BURST */
37
- VI(12) = 0x00000200; /* X_SCALE (320) */
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
- static inline void putpx(int x, int y, unsigned short c)
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
- if ((unsigned)x < SCREEN_W && (unsigned)y < SCREEN_H) fb[y * SCREEN_W + x] = c;
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
- int i; for (i = 0; i < SCREEN_W * SCREEN_H; i++) fb[i] = col;
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 r, c;
57
- for (r = 0; r < h; r++) for (c = 0; c < w; c++) putpx(x + c, y + r, col);
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
- /* edge function for the rasterizer */
61
- static inline int edge(int ax, int ay, int bx, int by, int cx, int cy)
62
- { return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); }
63
-
64
- void n64_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col)
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
- int minx = x0, maxx = x0, miny = y0, maxy = y0, px, py, w0, w1, w2, area;
67
- if (x1 < minx) minx = x1; if (x1 > maxx) maxx = x1;
68
- if (x2 < minx) minx = x2; if (x2 > maxx) maxx = x2;
69
- if (y1 < miny) miny = y1; if (y1 > maxy) maxy = y1;
70
- if (y2 < miny) miny = y2; if (y2 > maxy) maxy = y2;
71
- if (minx < 0) minx = 0; if (miny < 0) miny = 0;
72
- if (maxx >= SCREEN_W) maxx = SCREEN_W - 1; if (maxy >= SCREEN_H) maxy = SCREEN_H - 1;
73
- area = edge(x0, y0, x1, y1, x2, y2);
74
- if (area == 0) return;
75
- for (py = miny; py <= maxy; py++) {
76
- for (px = minx; px <= maxx; px++) {
77
- w0 = edge(x1, y1, x2, y2, px, py);
78
- w1 = edge(x2, y2, x0, y0, px, py);
79
- w2 = edge(x0, y0, x1, y1, px, py);
80
- if (area > 0) { if (w0 >= 0 && w1 >= 0 && w2 >= 0) fb[py * SCREEN_W + px] = col; }
81
- else { if (w0 <= 0 && w1 <= 0 && w2 <= 0) fb[py * SCREEN_W + px] = col; }
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
- /* swap buffers + point VI at the freshly drawn one, then wait for a frame. */
89
- unsigned int newfront = (unsigned int)(unsigned long)fb;
90
- VI(1) = newfront & 0x1FFFFFFF;
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. The PIF command 0x01 polls
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; /* PIF RAM */
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; /* SI_PIF_ADDR_RD64B: kick the PIF read */
207
+ si[1] = 0x1FC007C0;
107
208
  { volatile int t; for (t = 0; t < 5000; t++) { } }
108
- buttons = pif[1] >> 16; /* the button half-word */
109
- return (~0u) & buttons; /* JoyBus buttons are active-high already */
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 (shared with PS1 lib: 256-step binary angle) ── */
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 (identical to the PS1 lib) ── */
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
- n64_tri2d(x0,y0,x1,y1,x2,y2,col);
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
- n64_tri2d(x0,y0,x1,y1,x2,y2,col);
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 scaled) ── */
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).