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,55 @@
|
|
|
1
|
+
# PlayStation (PS1) — troubleshooting
|
|
2
|
+
|
|
3
|
+
Read `platform({op:'doc', platform:'ps1', name:'mental_model'})` first — PS1 renders
|
|
4
|
+
by issuing **GPU primitives (GP0)** on the hardware renderer, not by writing a CPU
|
|
5
|
+
framebuffer.
|
|
6
|
+
|
|
7
|
+
## "ROM builds + boots but the screen is BLACK"
|
|
8
|
+
|
|
9
|
+
1. **You're trying to write a raw framebuffer.** PS1's renderer is the hardware GPU
|
|
10
|
+
(`beetle_psx_hw`, `hwRender:true`) — it rasterizes the **GP0 command stream**, not
|
|
11
|
+
a CPU-written framebuffer. Draw with GPU primitives (the bundled `psx.c` emits
|
|
12
|
+
them); a CPU memset of "VRAM" won't show.
|
|
13
|
+
2. **You never set the display area / drawing environment.** GP1 sets the display
|
|
14
|
+
mode + display start; GP0 `0xE1..0xE6` set the drawing area/offset. Without a valid
|
|
15
|
+
draw env + display env the GPU has nowhere to put pixels. The helper lib's
|
|
16
|
+
`gpu_init` does this — call it before drawing.
|
|
17
|
+
3. **Nothing was actually submitted to GP0.** Confirm with `frame({op:'verify'})`
|
|
18
|
+
(nearlyBlank) + check you're writing the GP0 port (`0x1F80_1810`) / running a DMA
|
|
19
|
+
to it, not just touching VRAM.
|
|
20
|
+
|
|
21
|
+
## "Rectangles stretch to the edge of the screen / primitives are garbage"
|
|
22
|
+
|
|
23
|
+
`GP0 0x60` (and the rect family) is a **variable-size rectangle**: 3 words = color,
|
|
24
|
+
top-left corner, size. If you feed it 4 polygon vertices it reads corner+size+garbage
|
|
25
|
+
and stretches. Use the **triangle** family (`0x20`/`0x24`/`0x28`/`0x2C`) for polys and
|
|
26
|
+
the **rect** family (`0x60`/`0x64`/`0x68`/`0x6C`) for rects. Match the command to the
|
|
27
|
+
primitive and word count.
|
|
28
|
+
|
|
29
|
+
## "Geometry is wrong / inside-out / clipped"
|
|
30
|
+
|
|
31
|
+
Software-3D front-end math (the helper lib transforms before emitting GP0): check
|
|
32
|
+
16.16 fixed-point throughout, back-face winding matches your vertex order, perspective
|
|
33
|
+
divide before the GP0 vertex coords, and coords fit the 11-bit signed GPU range.
|
|
34
|
+
|
|
35
|
+
## "disasm/decompile of a multi-function program returns junk (fcn.00000000)"
|
|
36
|
+
|
|
37
|
+
Known rizin trap: for **absolute-addressed `jal`** (PS1 `jal 0x80010518`), rizin
|
|
38
|
+
ignores `-B` on a raw `malloc://` buffer and addresses flat from 0, so cross-function
|
|
39
|
+
discovery dangles outside the image. romdev fixes this by left-padding `.text` so the
|
|
40
|
+
flat offset == the VA's low 20 bits, seeding analysis there, and rebasing the high bits
|
|
41
|
+
back — so reported addresses are real VAs. If you see a lone `fcn.00000000`, you're on
|
|
42
|
+
a path that bypassed the rebase; build a **multi-function** program to exercise it (a
|
|
43
|
+
single-instruction smoke test hides this).
|
|
44
|
+
|
|
45
|
+
## "breakpoint / watch return N/A"
|
|
46
|
+
|
|
47
|
+
`cpu({op:'read'})` and `audioDebug({op:'inspect', chip:'spu'})` ARE wired on PS1 (the
|
|
48
|
+
core exports `romdev_mips_regs_get` + `romdev_spu_get`). `breakpoint`/`watch` are not
|
|
49
|
+
yet — those need interpreter-step + memory-path hooks patched into beetle (cpuState/
|
|
50
|
+
audioDebug are plain reads). Use `cpu`/`audioDebug` + `memory` + `disasm`/`decompile`
|
|
51
|
+
meanwhile.
|
|
52
|
+
|
|
53
|
+
## "renderingContext returns N/A"
|
|
54
|
+
|
|
55
|
+
Correct — PS1 is a 3D GPU machine with no 2D tile/sprite VDP for that op. Not a bug.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# PlayStation (PS1) — source you can read
|
|
2
|
+
|
|
3
|
+
Trust hierarchy (try in order before filing a feedback round):
|
|
4
|
+
|
|
5
|
+
1. **Bundled examples** (`examples/ps1/{shmup,platformer,puzzle,racing,sports}/main.c` +
|
|
6
|
+
`sprite_move/`) — verified building + rendering on the GPU. Start here.
|
|
7
|
+
2. **Bundled helper lib source** (`src/platforms/ps1/lib/c/psx.c` + `psx.h`) — the GPU
|
|
8
|
+
front end. Read this when a primitive renders wrong: it shows how the software-3D
|
|
9
|
+
transform feeds **GP0 GPU primitives** (the real console path — the GPU rasterizes the
|
|
10
|
+
command stream; this is NOT a software framebuffer). The GP0 `0x60` rect-vs-poly gotcha
|
|
11
|
+
is in here + TROUBLESHOOTING.
|
|
12
|
+
3. **The core source** (NOT bundled — fetch on demand):
|
|
13
|
+
|
|
14
|
+
| What | Upstream |
|
|
15
|
+
|---|---|
|
|
16
|
+
| beetle-psx (Mednafen PSX, libretro, HW renderer) | https://github.com/libretro/beetle-psx-libretro |
|
|
17
|
+
| R3000A CPU / SPU / GPU internals | (in the beetle tree: `mednafen/psx/cpu.c`, `spu.c`, `gpu.cpp`) |
|
|
18
|
+
|
|
19
|
+
The cpuState/audioDebug exports we patch in (`romdev_mips_regs_get`,
|
|
20
|
+
`romdev_spu_get`) live in `scripts/patches/romdev-snippets/beetle-psx-regsnap.c` +
|
|
21
|
+
`beetle-psx-spu.c`.
|
|
22
|
+
|
|
23
|
+
4. **The toolchain** — a from-scratch `mips-elf-gcc` cross-compiler (little-endian) built
|
|
24
|
+
to WASM (`scripts/build-mips-toolchain.sh` — one toolchain emits both N64 BE + PS1 LE):
|
|
25
|
+
|
|
26
|
+
| Component | Upstream |
|
|
27
|
+
|---|---|
|
|
28
|
+
| binutils | https://ftp.gnu.org/gnu/binutils/ |
|
|
29
|
+
| gcc | https://ftp.gnu.org/gnu/gcc/ |
|
|
30
|
+
| newlib | https://sourceware.org/pub/newlib/ |
|
|
31
|
+
|
|
32
|
+
5. **Reverse engineering** — Rizin + Ghidra MIPS (R3000A):
|
|
33
|
+
|
|
34
|
+
| What | Upstream |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Rizin | https://github.com/rizinorg/rizin |
|
|
37
|
+
| rz-ghidra | https://github.com/rizinorg/rz-ghidra |
|
|
38
|
+
|
|
39
|
+
## PS1 hardware docs
|
|
40
|
+
|
|
41
|
+
- **psx-spx** (Nocash, the canonical reference): https://psx-spx.consoledev.net/
|
|
42
|
+
- **PSn00bSDK** (a real open PS1 SDK — read its GPU/GTE/SPU code for the standard GP0/GP1
|
|
43
|
+
encodings + SPU register layout): https://github.com/Lameguy64/PSn00bSDK
|
|
44
|
+
|
|
45
|
+
## When to use what
|
|
46
|
+
|
|
47
|
+
- "Why is my screen BLACK?" → MENTAL_MODEL + TROUBLESHOOTING (you must issue GP0
|
|
48
|
+
primitives + set the draw/display environment — a CPU memset won't show).
|
|
49
|
+
- "My rectangles stretch to the edge" → the GP0 `0x60` variable-size-rect gotcha
|
|
50
|
+
(TROUBLESHOOTING).
|
|
51
|
+
- "How does the helper emit a triangle?" → `psx.c` + psx-spx GPU section.
|
|
52
|
+
- "audioDebug SPU volumes look off" → the SPU volume/sweep registers read processed
|
|
53
|
+
values; the decode reads the raw mirror (see `psx.c` / beetle `spu.c`).
|
|
54
|
+
- "disasm returns a single `fcn.00000000`" → TROUBLESHOOTING (the absolute-`jal` rebase).
|
|
@@ -261,6 +261,16 @@ synthesizes a fallback `issues[]` entry with a hint. The idioms to avoid:
|
|
|
261
261
|
block where the layout expects it. Use
|
|
262
262
|
`examples({op:'snippets', platform:"snes", mode:"get", snippetName:"lorom_header.asm"})`
|
|
263
263
|
for the canonical layout (and `lorom_multibank.asm` for multi-bank).
|
|
264
|
+
- **`readfile1`/`filesize`/`canreadfile` across MANY distinct files.** Converting
|
|
265
|
+
assets at assemble time (the commercial-disassembly idiom — e.g. an `incpal`
|
|
266
|
+
macro reading every `.pal` palette to emit 15-bit color) reads many *distinct*
|
|
267
|
+
files via the readfile API. asar-WASM can **abort with no diagnostic** above a
|
|
268
|
+
threshold of distinct files read that way (one file read 200× is fine — it's
|
|
269
|
+
the *count of distinct files*). `incbin` does NOT have this problem. The build
|
|
270
|
+
log carries an `[asar advisory]`/abort-hint when it sees a source over the
|
|
271
|
+
threshold. **Fix: pre-convert the assets to `.bin` blobs offline and `incbin`
|
|
272
|
+
them** — the output is byte-identical. (Put the converted blobs in a subdir;
|
|
273
|
+
`output:'project'` stages subdirectory assets recursively.)
|
|
264
274
|
|
|
265
275
|
(This is the asar/asm path. The default PVSnesLib **C** path goes through
|
|
266
276
|
tcc-65816 + wla-65816 and has its own C89 trap, above.)
|
|
@@ -74,6 +74,17 @@ export async function runAsar(args) {
|
|
|
74
74
|
});
|
|
75
75
|
let { binary, log, exitCode } = first;
|
|
76
76
|
|
|
77
|
+
// asar exits a NORMAL error build via its C++ exception path (quit throws a
|
|
78
|
+
// numeric heap pointer, not a clean status), so the worker appends a generic
|
|
79
|
+
// `[worker] Abort in WASM: <ptr>` line even though asar DID write proper
|
|
80
|
+
// diagnostics (e.g. `…: error: (Elabel_not_found): …`). That line reads as a
|
|
81
|
+
// silent crash + the heap-pointer "<number>" looks like an OOM/trap — it cost
|
|
82
|
+
// real diagnosis time (v0.70.0 feedback #5). When the log already has a real
|
|
83
|
+
// asar diagnostic, this was an ordinary error exit: drop the misleading line.
|
|
84
|
+
if (exitCode !== 0 && /:\s*(error|warning)\s*:/i.test(log)) {
|
|
85
|
+
log = log.replace(/\n?\s*\[worker\] Abort in WASM:[^\n]*\n?/g, "\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
// Silent-fail recovery. When asar throws a numeric value through Emscripten's
|
|
78
89
|
// C++-exception path, that "number" is a heap pointer to the thrown object,
|
|
79
90
|
// not an exit code — and we've lost the actual error message. Under
|
|
@@ -101,9 +112,19 @@ export async function runAsar(args) {
|
|
|
101
112
|
|
|
102
113
|
const hex = "0x" + (exitCode >>> 0).toString(16).toUpperCase();
|
|
103
114
|
const looksLikeHeapPointer = exitCode > 1024;
|
|
104
|
-
|
|
115
|
+
// Count DISTINCT files touched via the readfile/filesize/canreadfile API.
|
|
116
|
+
// asar-WASM has a known resource issue where reading enough distinct files
|
|
117
|
+
// that way (the canonical commercial-disassembly asset-conversion idiom, e.g.
|
|
118
|
+
// an incpal macro converting many .pal palettes) can abort with no diagnostic
|
|
119
|
+
// (v0.70.0 feedback #1). Surfacing the count turns a silent dead-end into a
|
|
120
|
+
// lead. (incbin of the same files does NOT trip it — only readfile1/filesize.)
|
|
121
|
+
const distinctReadfileFiles = countReadfileFiles(source);
|
|
122
|
+
const readfileHint = distinctReadfileFiles >= READFILE_DISTINCT_WARN
|
|
123
|
+
? ` This source reads ${distinctReadfileFiles} DISTINCT files via readfile1/filesize/canreadfile — asar-WASM can abort with no diagnostic above a threshold of distinct files read that way (a known limitation). If you're converting assets at assemble time (e.g. .pal -> 15-bit), pre-convert them to .bin blobs offline and incbin those instead (incbin does NOT leak) — the output is byte-identical.`
|
|
124
|
+
: "";
|
|
125
|
+
const hint = (looksLikeHeapPointer
|
|
105
126
|
? "Exit code looks like a heap pointer, not a real exit code — likely a C++ exception that escaped without writing to stderr. Common causes: bank-border-crossed, section overlap, ROM size exceeded, or out-of-memory while assembling."
|
|
106
|
-
: "asar exited non-zero with no output.";
|
|
127
|
+
: "asar exited non-zero with no output.") + readfileHint;
|
|
107
128
|
|
|
108
129
|
const layoutText = formatLayout(summarizeWrittenRegions(verboseBinary, baseRom, { flatBinary }));
|
|
109
130
|
|
|
@@ -155,6 +176,20 @@ export async function runAsar(args) {
|
|
|
155
176
|
includesInfo[name] = { size };
|
|
156
177
|
}
|
|
157
178
|
|
|
179
|
+
// Proactive advisory: even when THIS build squeaked by, a source reading many
|
|
180
|
+
// distinct files via readfile1/filesize is at risk of the asar-WASM abort —
|
|
181
|
+
// surface it (non-fatal) so a growing disassembly doesn't dead-end later.
|
|
182
|
+
{
|
|
183
|
+
const distinct = countReadfileFiles(source);
|
|
184
|
+
if (distinct >= READFILE_DISTINCT_WARN && !/readfile.*distinct files/i.test(log)) {
|
|
185
|
+
log =
|
|
186
|
+
`[asar advisory] this source reads ${distinct} distinct files via readfile1/filesize — ` +
|
|
187
|
+
`asar-WASM can abort (no diagnostic) above a threshold of distinct files read that way. ` +
|
|
188
|
+
`If you're converting assets at assemble time, prefer pre-converting them to .bin and ` +
|
|
189
|
+
`'incbin' (byte-identical, no leak).\n` + log;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
158
193
|
// Try to read the symbol file that asar emitted, if any.
|
|
159
194
|
let symbolsText = null;
|
|
160
195
|
if (wantSymbols) {
|
|
@@ -346,20 +381,30 @@ function asarPreflight(source, { binaryIncludes = {}, includes = {} } = {}) {
|
|
|
346
381
|
}
|
|
347
382
|
|
|
348
383
|
// -----------------------------------------------------------------------
|
|
349
|
-
// Header-overlap detection:
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
384
|
+
// Header-overlap detection: an `incbin "foo.bin"` whose span would reach
|
|
385
|
+
// $00FFC0 clobbers the LoROM header during pass 2. **The LoROM header lives
|
|
386
|
+
// ONLY in bank $00** ($00FFC0..$00FFFF) — for banks $01+, $XXFFC0 is ordinary
|
|
387
|
+
// ROM, and games (e.g. SMW's GFX banks 08-0B) legitimately fill across those
|
|
388
|
+
// boundaries under `check bankcross off`. So only flag BANK $00 (v0.70.0 #6:
|
|
389
|
+
// the old check false-positived on every bank as "$08FFC0 header"). Also honor
|
|
390
|
+
// `check bankcross off` (the user is explicitly opting into bank-crossing fills).
|
|
353
391
|
//
|
|
354
|
-
// We only check incbin's pointing at files in binaryIncludes (
|
|
355
|
-
//
|
|
356
|
-
//
|
|
392
|
+
// We only check incbin's pointing at files in binaryIncludes (size known) and
|
|
393
|
+
// assume each is preceded by an `org $008000`. Note: this scans only the entry
|
|
394
|
+
// source — `incbin`s inside `incsrc`'d files aren't seen (so the check is a
|
|
395
|
+
// best-effort early warning, not full coverage).
|
|
357
396
|
// -----------------------------------------------------------------------
|
|
358
397
|
const incbinDirective = /^\s*incbin\s+"([^"]+)"/;
|
|
359
|
-
|
|
398
|
+
const bankcrossOffRe = /^\s*check\s+bankcross\s+off\b/i;
|
|
399
|
+
const bankcrossOnRe = /^\s*check\s+bankcross\s+(on|half)\b/i;
|
|
400
|
+
// Walk top-to-bottom, tracking the most recent org as the "cursor" and the
|
|
401
|
+
// current `check bankcross` state (off = the user opted into crossing fills).
|
|
360
402
|
let cursor = null;
|
|
403
|
+
let bankcrossOff = false;
|
|
361
404
|
for (let i = 0; i < lines.length; i++) {
|
|
362
405
|
const line = stripComment(lines[i]);
|
|
406
|
+
if (bankcrossOffRe.test(line)) { bankcrossOff = true; continue; }
|
|
407
|
+
if (bankcrossOnRe.test(line)) { bankcrossOff = false; continue; }
|
|
363
408
|
const orgMatch = line.match(orgDirective);
|
|
364
409
|
if (orgMatch) {
|
|
365
410
|
cursor = parseInt(orgMatch[1], 16);
|
|
@@ -373,15 +418,16 @@ function asarPreflight(source, { binaryIncludes = {}, includes = {} } = {}) {
|
|
|
373
418
|
const size = blob instanceof Uint8Array ? blob.length : Buffer.from(blob, "base64").length;
|
|
374
419
|
const bank = (cursor >>> 16) & 0xFF;
|
|
375
420
|
const off = cursor & 0xFFFF;
|
|
376
|
-
const headerStart = 0xFFC0; // LoROM header lives at $
|
|
377
|
-
if (off >= 0x8000 && off + size > headerStart) {
|
|
421
|
+
const headerStart = 0xFFC0; // LoROM header lives at $00FFC0..$00FFFF (bank $00 ONLY)
|
|
422
|
+
if (!bankcrossOff && bank === 0x00 && off >= 0x8000 && off + size > headerStart) {
|
|
378
423
|
const fmtAddr = (a) => "$" + a.toString(16).padStart(6, "0").toUpperCase();
|
|
379
424
|
const endAddr = (bank << 16) | (off + size - 1);
|
|
380
425
|
issues.push(
|
|
381
426
|
`[asar preflight] line ${i + 1}: 'incbin "${name}"' (${size} bytes) starting at ` +
|
|
382
427
|
`${fmtAddr(cursor)} would extend to ${fmtAddr(endAddr)}, overlapping the LoROM ` +
|
|
383
|
-
`header at
|
|
384
|
-
`
|
|
428
|
+
`header at $00FFC0. Move this data to a higher bank (e.g. 'org $018000') or ` +
|
|
429
|
+
`shrink it below ${headerStart - off} bytes. (Add 'check bankcross off' if this ` +
|
|
430
|
+
`crossing is intentional.)`,
|
|
385
431
|
);
|
|
386
432
|
}
|
|
387
433
|
// Advance cursor so consecutive incbins in the same bank stack.
|
|
@@ -531,3 +577,27 @@ function asarPreflight(source, { binaryIncludes = {}, includes = {} } = {}) {
|
|
|
531
577
|
}
|
|
532
578
|
return null;
|
|
533
579
|
}
|
|
580
|
+
|
|
581
|
+
// asar-WASM aborts (no diagnostic) once readfile1/filesize/canreadfile touch
|
|
582
|
+
// enough DISTINCT files — the canonical commercial-disassembly asset-conversion
|
|
583
|
+
// idiom (an incpal-style macro converting many .pal palettes at assemble time).
|
|
584
|
+
// We can't always predict the exact ceiling, so warn above this many distinct
|
|
585
|
+
// files so the build doesn't silently dead-end (v0.70.0 feedback #1).
|
|
586
|
+
const READFILE_DISTINCT_WARN = 40;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Count DISTINCT file names passed to the readfile / filesize / canreadfile
|
|
590
|
+
* functions. Only literal string filenames are counted (computed names can't be
|
|
591
|
+
* resolved statically). Used for the readfile-leak advisory.
|
|
592
|
+
* @param {string} source
|
|
593
|
+
* @returns {number}
|
|
594
|
+
*/
|
|
595
|
+
function countReadfileFiles(source) {
|
|
596
|
+
const names = new Set();
|
|
597
|
+
// readfile1("x",..) / readfile2(...) / readfile3 / readfile4 / filesize("x")
|
|
598
|
+
// / canreadfile("x",..) / canreadfile1..4 — first string arg is the filename.
|
|
599
|
+
const re = /\b(?:readfile[1-4]|canreadfile[1-4]?|filesize)\s*\(\s*"([^"]+)"/gi;
|
|
600
|
+
let m;
|
|
601
|
+
while ((m = re.exec(source)) !== null) names.add(m[1]);
|
|
602
|
+
return names.size;
|
|
603
|
+
}
|
package/src/toolchains/index.js
CHANGED
|
@@ -91,6 +91,9 @@ const LANGUAGE_TOOLCHAIN = {
|
|
|
91
91
|
n64: {
|
|
92
92
|
c: { toolchain: "mips-elf-gcc", available: true, note: "C for N64 via gcc 14.2.0 + binutils + newlib (mips-elf, big-endian R4300), compiled to WASM. Bare path: minimal crt0 (stack + .bss + main()) → flat code image at 0x80000400. NOTE: a fully bootable N64 ROM needs the IPL3 bootcode + a libdragon-style header (libdragon SDK forthcoming) — this path exercises the toolchain + is the basis for the SDK." },
|
|
93
93
|
},
|
|
94
|
+
dreamcast: {
|
|
95
|
+
c: { toolchain: "sh-elf-gcc", available: true, note: "C for Dreamcast via gcc 14.2.0 + binutils + newlib (sh-elf, little-endian SH-4, m4-single-only FP), compiled to WASM. Bare path: a minimal crt0 sets the stack + clears .bss + calls main(); the output is an ELF that Flycast's reios HLE BIOS boots DIRECTLY (no GD-ROM/CDI image, no firmware). No KallistiOS yet — bring up the PowerVR2 framebuffer yourself: program FB_R_CTRL/FB_R_SIZE/FB_R_SOF1 + SPG for 640x480 RGB565 at VRAM 0xA5000000, then write pixels (see the dc.h helper). With flycast_emulate_framebuffer on, a plain pixel-writing program presents — no TA list needed." },
|
|
96
|
+
},
|
|
94
97
|
};
|
|
95
98
|
|
|
96
99
|
/**
|
|
@@ -123,6 +126,7 @@ const PLATFORM_DEFAULT_LANGUAGE = {
|
|
|
123
126
|
msx: "c",
|
|
124
127
|
ps1: "c",
|
|
125
128
|
n64: "c",
|
|
129
|
+
dreamcast: "c",
|
|
126
130
|
};
|
|
127
131
|
|
|
128
132
|
/**
|
|
@@ -502,6 +506,32 @@ export async function buildForPlatform(args) {
|
|
|
502
506
|
};
|
|
503
507
|
}
|
|
504
508
|
|
|
509
|
+
if (args.platform === "dreamcast") {
|
|
510
|
+
// Dreamcast SH-4 C: the bare gcc+newlib+libgcc path through the sh-elf-gcc WASM
|
|
511
|
+
// toolchain (cc1→as→ld). The output is an ELF that Flycast's reios HLE BIOS boots
|
|
512
|
+
// directly (no GD-ROM/CDI image). Bring up the PowerVR2 framebuffer yourself (see
|
|
513
|
+
// the dc.h helper); language defaults to "c".
|
|
514
|
+
const { buildShC } = await import("./sh-c/sh-c.js");
|
|
515
|
+
const r = await buildShC({
|
|
516
|
+
source: args.source,
|
|
517
|
+
sources: args.sources,
|
|
518
|
+
headers: args.includes,
|
|
519
|
+
cc1Options: args.options,
|
|
520
|
+
});
|
|
521
|
+
return {
|
|
522
|
+
ok: r.ok,
|
|
523
|
+
binary: r.binary,
|
|
524
|
+
listing: "",
|
|
525
|
+
symbols: r.symbols ?? "",
|
|
526
|
+
log: r.log,
|
|
527
|
+
issues: parseBuildLog(r.log),
|
|
528
|
+
exitCode: r.exitCode,
|
|
529
|
+
toolchain: "sh-elf-gcc",
|
|
530
|
+
stage: r.stage,
|
|
531
|
+
...(r.crash ? { crash: r.crash } : {}),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
505
535
|
if (args.platform === "gba") {
|
|
506
536
|
// R24 + R28: language:"c" routes through the arm-none-eabi gcc +
|
|
507
537
|
// binutils WASM toolchain (cc1-arm → as → ld → objcopy). Three
|
|
@@ -77,9 +77,30 @@ function wrapN64Rom(text, entry = 0x80000400) {
|
|
|
77
77
|
export async function buildMipsC(args) {
|
|
78
78
|
const platform = args.platform;
|
|
79
79
|
const endian = platform === "ps1" ? "little" : "big";
|
|
80
|
-
const headers = args.headers ?? {};
|
|
81
80
|
const cc1Options = [...(args.cc1Options ?? []), "-O2", "-G0", "-ffreestanding", "-fno-builtin", "-Wall"];
|
|
82
81
|
const sources = args.sources ?? (args.source != null ? { "main.c": args.source } : {});
|
|
82
|
+
|
|
83
|
+
// Auto-bundle the platform helper lib so `#include "n64.h"` / `#include "psx.h"`
|
|
84
|
+
// just works (parity with the Dreamcast sh-c path that auto-bundles dc.h). The
|
|
85
|
+
// header is added to the virtual headers; the matching .c is compiled + linked as
|
|
86
|
+
// an extra source — UNLESS the caller already provides their own (caller wins, and
|
|
87
|
+
// we skip auto-linking the .c if a same-named source is already present so there's
|
|
88
|
+
// no duplicate-symbol clash). The helper lives in platforms/<platform>/lib/c/.
|
|
89
|
+
const helperName = platform === "ps1" ? "psx" : platform === "n64" ? "n64" : null;
|
|
90
|
+
const headers = { ...(args.headers ?? {}) };
|
|
91
|
+
/** @type {Record<string,string>} */
|
|
92
|
+
let autoHelperSrc = null;
|
|
93
|
+
if (helperName) {
|
|
94
|
+
const helperDir = path.join(__dirname, "..", "..", "platforms", platform, "lib", "c");
|
|
95
|
+
const hPath = path.join(helperDir, `${helperName}.h`);
|
|
96
|
+
const cPath = path.join(helperDir, `${helperName}.c`);
|
|
97
|
+
if (headers[`${helperName}.h`] == null) {
|
|
98
|
+
const hSrc = await readFile(hPath, "utf-8").catch(() => null);
|
|
99
|
+
if (hSrc != null) headers[`${helperName}.h`] = hSrc;
|
|
100
|
+
}
|
|
101
|
+
const callerHasC = (args.sources && (args.sources[`${helperName}.c`] != null));
|
|
102
|
+
if (!callerHasC) autoHelperSrc = await readFile(cPath, "utf-8").catch(() => null);
|
|
103
|
+
}
|
|
83
104
|
let log = "";
|
|
84
105
|
|
|
85
106
|
/** @type {Record<string, Uint8Array>} */
|
|
@@ -93,6 +114,19 @@ export async function buildMipsC(args) {
|
|
|
93
114
|
if (as.exitCode !== 0 || !as.object) return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, ...(as.crash ? { crash: as.crash } : {}) };
|
|
94
115
|
userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
|
|
95
116
|
}
|
|
117
|
+
// Auto-bundled helper .c (n64.c / psx.c) — compiled with the SAME headers so it can
|
|
118
|
+
// see its own header, linked alongside the user objects. Skipped when the caller
|
|
119
|
+
// supplied their own helper .c (callerHasC) above.
|
|
120
|
+
if (autoHelperSrc != null) {
|
|
121
|
+
const cc = await runCc1mips({ source: autoHelperSrc, headers, options: cc1Options, endian });
|
|
122
|
+
log += `--- cc1 (${helperName}.c, bundled) ---\n${cc.log || "(ok)"}\n`;
|
|
123
|
+
if (cc.exitCode !== 0 || !cc.asmSource) return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${helperName}.c bundled)`, ...(cc.crash ? { crash: cc.crash } : {}) };
|
|
124
|
+
const as = await runMipsAs({ source: cc.asmSource, endian });
|
|
125
|
+
log += `--- as (${helperName}.c, bundled) ---\n${as.log || "(ok)"}\n`;
|
|
126
|
+
if (as.exitCode !== 0 || !as.object) return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${helperName}.c bundled)` };
|
|
127
|
+
userObjs[`${helperName}.o`] = as.object;
|
|
128
|
+
}
|
|
129
|
+
|
|
96
130
|
// raw .s sources too
|
|
97
131
|
for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
|
|
98
132
|
const as = await runMipsAs({ source: sources[sName], endian });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.section .text.start, "ax"
|
|
2
|
+
.global _start
|
|
3
|
+
_start:
|
|
4
|
+
mov.l stack_top, r15 ! stack = top of 16MB DC RAM
|
|
5
|
+
mov.l bss_start, r0 ! clear .bss
|
|
6
|
+
mov.l bss_end, r1
|
|
7
|
+
mov #0, r2
|
|
8
|
+
1: cmp/hs r1, r0
|
|
9
|
+
bt 2f
|
|
10
|
+
mov.l r2, @r0
|
|
11
|
+
add #4, r0
|
|
12
|
+
bra 1b
|
|
13
|
+
nop
|
|
14
|
+
2: mov.l main_addr, r0 ! call main()
|
|
15
|
+
jsr @r0
|
|
16
|
+
nop
|
|
17
|
+
hang: bra hang
|
|
18
|
+
nop
|
|
19
|
+
.align 4
|
|
20
|
+
stack_top: .long 0x8d000000
|
|
21
|
+
bss_start: .long __bss_start
|
|
22
|
+
bss_end: .long _end
|
|
23
|
+
main_addr: .long _main
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/* dc.h — minimal Dreamcast helper for romdev homebrew.
|
|
2
|
+
*
|
|
3
|
+
* Brings up the PowerVR2 video output for a plain 640x480 RGB565 framebuffer and
|
|
4
|
+
* exposes pixel/fill/rect primitives. No KallistiOS dependency — just the registers
|
|
5
|
+
* the Flycast (reios HLE) core needs to scan out a direct framebuffer.
|
|
6
|
+
*
|
|
7
|
+
* Memory map:
|
|
8
|
+
* PVR/HOLLY registers : 0xA05F8000 (uncached)
|
|
9
|
+
* VRAM (64-bit area) : 0xA5000000 (16 MB) — the framebuffer lives here
|
|
10
|
+
*
|
|
11
|
+
* The DC has two VRAM views; the 64-bit area (0xA5000000) is the linear one we draw to.
|
|
12
|
+
*/
|
|
13
|
+
#ifndef ROMDEV_DC_H
|
|
14
|
+
#define ROMDEV_DC_H
|
|
15
|
+
|
|
16
|
+
typedef unsigned char u8;
|
|
17
|
+
typedef unsigned short u16;
|
|
18
|
+
typedef unsigned int u32;
|
|
19
|
+
|
|
20
|
+
#define DC_W 640
|
|
21
|
+
#define DC_H 480
|
|
22
|
+
|
|
23
|
+
/* PVR register access */
|
|
24
|
+
#define PVR_BASE 0xA05F8000u
|
|
25
|
+
#define PVR_REG(off) (*(volatile u32 *)(PVR_BASE + (off)))
|
|
26
|
+
|
|
27
|
+
/* The framebuffer we draw to (offset 0 in the 64-bit VRAM area). */
|
|
28
|
+
#define DC_FB_OFFSET 0x000000u
|
|
29
|
+
#define DC_VRAM ((volatile u16 *)(0xA5000000u + DC_FB_OFFSET))
|
|
30
|
+
|
|
31
|
+
/* RGB565 helper */
|
|
32
|
+
static inline u16 dc_rgb(u8 r, u8 g, u8 b) {
|
|
33
|
+
return (u16)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Bring up 640x480 RGB565 video. Call once at start. The register values are the
|
|
37
|
+
* documented PowerVR2 settings for a progressive NTSC 640x480 RGB565 framebuffer;
|
|
38
|
+
* Flycast's reios + EmulateFramebuffer scanout reads FB_R_CTRL/FB_R_SIZE/FB_R_SOF1. */
|
|
39
|
+
static inline void dc_video_init(void) {
|
|
40
|
+
/* FB_R_CTRL: fb_enable=1, fb_depth=1 (RGB565), fb_concat=4 (fill low bits). */
|
|
41
|
+
PVR_REG(0x044) = 0x00000001u | (1u << 2) | (4u << 4); /* = 0x45 */
|
|
42
|
+
/* FB_W_CTRL: fb_packmode=1 (RGB565). */
|
|
43
|
+
PVR_REG(0x048) = 0x00000001u;
|
|
44
|
+
/* FB_R_SOF1 / FB_W_SOF1: framebuffer start = DC_FB_OFFSET. */
|
|
45
|
+
PVR_REG(0x050) = DC_FB_OFFSET; /* FB_R_SOF1 */
|
|
46
|
+
PVR_REG(0x060) = DC_FB_OFFSET; /* FB_W_SOF1 */
|
|
47
|
+
/* FB_R_SIZE: fb_x_size = (line bytes / 4) - 1, fb_y_size = lines - 1, modulus = 1.
|
|
48
|
+
* 640px * 2 bytes = 1280 bytes/line -> 1280/4 - 1 = 319. */
|
|
49
|
+
PVR_REG(0x05C) = (u32)(DC_W * 2 / 4 - 1) /* fb_x_size = 319 */
|
|
50
|
+
| ((u32)(DC_H - 1) << 10) /* fb_y_size = 479 */
|
|
51
|
+
| ((u32)1u << 20); /* fb_modulus = 1 */
|
|
52
|
+
/* FB_W_LINESTRIDE: line stride in 64-bit (8-byte) units = 1280/8 = 160. */
|
|
53
|
+
PVR_REG(0x11C) = (u32)(DC_W * 2 / 8);
|
|
54
|
+
/* VO_CONTROL: pixel double off, normal output. */
|
|
55
|
+
PVR_REG(0x0E8) = 0x00000000u;
|
|
56
|
+
/* VO_BORDER_COL: black border. */
|
|
57
|
+
PVR_REG(0x040) = 0x00000000u;
|
|
58
|
+
/* SPG_LOAD / SPG_CONTROL: NTSC 640x480 INTERLACED (480i). Non-interlace (240p)
|
|
59
|
+
* only displays 240 lines, so the top half of a 480-line framebuffer never shows.
|
|
60
|
+
* interlace=1 outputs the full 480-line image. */
|
|
61
|
+
PVR_REG(0x0D8) = (524u << 16) | 857u; /* SPG_LOAD: vcount, hcount */
|
|
62
|
+
PVR_REG(0x0D0) = 0x00000050u; /* SPG_CONTROL: NTSC, interlace=1 (bit4), PAL=0 */
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Plot a pixel (no bounds clamp on the hot path; caller keeps in range). */
|
|
66
|
+
static inline void dc_plot(int x, int y, u16 c) {
|
|
67
|
+
DC_VRAM[y * DC_W + x] = c;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Fill the whole framebuffer with one color. */
|
|
71
|
+
static inline void dc_clear(u16 c) {
|
|
72
|
+
int i;
|
|
73
|
+
for (i = 0; i < DC_W * DC_H; i++)
|
|
74
|
+
DC_VRAM[i] = c;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Fill an axis-aligned rectangle (clipped to the screen). */
|
|
78
|
+
static inline void dc_rect(int x0, int y0, int w, int h, u16 c) {
|
|
79
|
+
int x, y;
|
|
80
|
+
if (x0 < 0) { w += x0; x0 = 0; }
|
|
81
|
+
if (y0 < 0) { h += y0; y0 = 0; }
|
|
82
|
+
if (x0 + w > DC_W) w = DC_W - x0;
|
|
83
|
+
if (y0 + h > DC_H) h = DC_H - y0;
|
|
84
|
+
for (y = 0; y < h; y++)
|
|
85
|
+
for (x = 0; x < w; x++)
|
|
86
|
+
DC_VRAM[(y0 + y) * DC_W + (x0 + x)] = c;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Controller input (Maple bus, port 0) ─────────────────────────────────────
|
|
90
|
+
* Digital-button bits, matching Flycast's DC kcode (active-LOW on the wire — a bit
|
|
91
|
+
* is 0 when pressed). dc_pad() does a real Maple "Get Condition" DMA, inverts the
|
|
92
|
+
* kcode to active-HIGH, and masks to the real button bits. The romdev host's
|
|
93
|
+
* setInput drives the emulated pad. */
|
|
94
|
+
#define DC_BTN_C (1u << 0)
|
|
95
|
+
#define DC_BTN_B (1u << 1)
|
|
96
|
+
#define DC_BTN_A (1u << 2)
|
|
97
|
+
#define DC_BTN_START (1u << 3)
|
|
98
|
+
#define DC_BTN_UP (1u << 4)
|
|
99
|
+
#define DC_BTN_DOWN (1u << 5)
|
|
100
|
+
#define DC_BTN_LEFT (1u << 6)
|
|
101
|
+
#define DC_BTN_RIGHT (1u << 7)
|
|
102
|
+
#define DC_BTN_Y (1u << 9)
|
|
103
|
+
#define DC_BTN_X (1u << 10)
|
|
104
|
+
/* only the bits above are real digital buttons; mask out reserved bits on read. */
|
|
105
|
+
#define DC_BTN_MASK 0x06FFu
|
|
106
|
+
|
|
107
|
+
/* Maple DMA registers (Holly system bus). */
|
|
108
|
+
#define DC_SB(o) (*(volatile u32 *)(0xA05F6C00u + (o)))
|
|
109
|
+
#define DC_MDSTAR 0x04 /* command-table address */
|
|
110
|
+
#define DC_MDEN 0x14 /* DMA enable */
|
|
111
|
+
#define DC_MDST 0x18 /* DMA start */
|
|
112
|
+
|
|
113
|
+
/* Read controller port 0 via a Maple "Get Condition" DMA. Builds a one-shot command
|
|
114
|
+
* frame (recipient = port-0 controller AP 0x20, cmd 0x09 GetCondition, function =
|
|
115
|
+
* controller 0x01000000), points the recv buffer at a scratch, kicks the DMA, then
|
|
116
|
+
* scans the response for the controller's function-id marker (0x01000000) and reads
|
|
117
|
+
* the 16-bit kcode that immediately follows. kcode is active-LOW on the wire, so we
|
|
118
|
+
* invert + mask to the real button bits → DC_BTN_* set == pressed.
|
|
119
|
+
*
|
|
120
|
+
* NOTE: the exact word offset of the kcode in the recv frame can vary; we locate it
|
|
121
|
+
* by the MFID marker rather than a fixed index, which is robust across the response
|
|
122
|
+
* framing. Returns 0 if no controller response is found. */
|
|
123
|
+
static inline u32 dc_pad(void) {
|
|
124
|
+
static volatile u32 cmd[8] __attribute__((aligned(32)));
|
|
125
|
+
static volatile u32 rsp[16] __attribute__((aligned(32)));
|
|
126
|
+
u32 phys_cmd = ((u32)(unsigned long)cmd) & 0x1FFFFFFF;
|
|
127
|
+
u32 phys_rsp = ((u32)(unsigned long)rsp) & 0x1FFFFFFF;
|
|
128
|
+
int i;
|
|
129
|
+
for (i = 0; i < 16; i++) rsp[i] = 0;
|
|
130
|
+
cmd[0] = 0x80000000u; /* last xfer (bit31), op=START(0), frame len-1=0 */
|
|
131
|
+
cmd[1] = phys_rsp; /* receive address */
|
|
132
|
+
cmd[2] = (1u << 24) /* data length in words (1) */
|
|
133
|
+
| (0x20u << 16) /* sender address */
|
|
134
|
+
| (0x20u << 8) /* recipient: port 0 controller (AP 0x20) */
|
|
135
|
+
| 0x09u; /* command: Get Condition */
|
|
136
|
+
cmd[3] = 0x01000000u; /* function: controller */
|
|
137
|
+
DC_SB(DC_MDEN) = 1;
|
|
138
|
+
DC_SB(DC_MDSTAR) = phys_cmd;
|
|
139
|
+
DC_SB(DC_MDST) = 1; /* synchronous under the romdev/HLE core */
|
|
140
|
+
/* find the controller function-id (0x01000000) in the response; the kcode is the
|
|
141
|
+
* next word's low half. The response data starts after the frame header word. */
|
|
142
|
+
for (i = 0; i < 15; i++) {
|
|
143
|
+
if (rsp[i] == 0x01000000u) {
|
|
144
|
+
u32 kcode = rsp[i + 1] & 0xFFFF;
|
|
145
|
+
return ((~kcode) & DC_BTN_MASK);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
static inline int dc_pressed(u32 mask) { return (dc_pad() & mask) ? 1 : 0; }
|
|
151
|
+
|
|
152
|
+
#endif /* ROMDEV_DC_H */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ENTRY(_start)
|
|
2
|
+
MEMORY { ram (rwx) : ORIGIN = 0x8c010000, LENGTH = 0x800000 }
|
|
3
|
+
SECTIONS {
|
|
4
|
+
.text : { *(.text.start) *(.text*) *(.rodata*) } > ram
|
|
5
|
+
.data : { *(.data*) } > ram
|
|
6
|
+
. = ALIGN(4);
|
|
7
|
+
__bss_start = .;
|
|
8
|
+
.bss : { *(.bss*) *(COMMON) } > ram
|
|
9
|
+
. = ALIGN(4);
|
|
10
|
+
_end = .;
|
|
11
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|