romdevtools 0.70.0 → 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 +59 -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 +3 -3
- package/src/cores/capabilities.js +13 -9
- package/src/host/LibretroHost.js +242 -23
- package/src/host/callbacks.js +68 -1
- 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/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/mips-c/mips-c.js +35 -1
- package/src/toolchains/sh-c/lib/dc.h +65 -15
- package/src/toolchains/sh-c/sh-c.js +6 -3
|
@@ -42,6 +42,7 @@ import { decodeLynxMikey, decodeLynxPalette } from "../../host/lynx-mikey-state.
|
|
|
42
42
|
import { getPcePsgState } from "../../host/pce-psg-state.js";
|
|
43
43
|
import { decodePs1Spu } from "../../host/ps1-spu-state.js";
|
|
44
44
|
import { decodeN64Ai } from "../../host/n64-ai-state.js";
|
|
45
|
+
import { decodeAica } from "../../host/dc-aica-state.js";
|
|
45
46
|
import { getMsxAyState } from "../../host/msx-ay-state.js";
|
|
46
47
|
import { decodeGbaSprites, decodeGbaPalette } from "../../host/gba-video-state.js";
|
|
47
48
|
|
|
@@ -440,7 +441,14 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
440
441
|
if (!regs) throw new Error("getAudioState chip:'ai' — no AI region (load an N64 ROM into the rebuilt parallel_n64 core).");
|
|
441
442
|
return { platform: "n64", ...decodeN64Ai(regs) };
|
|
442
443
|
}
|
|
443
|
-
|
|
444
|
+
if (chip === "aica") {
|
|
445
|
+
// Dreamcast AICA — 64 PCM/ADPCM channels (key-on/volume/pitch/loop) + master
|
|
446
|
+
// volume, from the rebuilt flycast core's romdev_aica_get register window.
|
|
447
|
+
const regs = host.getAicaRegs?.();
|
|
448
|
+
if (!regs) throw new Error("getAudioState chip:'aica' — no AICA region (load a Dreamcast program into the rebuilt flycast core).");
|
|
449
|
+
return { platform: "dreamcast", ...decodeAica(regs) };
|
|
450
|
+
}
|
|
451
|
+
throw new Error(`getAudioState: unknown chip '${chip}'. Use 'nes' (NES 2A03), 'gb' (Game Boy/GBC), 'gba' (GBA), 'dsp' (SNES), 'psg' (Genesis/SMS/GG SN76489), 'ym2612' (Genesis FM), 'sid' (C64), 'mikey' (Lynx), 'pce', 'ay8910' (MSX), 'spu' (PS1), or 'aica' (Dreamcast).`);
|
|
444
452
|
}
|
|
445
453
|
|
|
446
454
|
getAudioStateCore = async ({ chip }, callerSessionKey) => jsonContent(readAudioChip(chip, callerSessionKey));
|
|
@@ -470,8 +470,16 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
470
470
|
return jsonContent(payload);
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, linkerConfigPath, inesHeader, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
|
|
473
|
+
async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, linkerConfigPath, inesHeader, options, defines, entry, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
|
|
474
474
|
const { buildForPlatform } = await import("../../toolchains/index.js");
|
|
475
|
+
// Merge `defines` ({_VER:1}) into `options` (--define) just like the project/rom paths,
|
|
476
|
+
// so a project-dir RUN honors them too.
|
|
477
|
+
const mergedOptions = [
|
|
478
|
+
...(Array.isArray(options) ? options : []),
|
|
479
|
+
...(defines && typeof defines === "object"
|
|
480
|
+
? Object.entries(defines).flatMap(([k, v]) => ["--define", `${k}=${v}`])
|
|
481
|
+
: []),
|
|
482
|
+
];
|
|
475
483
|
const resolved = resolveCore(platform);
|
|
476
484
|
if (!resolved) throw new Error(`no core available for platform '${platform}'`);
|
|
477
485
|
|
|
@@ -481,7 +489,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
481
489
|
// happy path; without it, output:'run' + path errored ("requires source").
|
|
482
490
|
const noExplicitSources = source == null && sourcePath == null && sources == null && sourcesPaths == null;
|
|
483
491
|
if (projPath && noExplicitSources) {
|
|
484
|
-
const r = await readProjectDir(projPath, platform);
|
|
492
|
+
const r = await readProjectDir(projPath, platform, { entry });
|
|
485
493
|
includes = { ...(includes ?? {}), ...r.includes };
|
|
486
494
|
binaryIncludes = { ...(binaryIncludes ?? {}), ...r.binaryIncludes };
|
|
487
495
|
if (r.crt0 != null) crt0 = r.crt0;
|
|
@@ -586,6 +594,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
586
594
|
crt0: crt0Rel2,
|
|
587
595
|
codeLoc,
|
|
588
596
|
dataLoc,
|
|
597
|
+
options: mergedOptions.length ? mergedOptions : undefined,
|
|
589
598
|
});
|
|
590
599
|
logBuildResult("build:run", platform, build);
|
|
591
600
|
if (!build.ok || !build.binary) {
|
|
@@ -703,7 +712,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
703
712
|
"• output:'rom' (default) — assemble or compile `source` (single) / `sources` ({name:contents}) / `sourcePath` / `sourcesPaths`. Returns the ROM (path by default; `inline:true` for binaryBase64) + build log. **`binaryIncludes`/`binaryIncludePaths` (base64/path CHR-ROM, music blobs for `.incbin`) — WITHOUT them no game with external assets builds.** `includes`/`includePaths` for `.include`d text. `linkerConfig` (cc65; NES preset 'chr-ram-runtime' RECOMMENDED). `crt0`/`crt0Path`/`codeLoc`/`dataLoc` (SDCC). `runtime`/`maxmod`/`rebuildSdk` (GBA/Genesis SDK). **`lint:'strict'` fails the build (stage:'lint', no binary) if the pre-flight SDCC crash-pattern scan flags anything (e.g. the uint8 loop-bound trap); 'advisory' (default) just lists hits in issues[].** **`includeSymbols:true` returns the .map text inline on a PLAIN rom build — distinct from output:'romWithDebug' which writes .dbg/.map FILES.** Language is inferred from extension/content — usually OMIT `language`.\n" +
|
|
704
713
|
"• output:'romWithDebug' — like 'rom' but also emits linker debug info for the `symbols` tool: cc65 → `.dbg`, SDCC → sdld `.map`, Genesis m68k → GNU ld map (find where a RAM var landed). DEFAULT writes ROM + debug file + log to disk (`outputPath` required unless `inline:true`). **`resolveSymbols:['grid','score']` folds those names' addresses ({resolvedSymbols:{grid:{address,hex,region?,ramOffset?}}}) straight into the result — the cheap way to a WRAM variable's address without loading the whole map (or round-tripping it through `symbols`).**\n" +
|
|
705
714
|
"• output:'run' — BUILD + LOAD + RUN + SCREENSHOT in one round trip — the fastest iteration loop. Same build args; runs `frames` frames and returns the screenshot INLINE. `holdInputs` holds controller state; `screenshotPath` writes the PNG to disk instead; `projectName` titles the playtest window.\n" +
|
|
706
|
-
"• output:'project' — build a project DIRECTORY (`path`) without re-passing the file manifest each call. Entry point
|
|
715
|
+
"• output:'project' — build a project DIRECTORY (`path`) without re-passing the file manifest each call. Entry point auto-detects `main.c` (C/SGDK Genesis, GBA, cc65/SDCC C) OR `main.s`/`main.asm` (asm); pass `entry:'smw.asm'` to point at a differently-named top-level file (e.g. an existing disassembly). Every `.c`/`.s`/`.asm` in the dir is a translation unit (linked together), every `.h`/`.inc` an include, and `.bin/.chr/.pcm/.brr/.vgm/...` (recursively, including subdirectories) become binaryIncludes (for `.incbin`). `options` (e.g. asar `--define _VER=1`) and `defines` are honored here too. Iterate an on-disk project by re-calling with just `{path, platform}`. **This is the no-boilerplate path for an examples({op:'fork'}) dir: the per-platform recipe auto-supplies the crt0 + load address — GB/GBC default `gb_crt0.s` + `codeLoc:0x150` (don't hand-pass them!), MSX routes `msx_crt0.s` + `codeLoc:0x4010`, SMS/GG auto-inject their bundled crt0, NES applies the chr-ram-runtime preset. PREFER this over re-passing `crt0Path`/`codeLoc` to output:'rom' for a forked project.**",
|
|
707
716
|
{
|
|
708
717
|
output: z.enum(["rom", "romWithDebug", "run", "project"])
|
|
709
718
|
.describe("rom=produce a ROM (default); romWithDebug=ROM + .dbg/.map debug files; run=build+load+run+screenshot; project=build a project directory."),
|
|
@@ -722,7 +731,8 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
722
731
|
crt0Path: z.string().optional().describe("Path-based `crt0`. NOT read by output:'romWithDebug' — pass `crt0` there."),
|
|
723
732
|
codeLoc: z.coerce.number().int().optional().describe("SDCC — _CODE load address (default $0000; GB/GBC bundled crt0 wants 0x150)."),
|
|
724
733
|
dataLoc: z.coerce.number().int().optional().describe("SDCC — _DATA (WRAM) load address (default $C000 on Z80). NOT read by output:'romWithDebug'."),
|
|
725
|
-
options: z.array(z.string()).optional().describe("output:'rom'
|
|
734
|
+
options: z.array(z.string()).optional().describe("Extra toolchain CLI options. Honored by output:'rom' AND output:'project' (e.g. asar `--define _VER=1`, `--fix-checksum=off`, `-wno…`)."),
|
|
735
|
+
defines: z.record(z.string(), z.union([z.string(), z.number()])).optional().describe("Assembler defines as a map ({_VER:1}) — convenience for asar's `--define NAME=VALUE`. Merged into `options` for output:'rom'/'project'. (Equivalent to passing `--define _VER=1` yourself.)"),
|
|
726
736
|
linkerConfig: z.string().optional().describe("ld65 linker config (cc65). NES presets: 'chr-ram-runtime' (RECOMMENDED for homebrew C — full crt0 + iNES header + NMI w/ OAM DMA + `_shadow_oam` at $0200), 'chr-ram' (bare nmi:rti stub), 'chr-rom' (cc65-C with FIXED CHR-ROM art — segment split + CHARS segment; supply CHR via binaryIncludePaths into a CHARS source + the header via `inesHeader`). Or full .cfg contents. Preset NAMES only resolve on output:'rom'/'run'; output:'romWithDebug' takes raw .cfg contents only. **For rebuilding a commercial NROM game from its disassembly, prefer `inesHeader` over a raw .cfg.**"),
|
|
727
737
|
linkerConfigPath: z.string().optional().describe("Path-based `linkerConfig`: absolute path to a .cfg file on disk (the server reads it — the cfg never enters your context; e.g. the multi-bank cfg a banked-NES disasm project ships). Ignored when `linkerConfig` is passed inline."),
|
|
728
738
|
inesHeader: z.object({
|
|
@@ -746,6 +756,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
746
756
|
projectName: z.string().optional().describe("output:'run' — playtest window title (no effect on the ROM)."),
|
|
747
757
|
// project-only
|
|
748
758
|
path: z.string().optional().describe("output:'project' — absolute path to the project directory."),
|
|
759
|
+
entry: z.string().optional().describe("output:'project' — name of the top-level source file when it isn't main.c/main.s/main.asm (e.g. 'smw.asm' for an existing disassembly). Project-relative or a bare filename. Default: auto-detect main.c / main.s / main.asm."),
|
|
749
760
|
// shared output
|
|
750
761
|
outputPath: z.string().optional().describe("output:'rom'/'romWithDebug'/'project' — absolute path to write the ROM (romWithDebug writes .dbg/.map/.log alongside; REQUIRED for romWithDebug unless inline:true). output:'rom' omitted → temp-file path returned (or inline:true for base64)."),
|
|
751
762
|
inline: z.boolean().default(false).describe("output:'rom'/'romWithDebug' — return binaryBase64 (+ debug text for romWithDebug) in the response instead of writing to disk."),
|
|
@@ -905,6 +916,45 @@ export function projectBuildRecipe(platform, names) {
|
|
|
905
916
|
return r;
|
|
906
917
|
}
|
|
907
918
|
|
|
919
|
+
/**
|
|
920
|
+
* Recursively collect files in SUBDIRECTORIES of a project (top-level files are
|
|
921
|
+
* handled by the recipe). Returns {rel, abs} with `rel` POSIX-relative to root
|
|
922
|
+
* (e.g. "col/misc/x.pal"), so the asar/incbin mount resolves the same path the
|
|
923
|
+
* source references. Skips dot-dirs and common build/VCS dirs to avoid hauling
|
|
924
|
+
* in junk. Caps total files to keep a pathological tree from blowing up.
|
|
925
|
+
* @param {string} root
|
|
926
|
+
* @returns {Promise<Array<{rel:string, abs:string}>>}
|
|
927
|
+
*/
|
|
928
|
+
async function walkSubdirAssets(root) {
|
|
929
|
+
/** @type {Array<{rel:string, abs:string}>} */
|
|
930
|
+
const out = [];
|
|
931
|
+
const SKIP_DIR = new Set([".git", ".svn", "node_modules", "out", "build", "obj", ".vscode"]);
|
|
932
|
+
const MAX = 5000;
|
|
933
|
+
async function walk(dir, rel) {
|
|
934
|
+
let ents;
|
|
935
|
+
try { ents = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
936
|
+
for (const e of ents) {
|
|
937
|
+
if (out.length >= MAX) return;
|
|
938
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
939
|
+
if (e.isDirectory()) {
|
|
940
|
+
if (e.name.startsWith(".") || SKIP_DIR.has(e.name)) continue;
|
|
941
|
+
await walk(path.join(dir, e.name), childRel);
|
|
942
|
+
} else if (e.isFile()) {
|
|
943
|
+
out.push({ rel: childRel, abs: path.join(dir, e.name) });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// Only descend into subdirectories (top-level handled by the flat loop/recipe).
|
|
948
|
+
let top;
|
|
949
|
+
try { top = await readdir(root, { withFileTypes: true }); } catch { return out; }
|
|
950
|
+
for (const e of top) {
|
|
951
|
+
if (e.isDirectory() && !e.name.startsWith(".") && !SKIP_DIR.has(e.name)) {
|
|
952
|
+
await walk(path.join(root, e.name), e.name);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return out;
|
|
956
|
+
}
|
|
957
|
+
|
|
908
958
|
/**
|
|
909
959
|
* Read a scaffolded project DIRECTORY into the build inputs, applying the
|
|
910
960
|
* per-platform recipe (crt0 routing, linker preset, runtime, skip-list) and
|
|
@@ -913,17 +963,40 @@ export function projectBuildRecipe(platform, names) {
|
|
|
913
963
|
* can never drift. Returns crt0 as RAW source text (callers assemble it).
|
|
914
964
|
* @param {string} projPath
|
|
915
965
|
* @param {string} platform
|
|
966
|
+
* @param {{entry?: string}} [opts] entry: name of the top-level source when it isn't main.*
|
|
916
967
|
*/
|
|
917
|
-
export async function readProjectDir(projPath, platform) {
|
|
968
|
+
export async function readProjectDir(projPath, platform, opts = {}) {
|
|
918
969
|
const entries = await readdir(projPath, { withFileTypes: true });
|
|
919
970
|
const files = entries.filter((e) => e.isFile());
|
|
920
971
|
|
|
972
|
+
// Subdirectory assets — a real disassembly keeps palettes/gfx in nested dirs
|
|
973
|
+
// (e.g. col/misc/back_area.pal) and `.incbin`s them by relative path. The old
|
|
974
|
+
// flat readdir never saw them → "file not found". Walk recursively and add any
|
|
975
|
+
// binary asset (regardless of extension/double-extension) keyed by its path
|
|
976
|
+
// RELATIVE to the project root, so the asar/incbin mount resolves it.
|
|
977
|
+
const subAssets = await walkSubdirAssets(projPath);
|
|
978
|
+
|
|
979
|
+
// Entry override: an existing project whose top file isn't main.* (e.g.
|
|
980
|
+
// smw.asm). Accept a project-relative or bare name; it becomes the single
|
|
981
|
+
// entry source. Without it, auto-detect main.c / main.s / main.asm.
|
|
982
|
+
const entryName = opts.entry ? path.normalize(opts.entry).replace(/^[./]+/, "") : null;
|
|
983
|
+
if (entryName) {
|
|
984
|
+
const exists = files.some((f) => f.name === entryName) ||
|
|
985
|
+
subAssets.some((a) => a.rel === entryName);
|
|
986
|
+
if (!exists) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`entry '${entryName}' not found in ${projPath}. Found top-level: ` +
|
|
989
|
+
`${files.map((f) => f.name).join(", ") || "(none)"}.`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
921
994
|
const hasC = files.some((f) => f.name === "main.c");
|
|
922
995
|
const hasAsm = files.some((f) => f.name === "main.s" || f.name === "main.asm");
|
|
923
|
-
if (!hasC && !hasAsm) {
|
|
996
|
+
if (!entryName && !hasC && !hasAsm) {
|
|
924
997
|
throw new Error(
|
|
925
|
-
`no entry point in ${projPath}: expected main.c (C/SGDK/GBA/cc65 project) or main.s / main.asm (asm project)
|
|
926
|
-
`Found: ${files.map((f) => f.name).join(", ") || "(empty)"}.`
|
|
998
|
+
`no entry point in ${projPath}: expected main.c (C/SGDK/GBA/cc65 project) or main.s / main.asm (asm project), ` +
|
|
999
|
+
`or pass entry:'<top-file>'. Found: ${files.map((f) => f.name).join(", ") || "(empty)"}.`
|
|
927
1000
|
);
|
|
928
1001
|
}
|
|
929
1002
|
|
|
@@ -933,6 +1006,15 @@ export async function readProjectDir(projPath, platform) {
|
|
|
933
1006
|
// the dir build matches the hand-written build({output:'run'}) call.
|
|
934
1007
|
const recipe = projectBuildRecipe(platform, files.map((f) => f.name));
|
|
935
1008
|
|
|
1009
|
+
// With a custom entry, the entry is the ONE source; every OTHER top-level
|
|
1010
|
+
// .asm/.s/.c routes as an include (asar/wla resolve `.include`/`#include`
|
|
1011
|
+
// from the includes mount), matching the single-source asm model.
|
|
1012
|
+
if (entryName && /\.(asm|s)$/i.test(entryName)) {
|
|
1013
|
+
for (const f of files) {
|
|
1014
|
+
if (/\.(c|s|asm)$/i.test(f.name) && f.name !== entryName) recipe.includeAsC.add(f.name);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
936
1018
|
/** @type {Record<string,string>} */ const sources = {};
|
|
937
1019
|
/** @type {Record<string,string>} */ const includes = {};
|
|
938
1020
|
/** @type {Record<string,string>} */ const binaryIncludes = {};
|
|
@@ -953,6 +1035,19 @@ export async function readProjectDir(projPath, platform) {
|
|
|
953
1035
|
}
|
|
954
1036
|
}
|
|
955
1037
|
|
|
1038
|
+
// Stage subdirectory assets (any extension) keyed by their path relative to the
|
|
1039
|
+
// project root, so `incbin "col/misc/X.pal"` / `.incbin "gfx/a.bin"` resolve in
|
|
1040
|
+
// the mount. Text-ish includes (.asm/.s/.h/.inc) found in subdirs go to includes
|
|
1041
|
+
// (so `incsrc "lib/foo.asm"` works); everything else is a binary asset.
|
|
1042
|
+
for (const a of subAssets) {
|
|
1043
|
+
if (recipe.skip.has(a.rel) || a.rel === entryName) continue;
|
|
1044
|
+
if (/\.(h|inc|asm|s)$/i.test(a.rel)) {
|
|
1045
|
+
includes[a.rel] = (await readFile(a.abs)).toString("utf-8");
|
|
1046
|
+
} else {
|
|
1047
|
+
binaryIncludes[a.rel] = (await readFile(a.abs)).toString("base64");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
956
1051
|
// SMS/GG with no crt0 file in the dir → fall back to the bundled crt0,
|
|
957
1052
|
// exactly like the output:'rom'/'run' handlers do. Without this the link
|
|
958
1053
|
// silently uses SDCC's stock z80 crt0, which never calls main() (black
|
|
@@ -972,8 +1067,17 @@ export async function readProjectDir(projPath, platform) {
|
|
|
972
1067
|
return { sources, includes, binaryIncludes, crt0, codeLoc: recipe.codeLoc, dataLoc: recipe.dataLoc, linkerConfig: recipe.linkerConfig, runtime, maxmod: recipe.maxmod };
|
|
973
1068
|
}
|
|
974
1069
|
|
|
975
|
-
export async function buildProjectCore({ path: projPath, platform, outputPath }) {
|
|
976
|
-
const { sources, includes, binaryIncludes, crt0, codeLoc, dataLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform);
|
|
1070
|
+
export async function buildProjectCore({ path: projPath, platform, outputPath, options, defines, entry }) {
|
|
1071
|
+
const { sources, includes, binaryIncludes, crt0, codeLoc, dataLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform, { entry });
|
|
1072
|
+
|
|
1073
|
+
// Thread `options` (e.g. asar `--define _VER=1`, `--fix-checksum=off`) + the `defines`
|
|
1074
|
+
// convenience map through to the toolchain — project mode used to silently drop them.
|
|
1075
|
+
const mergedOptions = [
|
|
1076
|
+
...(Array.isArray(options) ? options : []),
|
|
1077
|
+
...(defines && typeof defines === "object"
|
|
1078
|
+
? Object.entries(defines).flatMap(([k, v]) => ["--define", `${k}=${v}`])
|
|
1079
|
+
: []),
|
|
1080
|
+
];
|
|
977
1081
|
|
|
978
1082
|
// Linker preset: the recipe names it (e.g. NES 'chr-ram-runtime', which ships
|
|
979
1083
|
// the OAM/CHARS segments + its own crt0). resolveLinkerConfig also returns any
|
|
@@ -1012,6 +1116,7 @@ export async function buildProjectCore({ path: projPath, platform, outputPath })
|
|
|
1012
1116
|
crt0: crt0Rel,
|
|
1013
1117
|
codeLoc,
|
|
1014
1118
|
dataLoc,
|
|
1119
|
+
options: mergedOptions.length ? mergedOptions : undefined,
|
|
1015
1120
|
});
|
|
1016
1121
|
if (outputPath && result.binary) {
|
|
1017
1122
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Sega Dreamcast — mental model
|
|
2
|
+
|
|
3
|
+
The Dreamcast is a **3D machine**: a 200 MHz Hitachi SH-4 CPU (with an on-die FPU/
|
|
4
|
+
vector unit) + the PowerVR2 (CLX2) **tile-based deferred renderer** drawing into
|
|
5
|
+
8 MB VRAM, plus the AICA sound system (an ARM7 CPU + DSP). romdev runs it on
|
|
6
|
+
**Flycast** built to WASM, presenting through the **native-gles / WebGL2 hardware
|
|
7
|
+
GPU**, with the **reios HLE BIOS** so you don't ship a real `dc_boot.bin`.
|
|
8
|
+
|
|
9
|
+
## The one thing to know about rendering
|
|
10
|
+
|
|
11
|
+
Flycast is **GPU-first**: the PowerVR2 is a Tile Accelerator (TA) — homebrew normally
|
|
12
|
+
submits TA display lists (polygon/vertex lists) that the GPU rasterizes. romdev runs
|
|
13
|
+
Flycast `hwRender: true` (the real GPU via WebGL2). **BUT** the core is configured
|
|
14
|
+
with `flycast_emulate_framebuffer: enabled`, which gives a working **direct-framebuffer
|
|
15
|
+
scanout** path — a program that writes the DC framebuffer displays (verified: a
|
|
16
|
+
framebuffer-writing program captured ~727k pixels). So both routes work:
|
|
17
|
+
|
|
18
|
+
- **Direct framebuffer** (simplest for 2D / first pixels): write VRAM and let the
|
|
19
|
+
emulated-framebuffer path scan it out. The starting point for "just show something."
|
|
20
|
+
- **TA / GPU** (the real path for 3D): submit PowerVR2 TA lists. The native SDK,
|
|
21
|
+
**KallistiOS (KOS)**, wraps the TA and ships an OpenGL port — lean on KOS rather than
|
|
22
|
+
hand-rolling the TA command encoding.
|
|
23
|
+
|
|
24
|
+
Decide which your homebrew uses; the bundled helper lib targets the framebuffer path
|
|
25
|
+
first (like a software front end) and grows toward TA.
|
|
26
|
+
|
|
27
|
+
## CPU / memory map (SH-4, 32-bit, little-endian)
|
|
28
|
+
|
|
29
|
+
| Region | Address | Notes |
|
|
30
|
+
|--------|---------|-------|
|
|
31
|
+
| System RAM | `0x8C00_0000`+ | 16 MB main RAM (KOS links code here) |
|
|
32
|
+
| VRAM | `0xA500_0000`+ | 8 MB texture/framebuffer RAM |
|
|
33
|
+
| Hardware regs | `0xA05F_xxxx` | PowerVR2 (TA/CORE), Holly, GD-ROM, AICA, Maple |
|
|
34
|
+
| AICA RAM | `0x00800000` (AICA bus) | ARM7 sound RAM |
|
|
35
|
+
|
|
36
|
+
Addresses are **little-endian**. SH-4 uses **PC-relative loads** for constants/
|
|
37
|
+
addresses (`mov.l @(disp,PC)`) heavily — relevant for RE base-address alignment.
|
|
38
|
+
`memory({op:'read', region:'system_ram'})` reads main RAM.
|
|
39
|
+
|
|
40
|
+
## Booting — KOS ELF + reios HLE (no disc needed for the dev loop)
|
|
41
|
+
|
|
42
|
+
A real Dreamcast boots from a GD-ROM with an `IP.BIN` bootstrap (a CDI/CHD disc image).
|
|
43
|
+
For homebrew, that's heavy. romdev uses Flycast's **reios HLE BIOS** (`flycast_hle_bios:
|
|
44
|
+
enabled`), and KallistiOS produces a ready-to-boot **ELF** — Flycast loads the raw KOS
|
|
45
|
+
ELF directly, so you skip the disc-image dance. (Build a disc image only if a specific
|
|
46
|
+
title demands it.) `build({platform:'dreamcast'})` produces the loadable image.
|
|
47
|
+
|
|
48
|
+
## Input
|
|
49
|
+
|
|
50
|
+
Homebrew reads controllers over the **Maple bus** (`0xA05F_6C00`+) — the DC's
|
|
51
|
+
peripheral protocol. KOS abstracts this (`maple_enum_*`, controller state). The host's
|
|
52
|
+
`setInput`/`run` holdInputs drive the emulated controller; the game polls Maple.
|
|
53
|
+
|
|
54
|
+
## Sound
|
|
55
|
+
|
|
56
|
+
The **AICA** = an ARM7 CPU + DSP with its own RAM. The SH-4 uploads a sound program +
|
|
57
|
+
samples to AICA RAM and triggers channels. KOS ships an AICA sound driver
|
|
58
|
+
(`snd_stream`). `audioChips` lists `aica`.
|
|
59
|
+
|
|
60
|
+
## MCP debug & inspection tooling — current state
|
|
61
|
+
|
|
62
|
+
- **`run` / `screenshot` / `frame({op:'verify'})`** — boots + presents + render-health.
|
|
63
|
+
✅ (reios HLE; the framebuffer path captures pixels.)
|
|
64
|
+
- **`memory({op:'read', region:'system_ram'})`** — SH-4 main RAM. ✅
|
|
65
|
+
- **`disasm` / `decompile`** — SH-4 via rizin (`sh`) + Ghidra (SuperH4 SLEIGH). ✅
|
|
66
|
+
(Good decompile quality; SH-4 is a clean 32-bit RISC.)
|
|
67
|
+
- **`build`** — sh-elf-gcc → WASM toolchain. ✅
|
|
68
|
+
- **`cpu({op:'read'})`** — live SH-4 register file (`romdev_sh4_regs_get`: r0..r15, pc,
|
|
69
|
+
pr, gbr, vbr, sr, mac, fpul + decoded SR flags). ✅
|
|
70
|
+
- **`audioDebug({op:'inspect', chip:'aica'})`** — the AICA's 64 PCM/ADPCM channels
|
|
71
|
+
(key-on / volume / pitch / loop / format) + master volume (`romdev_aica_get`). ✅
|
|
72
|
+
- **`breakpoint` / `watch`** — not yet (need interpreter-step + memory-path hooks in
|
|
73
|
+
flycast; cpuState/audioDebug are plain reads and ARE wired).
|
|
74
|
+
- **`renderingContext`** is **N/A** — 3D TA machine, no 2D tile VDP to decode.
|
|
75
|
+
|
|
76
|
+
## Build pipeline
|
|
77
|
+
|
|
78
|
+
`build({platform:'dreamcast'})` cross-compiles with an **sh-elf-gcc → WASM** toolchain
|
|
79
|
+
(adapted from KOS's `dc-chain`; SH-4 is single-endian little, so no be/el split). Link
|
|
80
|
+
against KallistiOS for libc + the TA/AICA/Maple drivers. `#include` the bundled
|
|
81
|
+
`dc.h` helper for the framebuffer front end + input/audio helpers.
|
|
82
|
+
|
|
83
|
+
## What's NOT bundled / hardware limits
|
|
84
|
+
|
|
85
|
+
- No GD-ROM/CDDA streaming or VMU save emulation in the dev loop.
|
|
86
|
+
- `breakpoint`/`watch` not yet (need interpreter-step hooks; cpuState/audioDebug ARE wired).
|
|
87
|
+
- `renderingContext` is N/A (3D TA, no tile VDP).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Sega Dreamcast — troubleshooting
|
|
2
|
+
|
|
3
|
+
Read `platform({op:'doc', platform:'dreamcast', name:'mental_model'})` first — DC runs
|
|
4
|
+
on Flycast (hardware GPU + reios HLE BIOS), with a working emulated-framebuffer path.
|
|
5
|
+
|
|
6
|
+
## "ROM/ELF builds but won't boot"
|
|
7
|
+
|
|
8
|
+
1. **reios HLE must be on.** Flycast only boots a raw homebrew ELF via the **reios HLE
|
|
9
|
+
BIOS** (`flycast_hle_bios: enabled`) — it parses the ELF + jumps. With it off,
|
|
10
|
+
Flycast wants a real `dc_boot.bin` you don't ship. romdev's DC core config already
|
|
11
|
+
enables reios; if you swapped the core/config, re-enable it.
|
|
12
|
+
2. **You built a flat binary, not a KOS ELF.** Flycast loads the KallistiOS ELF
|
|
13
|
+
(linked at `0x8C00_0000`). Use `build({platform:'dreamcast'})` — it links against
|
|
14
|
+
KOS and emits the loadable image. A bare `.bin` with no ELF/entry won't boot.
|
|
15
|
+
|
|
16
|
+
## "Boots but the screen is BLACK"
|
|
17
|
+
|
|
18
|
+
1. **Nothing was drawn to the framebuffer OR submitted to the TA.** DC has two render
|
|
19
|
+
routes (see mental model). For the simple path, write the DC framebuffer (the
|
|
20
|
+
`flycast_emulate_framebuffer: enabled` path scans it out — a framebuffer-writing
|
|
21
|
+
program shows pixels). For 3D, submit PowerVR2 **TA** lists via KOS. A blank screen
|
|
22
|
+
= neither happened. Confirm with `frame({op:'verify'})` (nearlyBlank).
|
|
23
|
+
2. **No display/render init.** The PowerVR2 needs its video mode + render target set
|
|
24
|
+
up (KOS `vid_set_mode` / `pvr_init`). Without it there's no scanout. Call the KOS
|
|
25
|
+
init (or the helper lib's init) before drawing.
|
|
26
|
+
3. **You expected raw VRAM writes to a TA-only setup to show.** With the framebuffer-
|
|
27
|
+
emulation path enabled they do; if you disabled it and use pure TA, only submitted
|
|
28
|
+
TA geometry renders.
|
|
29
|
+
|
|
30
|
+
## "Geometry/3D is wrong or missing (TA path)"
|
|
31
|
+
|
|
32
|
+
PowerVR2 is a **tile-based DEFERRED** renderer — opaque, punch-through, and translucent
|
|
33
|
+
lists are submitted + sorted per tile. Submitting polys to the wrong list type, or not
|
|
34
|
+
finalizing the list, drops them. Use KOS's `pvr_*` list API (it orders this correctly)
|
|
35
|
+
rather than hand-encoding TA commands.
|
|
36
|
+
|
|
37
|
+
## "disasm/decompile returns junk addresses on a multi-function program"
|
|
38
|
+
|
|
39
|
+
SH-4 decompiles well, but it uses **PC-relative loads** (`mov.l @(disp,PC)`) heavily,
|
|
40
|
+
so the analysis buffer's flat offsets must line up with the binary's load VA or
|
|
41
|
+
constant/address resolution breaks. Build a **multi-function** test program to verify
|
|
42
|
+
`functions` returns real VAs that round-trip — a single-instruction smoke test hides
|
|
43
|
+
base-address misalignment. (Same class of trap as the PS1 rebase issue.)
|
|
44
|
+
|
|
45
|
+
## "breakpoint / watch return N/A"
|
|
46
|
+
|
|
47
|
+
`cpu({op:'read'})` and `audioDebug({op:'inspect', chip:'aica'})` ARE wired on DC (the
|
|
48
|
+
flycast core exports `romdev_sh4_regs_get` + `romdev_aica_get`). `breakpoint`/`watch`
|
|
49
|
+
are not yet — those need interpreter-step + memory-path hooks in flycast (cpuState/
|
|
50
|
+
audioDebug are plain register reads). Use `cpu`/`audioDebug` + `memory` +
|
|
51
|
+
`disasm`/`decompile` meanwhile.
|
|
52
|
+
|
|
53
|
+
## "renderingContext returns N/A"
|
|
54
|
+
|
|
55
|
+
Correct — DC is a 3D TA machine with no 2D tile/sprite VDP for that op. Not a bug.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Sega Dreamcast — source you can read
|
|
2
|
+
|
|
3
|
+
Trust hierarchy (try in order before filing a feedback round):
|
|
4
|
+
|
|
5
|
+
1. **Bundled examples** (`examples/dreamcast/{hello,shmup,platformer,puzzle,racing,sports}/main.c`)
|
|
6
|
+
— verified building + rendering on the GPU (full 480-line frame). Start here.
|
|
7
|
+
2. **Bundled helper lib source** (`src/toolchains/sh-c/lib/dc.h`) — the PowerVR2
|
|
8
|
+
framebuffer bring-up + drawing + input. Read this when something doesn't render or input
|
|
9
|
+
doesn't read: it shows `dc_video_init` (FB_R_CTRL/SIZE/SOF1 + SPG, **480i interlace** —
|
|
10
|
+
240p only shows the top 240 lines), `dc_clear`/`dc_rect`, and the Maple-DMA `dc_pad()`.
|
|
11
|
+
3. **The core source** (NOT bundled — fetch on demand):
|
|
12
|
+
|
|
13
|
+
| What | Upstream |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Flycast (libretro core, PowerVR2 + reios HLE BIOS) | https://github.com/flyinghead/flycast |
|
|
16
|
+
| SH-4 / PowerVR2 / AICA / Maple internals | (in the flycast tree: `core/hw/sh4/`, `core/hw/pvr/`, `core/hw/aica/`, `core/hw/maple/`) |
|
|
17
|
+
|
|
18
|
+
The cpuState/audioDebug exports we patch in (`romdev_sh4_regs_get`, `romdev_aica_get`)
|
|
19
|
+
live in `scripts/patches/romdev-snippets/flycast-debug.c`.
|
|
20
|
+
|
|
21
|
+
4. **The toolchain** — a from-scratch `sh-elf-gcc` cross-compiler (little-endian SH-4,
|
|
22
|
+
m4-single-only FP) built to WASM (`scripts/build-sh-toolchain.sh` +
|
|
23
|
+
`build-sh-wasm-tools.sh`). NOTE: cc1 defaults to **-O1** (the sh-elf cc1.wasm has an
|
|
24
|
+
-O2-only crash on common control flow):
|
|
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 SH-4 (SuperH4 SLEIGH):
|
|
33
|
+
|
|
34
|
+
| What | Upstream |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Rizin | https://github.com/rizinorg/rizin |
|
|
37
|
+
| rz-ghidra | https://github.com/rizinorg/rz-ghidra |
|
|
38
|
+
|
|
39
|
+
## Dreamcast hardware docs
|
|
40
|
+
|
|
41
|
+
- **KallistiOS (KOS)** — the canonical open DC SDK; read its PowerVR2 TA, AICA, and Maple
|
|
42
|
+
drivers for the standard register sequences: https://github.com/KallistiOS/KallistiOS
|
|
43
|
+
- **Mc Spankled / DCEmulation docs + the Sega Dreamcast Hardware Specification** (PVR2/Holly
|
|
44
|
+
register maps): cross-check against flycast's `core/hw/pvr/pvr_regs.h` enum.
|
|
45
|
+
- **SH-4 ISA / SH7750 manual** (Renesas) for the CPU.
|
|
46
|
+
|
|
47
|
+
## When to use what
|
|
48
|
+
|
|
49
|
+
- "Boots but the screen is BLACK / only the top half draws" → MENTAL_MODEL +
|
|
50
|
+
TROUBLESHOOTING (it's the framebuffer path + the 480i-interlace requirement).
|
|
51
|
+
- "How does the helper set up the PowerVR2 framebuffer?" → `dc.h` `dc_video_init` +
|
|
52
|
+
flycast `core/hw/pvr/pvr_regs.h` (SPG_CONTROL interlace bit).
|
|
53
|
+
- "dc_pad doesn't reflect button presses" → `dc.h` `dc_pad()` (Maple Get-Condition DMA) +
|
|
54
|
+
flycast `core/hw/maple/maple_devs.cpp` (the controller response framing). Reads the
|
|
55
|
+
resting state; press mapping is a known follow-up.
|
|
56
|
+
- "Which AICA register holds channel key-on/volume?" → `dc-aica-state.js` decode +
|
|
57
|
+
flycast `core/hw/aica/`.
|
|
@@ -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).
|