romdevtools 0.14.0 → 0.16.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/AGENTS.md +18 -11
- package/CHANGELOG.md +94 -0
- package/README.md +1 -1
- package/examples/atari2600/main.asm +1 -1
- package/examples/atari2600/templates/default.asm +1 -1
- package/examples/atari2600/templates/paddle.asm +59 -47
- package/examples/atari7800/main.c +1 -1
- package/examples/atari7800/templates/default.c +1 -1
- package/examples/atari7800/templates/music_demo.c +1 -1
- package/examples/c64/main.c +1 -1
- package/examples/c64/templates/platformer.c +2 -2
- package/examples/c64/templates/puzzle.c +1 -1
- package/examples/c64/templates/racing.c +3 -3
- package/examples/c64/templates/shmup.c +6 -5
- package/examples/c64/templates/sports.c +4 -4
- package/examples/gb/main.asm +1 -1
- package/examples/gb/main.c +1 -1
- package/examples/gb/templates/puzzle.c +1 -1
- package/examples/gb/templates/racing.c +1 -1
- package/examples/gb/templates/shmup.c +1 -1
- package/examples/gba/templates/gba_hello.c +1 -1
- package/examples/gba/templates/maxmod_demo.c +1 -1
- package/examples/gba/templates/puzzle.c +17 -3
- package/examples/gba/templates/racing.c +16 -2
- package/examples/gba/templates/shmup.c +23 -4
- package/examples/gba/templates/tonc_hello.c +6 -4
- package/examples/gbc/main.asm +1 -1
- package/examples/gbc/templates/puzzle.c +1 -1
- package/examples/gbc/templates/racing.c +1 -1
- package/examples/gbc/templates/shmup.c +1 -1
- package/examples/genesis/main.s +1 -1
- package/examples/genesis/templates/puzzle.c +1 -1
- package/examples/genesis/templates/racing.c +45 -1
- package/examples/genesis/templates/shmup.c +12 -3
- package/examples/genesis/templates/shmup_2p.c +2 -2
- package/examples/genesis/templates/sports.c +39 -0
- package/examples/gg/templates/hello_sprite.c +38 -23
- package/examples/gg/templates/music_demo.c +11 -8
- package/examples/gg/templates/platformer.c +37 -15
- package/examples/gg/templates/racing.c +25 -12
- package/examples/gg/templates/shmup.c +12 -6
- package/examples/gg/templates/sports.c +30 -16
- package/examples/gg/templates/tile_engine.c +24 -10
- package/examples/lynx/templates/platformer.c +7 -1
- package/examples/lynx/templates/puzzle.c +8 -2
- package/examples/lynx/templates/racing.c +7 -1
- package/examples/lynx/templates/sports.c +7 -1
- package/examples/nes/main.c +2 -2
- package/examples/nes/space-shooter/nes_runtime.h +1 -1
- package/examples/nes/templates/default.c +4 -1
- package/examples/nes/templates/racing.c +50 -1
- package/examples/pce/main.c +1 -1
- package/examples/sms/templates/hello_sprite.c +1 -1
- package/examples/sms/templates/music_demo.c +1 -1
- package/examples/sms/templates/puzzle.c +1 -1
- package/examples/sms/templates/racing.c +1 -1
- package/examples/sms/templates/shmup.c +1 -1
- package/examples/sms/templates/shmup_2p.c +2 -2
- package/examples/snes/main.asm +1 -1
- package/examples/snes/templates/c-hello-data.asm +309 -14
- package/examples/snes/templates/c-hello.c +13 -2
- package/examples/snes/templates/default.c +1 -1
- package/examples/snes/templates/hello_sprite-data.asm +300 -2
- package/examples/snes/templates/hello_sprite.c +10 -1
- package/examples/snes/templates/music_demo-data.asm +300 -2
- package/examples/snes/templates/music_demo.c +10 -1
- package/examples/snes/templates/platformer-data.asm +300 -2
- package/examples/snes/templates/platformer.c +10 -1
- package/examples/snes/templates/puzzle-data.asm +300 -2
- package/examples/snes/templates/puzzle.c +11 -1
- package/examples/snes/templates/racing-data.asm +300 -2
- package/examples/snes/templates/racing.c +40 -4
- package/examples/snes/templates/shmup-data.asm +299 -6
- package/examples/snes/templates/shmup.c +11 -7
- package/examples/snes/templates/sports-data.asm +300 -2
- package/examples/snes/templates/sports.c +40 -5
- package/package.json +1 -1
- package/src/http/skill-doc.js +1 -1
- package/src/http/tool-registry.js +1 -1
- package/src/mcp/tools/index.js +4 -4
- package/src/mcp/tools/project.js +33 -22
- package/src/mcp/tools/toolchain.js +196 -20
- package/src/observer/livestream.html +34 -4
- package/src/platforms/gg/MENTAL_MODEL.md +14 -13
- package/src/platforms/gg/lib/c/vdp_init.c +10 -8
- package/src/platforms/msx/MENTAL_MODEL.md +1 -1
- package/src/platforms/nes/TROUBLESHOOTING.md +1 -1
- package/src/platforms/nes/lib/c/nes_runtime.c +28 -6
- package/src/platforms/pce/MENTAL_MODEL.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +1 -0
- package/src/platforms/pce/lib/c/pce_video.c +26 -0
- package/src/platforms/sms/MENTAL_MODEL.md +12 -12
- package/src/platforms/sms/lib/c/vdp_init.c +10 -8
- package/src/platforms/sms/lib/vdp_init.s +1 -1
- package/src/toolchains/cc65/cc65.js +8 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.crt0.s +1 -1
- package/src/toolchains/gba-c/gba-c.js +6 -1
- package/src/toolchains/genesis-c/README.md +1 -1
- package/src/toolchains/genesis-c/genesis-c.js +10 -2
- package/src/toolchains/parse-errors.js +67 -5
- package/src/toolchains/sdcc/preflight-lint.js +47 -7
- package/src/toolchains/snes-c/snes-c.js +3 -7
|
@@ -68,6 +68,7 @@ const LOG_TAIL = 1200;
|
|
|
68
68
|
// stays visible. Left intact on failure (timing can matter when diagnosing a
|
|
69
69
|
// hang/OOM). The full untrimmed log is still written to logPath when spilled.
|
|
70
70
|
export function denoiseSuccessLog(log) {
|
|
71
|
+
if (typeof log !== "string") return log ?? "";
|
|
71
72
|
const lines = log.split("\n");
|
|
72
73
|
const kept = [];
|
|
73
74
|
let inTiming = false;
|
|
@@ -293,7 +294,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
293
294
|
codeLoc,
|
|
294
295
|
dataLoc,
|
|
295
296
|
});
|
|
296
|
-
logBuildResult("
|
|
297
|
+
logBuildResult("build:rom", platform, result);
|
|
297
298
|
// lint:"strict" — if any lint warning fired, fail the build with
|
|
298
299
|
// stage:"lint" so the agent must fix patterns before iterating.
|
|
299
300
|
// We mutate the result rather than re-running because the lint
|
|
@@ -396,11 +397,40 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
396
397
|
return jsonContent(payload);
|
|
397
398
|
}
|
|
398
399
|
|
|
399
|
-
async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, frames = 60, holdInputs, screenshotPath, projectName }) {
|
|
400
|
+
async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
|
|
400
401
|
const { buildForPlatform } = await import("../../toolchains/index.js");
|
|
401
402
|
const resolved = resolveCore(platform);
|
|
402
403
|
if (!resolved) throw new Error(`no core available for platform '${platform}'`);
|
|
403
404
|
|
|
405
|
+
// PROJECT-DIR run: `build({output:'run', path})` with no explicit sources →
|
|
406
|
+
// read the scaffolded dir via the per-platform recipe (same as
|
|
407
|
+
// output:'project'), then run it. This is the documented "iterate on a dir"
|
|
408
|
+
// happy path; without it, output:'run' + path errored ("requires source").
|
|
409
|
+
const noExplicitSources = source == null && sourcePath == null && sources == null && sourcesPaths == null;
|
|
410
|
+
if (projPath && noExplicitSources) {
|
|
411
|
+
const r = await readProjectDir(projPath, platform);
|
|
412
|
+
includes = { ...(includes ?? {}), ...r.includes };
|
|
413
|
+
binaryIncludes = { ...(binaryIncludes ?? {}), ...r.binaryIncludes };
|
|
414
|
+
if (r.crt0 != null) crt0 = r.crt0;
|
|
415
|
+
if (r.codeLoc != null) codeLoc = r.codeLoc;
|
|
416
|
+
if (r.linkerConfig != null && linkerConfig == null) linkerConfig = r.linkerConfig;
|
|
417
|
+
if (r.runtime != null && runtime == null) runtime = r.runtime;
|
|
418
|
+
if (r.maxmod != null && maxmod == null) maxmod = r.maxmod;
|
|
419
|
+
// Single-source toolchains (dasm/atari2600, vasm/asm) need `source`, not
|
|
420
|
+
// a `sources` map. Collapse a lone source so those targets build via the
|
|
421
|
+
// dir path too. (resolveLinkerConfig support-sources, if any, force the
|
|
422
|
+
// map form below.)
|
|
423
|
+
const srcNames = Object.keys(r.sources);
|
|
424
|
+
if (srcNames.length === 1 && r.crt0 == null && r.linkerConfig == null) {
|
|
425
|
+
// Leave `source` null and set sourcePath — runSourceImpl reads it +
|
|
426
|
+
// derives sourceName (extension) for language inference. Single-source
|
|
427
|
+
// targets (dasm/vasm) require this form, not a `sources` map.
|
|
428
|
+
sourcePath = path.join(projPath, srcNames[0]);
|
|
429
|
+
} else {
|
|
430
|
+
sources = r.sources;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
404
434
|
if (source != null && sourcePath != null) {
|
|
405
435
|
throw new Error("build({output:'run'}): pass either `source` OR `sourcePath`, not both.");
|
|
406
436
|
}
|
|
@@ -474,7 +504,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
474
504
|
codeLoc,
|
|
475
505
|
dataLoc,
|
|
476
506
|
});
|
|
477
|
-
logBuildResult("
|
|
507
|
+
logBuildResult("build:run", platform, build);
|
|
478
508
|
if (!build.ok || !build.binary) {
|
|
479
509
|
// runSource builds in-memory (no ROM path), so a large failure log
|
|
480
510
|
// has nowhere to land — gate it to a tail + size rather than dumping
|
|
@@ -521,10 +551,10 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
521
551
|
if (!isPlaytestRunning(sessionKey) && !playtestHintGiven.has(sessionKey)) {
|
|
522
552
|
playtestHintGiven.add(sessionKey);
|
|
523
553
|
hint = "No playtest window is open. If a human is watching, consider " +
|
|
524
|
-
"`
|
|
525
|
-
"
|
|
526
|
-
"
|
|
527
|
-
"
|
|
554
|
+
"`playtest({op:\"open\"})` so they can play this ROM live while you " +
|
|
555
|
+
"keep iterating with build({output:\"run\"}) (rebuilds update the " +
|
|
556
|
+
"live game in place). Skip if this session is headless " +
|
|
557
|
+
"(CI / batch / automated).";
|
|
528
558
|
}
|
|
529
559
|
|
|
530
560
|
const summary = {
|
|
@@ -628,7 +658,18 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
628
658
|
},
|
|
629
659
|
safeTool(async (args) => {
|
|
630
660
|
switch (args.output) {
|
|
631
|
-
case "rom":
|
|
661
|
+
case "rom": {
|
|
662
|
+
// `build({output:'rom', path})` (a project dir, no explicit sources) is
|
|
663
|
+
// the natural "build my scaffolded dir to a ROM file" call. Route it to
|
|
664
|
+
// the dir builder (same recipe as output:'project'/'run') — otherwise it
|
|
665
|
+
// fell into buildSourceImpl with no source and crashed on an undefined
|
|
666
|
+
// log. With path AND explicit sources, the sources win (manual build).
|
|
667
|
+
if (args.path && args.source == null && args.sources == null &&
|
|
668
|
+
args.sourcePath == null && args.sourcesPaths == null) {
|
|
669
|
+
return await buildProjectImpl(args);
|
|
670
|
+
}
|
|
671
|
+
return await buildSourceImpl(args);
|
|
672
|
+
}
|
|
632
673
|
case "run": return await runSourceImpl(args);
|
|
633
674
|
case "project": {
|
|
634
675
|
if (!args.path) throw new Error("build({output:'project'}): `path` (the project directory) is required.");
|
|
@@ -648,13 +689,107 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
648
689
|
* a jsonContent payload (the router calls it via safeTool, which turns a throw —
|
|
649
690
|
* e.g. no entry point — into an {isError:true} result).
|
|
650
691
|
*/
|
|
651
|
-
|
|
692
|
+
/**
|
|
693
|
+
* Per-platform recipe for building a SCAFFOLDED project directory. Given the
|
|
694
|
+
* platform + the list of filenames present, decide which file (if any) is a crt0
|
|
695
|
+
* that must be routed via `crt0`/`codeLoc` (not linked as a plain TU), which
|
|
696
|
+
* linker preset to apply, which SDK runtime to select, and which files to SKIP
|
|
697
|
+
* (preset-supplied crt0s, SDK intermediates). This is the single source of truth
|
|
698
|
+
* that makes `build({output:'project'|'run', path})` match what the scaffold
|
|
699
|
+
* README's hand-written build call does.
|
|
700
|
+
* @param {string} platform
|
|
701
|
+
* @param {string[]} names filenames present in the project dir
|
|
702
|
+
*/
|
|
703
|
+
export function projectBuildRecipe(platform, names) {
|
|
704
|
+
const has = (n) => names.includes(n);
|
|
705
|
+
/** @type {{crt0File:string|null, codeLoc:number|undefined, linkerConfig:string|undefined, runtime:string|undefined, maxmod:boolean|undefined, skip:Set<string>, includeAsC:Set<string>}} */
|
|
706
|
+
const r = { crt0File: null, codeLoc: undefined, linkerConfig: undefined, runtime: undefined, maxmod: undefined, skip: new Set(), includeAsC: new Set() };
|
|
707
|
+
|
|
708
|
+
// Reference/upstream sources ship for grepping, not compiling (e.g. GB
|
|
709
|
+
// music_demo's hUGEDriver.upstream.asm — the .c port is what builds). Skip
|
|
710
|
+
// any *.upstream.* on every platform.
|
|
711
|
+
for (const n of names) if (/\.upstream\./i.test(n)) r.skip.add(n);
|
|
712
|
+
|
|
713
|
+
if (platform === "gb" || platform === "gbc") {
|
|
714
|
+
// GB/GBC ship gb_crt0.s — it MUST go via crt0+codeLoc:0x150, never as a
|
|
715
|
+
// source (SDCC emits its own gsinit → "Multiple definition of gsinit").
|
|
716
|
+
if (has("gb_crt0.s")) { r.crt0File = "gb_crt0.s"; r.codeLoc = 0x150; }
|
|
717
|
+
} else if (platform === "nes") {
|
|
718
|
+
// A SCAFFOLDED NES project ships nes_runtime.c + a crt0 + a .cfg and needs
|
|
719
|
+
// the chr-ram-runtime preset (it defines the OAM/CHARS segments + a NMI with
|
|
720
|
+
// OAM-DMA; without it: "Missing memory area 'OAM'"). The preset SUPPLIES its
|
|
721
|
+
// own crt0 + expects nes_runtime.c, so skip the scaffold's crt0/.cfg (the
|
|
722
|
+
// preset replaces them). A BARE hand-rolled NES dir (no scaffold crt0/.cfg)
|
|
723
|
+
// is left alone — forcing the preset there would demand runtime symbols it
|
|
724
|
+
// doesn't have. Detect "scaffolded" by the presence of a crt0 or .cfg.
|
|
725
|
+
const looksScaffolded = names.some((n) => /crt0.*\.s$/i.test(n) || /\.cfg$/i.test(n));
|
|
726
|
+
if (looksScaffolded) {
|
|
727
|
+
r.linkerConfig = "chr-ram-runtime";
|
|
728
|
+
for (const n of names) {
|
|
729
|
+
if (/crt0.*\.s$/i.test(n) || /\.cfg$/i.test(n)) r.skip.add(n);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} else if (platform === "sms" || platform === "gg") {
|
|
733
|
+
// SMS/GG auto-inject their bundled crt0 inside buildForPlatform — so the
|
|
734
|
+
// scaffold's own *_crt0.s would be a DUPLICATE. Skip it.
|
|
735
|
+
for (const n of names) if (/_crt0\.s$/i.test(n)) r.skip.add(n);
|
|
736
|
+
} else if (platform === "genesis" || platform === "megadrive" || platform === "md") {
|
|
737
|
+
// SGDK supplies sega startup + rom header. The scaffold dir may contain
|
|
738
|
+
// generated intermediates (sega.s, sega.preprocessed.s, rom_header.*, and an
|
|
739
|
+
// out/ build dir) that must NOT be recompiled — sega.preprocessed.s refs a
|
|
740
|
+
// missing out/rom_header.bin and aborts the build.
|
|
741
|
+
for (const n of names) {
|
|
742
|
+
if (/^sega(\.preprocessed)?\.s$/i.test(n) || /^rom_header\./i.test(n) || /\.preprocessed\.s$/i.test(n)) r.skip.add(n);
|
|
743
|
+
}
|
|
744
|
+
} else if (platform === "snes") {
|
|
745
|
+
const asmEntry = has("main.asm") && !has("main.c"); // asar asm template
|
|
746
|
+
if (asmEntry) {
|
|
747
|
+
// SNES asar asm template: main.asm `.include`s its siblings
|
|
748
|
+
// (lorom_header/reset_init/cgram_upload.asm). asar takes ONE source +
|
|
749
|
+
// resolves .include from the includes mount — so route non-main .asm as
|
|
750
|
+
// includes, leaving main.asm the single source.
|
|
751
|
+
for (const n of names) {
|
|
752
|
+
if (/\.asm$/i.test(n) && n !== "main.asm") r.includeAsC.add(n);
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
// SNES (PVSnesLib/tcc) C scaffolds combine C via `#include "snes_sfx.c"`
|
|
756
|
+
// from main.c — a single TU. A non-main .c is an INCLUDE (tcc must find it
|
|
757
|
+
// for the #include), NOT a separate source TU (which would double-define).
|
|
758
|
+
// (data.asm / snes_sfx_data.asm stay real wla sources, compiled + linked.)
|
|
759
|
+
for (const n of names) {
|
|
760
|
+
if (/\.c$/i.test(n) && n !== "main.c") r.includeAsC.add(n);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// The SPC700 audio driver sources (spc_driver.asm, apu_blob.asm) are 65816-
|
|
764
|
+
// INCOMPATIBLE SPC700 asm used OFFLINE to regenerate apu_blob.bin — the
|
|
765
|
+
// scaffold already ships the built .bin (incbin'd by snes_sfx_data.asm).
|
|
766
|
+
// Compiling them as 65816 sources fails ("Cannot process spc700"). Skip them.
|
|
767
|
+
for (const n of names) {
|
|
768
|
+
if (n === "spc_driver.asm" || n === "apu_blob.asm") r.skip.add(n);
|
|
769
|
+
}
|
|
770
|
+
} else if (platform === "gba") {
|
|
771
|
+
// GBA: default runtime is libtonc; a soundbank.bin means the maxmod path.
|
|
772
|
+
// The libgba-vs-libtonc choice needs the source CONTENT (which header it
|
|
773
|
+
// includes), so buildProjectCore refines r.runtime after reading main.c.
|
|
774
|
+
r.runtime = "libtonc";
|
|
775
|
+
if (has("soundbank.bin")) r.maxmod = true;
|
|
776
|
+
}
|
|
777
|
+
return r;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Read a scaffolded project DIRECTORY into the build inputs, applying the
|
|
782
|
+
* per-platform recipe (crt0 routing, linker preset, runtime, skip-list) and
|
|
783
|
+
* the GBA runtime content-sniff. The SINGLE source of truth shared by
|
|
784
|
+
* build({output:'project'}) and build({output:'run', path}) — so the two paths
|
|
785
|
+
* can never drift. Returns crt0 as RAW source text (callers assemble it).
|
|
786
|
+
* @param {string} projPath
|
|
787
|
+
* @param {string} platform
|
|
788
|
+
*/
|
|
789
|
+
export async function readProjectDir(projPath, platform) {
|
|
652
790
|
const entries = await readdir(projPath, { withFileTypes: true });
|
|
653
791
|
const files = entries.filter((e) => e.isFile());
|
|
654
792
|
|
|
655
|
-
// Entry point: a C project uses main.c (SGDK/Genesis, GBA, cc65/SDCC C); an
|
|
656
|
-
// asm project uses main.s / main.asm. Pick whichever exists — so the SAME
|
|
657
|
-
// dir-build works for C/SGDK Genesis projects, not just asm/cc65.
|
|
658
793
|
const hasC = files.some((f) => f.name === "main.c");
|
|
659
794
|
const hasAsm = files.some((f) => f.name === "main.s" || f.name === "main.asm");
|
|
660
795
|
if (!hasC && !hasAsm) {
|
|
@@ -664,40 +799,81 @@ export async function buildProjectCore({ path: projPath, platform, outputPath })
|
|
|
664
799
|
);
|
|
665
800
|
}
|
|
666
801
|
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
802
|
+
// Per-platform PROJECT RECIPE — see projectBuildRecipe. Globbing every file as
|
|
803
|
+
// a source is what broke GB (gsinit double-def), NES (no OAM/CHARS), Genesis
|
|
804
|
+
// (sega.preprocessed.s). The recipe routes crt0 / preset / runtime / skips so
|
|
805
|
+
// the dir build matches the hand-written build({output:'run'}) call.
|
|
806
|
+
const recipe = projectBuildRecipe(platform, files.map((f) => f.name));
|
|
807
|
+
|
|
670
808
|
/** @type {Record<string,string>} */ const sources = {};
|
|
671
809
|
/** @type {Record<string,string>} */ const includes = {};
|
|
672
810
|
/** @type {Record<string,string>} */ const binaryIncludes = {};
|
|
811
|
+
let crt0 = null;
|
|
673
812
|
for (const f of files) {
|
|
674
813
|
const n = f.name;
|
|
814
|
+
if (recipe.skip.has(n)) continue;
|
|
815
|
+
if (recipe.crt0File === n) { crt0 = await readFile(path.join(projPath, n), "utf-8"); continue; }
|
|
816
|
+
// includeAsC: a .c that's `#include`d by another TU (e.g. SNES main.c
|
|
817
|
+
// includes snes_sfx.c) — make it an include, NOT a separate source TU.
|
|
818
|
+
if (recipe.includeAsC.has(n)) { includes[n] = await readFile(path.join(projPath, n), "utf-8"); continue; }
|
|
675
819
|
if (/\.(c|s|asm)$/i.test(n)) sources[n] = await readFile(path.join(projPath, n), "utf-8");
|
|
676
820
|
else if (/\.(h|inc)$/i.test(n)) includes[n] = await readFile(path.join(projPath, n), "utf-8");
|
|
677
|
-
else if (/\.(bin|chr|pcm|brr|vgm|xgm|nsf|raw|pal)$/i.test(n)) {
|
|
821
|
+
else if (/\.(bin|chr|pcm|brr|vgm|vgz|xgm|xgc|xgm2|esf|tfi|eif|nsf|raw|pal|map|tmx|spc|wav|gbs)$/i.test(n)) {
|
|
822
|
+
// Any binary asset an .incbin might reference. (.xgc = compiled XGM2 blob,
|
|
823
|
+
// .vgz/.esf/etc = music driver inputs.) Missing one = "file not found: X".
|
|
678
824
|
binaryIncludes[n] = (await readFile(path.join(projPath, n))).toString("base64");
|
|
679
825
|
}
|
|
680
826
|
}
|
|
681
827
|
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
|
|
828
|
+
// GBA runtime refinement: libgba if the entry includes <gba.h>, else the
|
|
829
|
+
// libtonc default the recipe set.
|
|
830
|
+
let runtime = recipe.runtime;
|
|
831
|
+
if (platform === "gba" && sources["main.c"] && /#\s*include\s*[<"]gba\.h[>"]/.test(sources["main.c"])) {
|
|
832
|
+
runtime = "libgba";
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return { sources, includes, binaryIncludes, crt0, codeLoc: recipe.codeLoc, linkerConfig: recipe.linkerConfig, runtime, maxmod: recipe.maxmod };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export async function buildProjectCore({ path: projPath, platform, outputPath }) {
|
|
839
|
+
const { sources, includes, binaryIncludes, crt0, codeLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform);
|
|
840
|
+
|
|
841
|
+
// Linker preset: the recipe names it (e.g. NES 'chr-ram-runtime', which ships
|
|
842
|
+
// the OAM/CHARS segments + its own crt0). resolveLinkerConfig also returns any
|
|
843
|
+
// preset support sources (the preset crt0) — those merge into the sources.
|
|
844
|
+
const { cfg: resolvedLinkerConfig, supportSources } = await resolveLinkerConfig(platform, linkerConfig);
|
|
685
845
|
const mergedSources = Object.keys(supportSources).length ? { ...supportSources, ...sources } : sources;
|
|
686
846
|
|
|
847
|
+
// Assemble a routed crt0 (SDCC sm83/z80) into a .rel, exactly like the
|
|
848
|
+
// output:'rom'/'run' path — passing it as `crt0` (NOT a source TU) is what
|
|
849
|
+
// avoids the gsinit double-definition on GB/GBC.
|
|
850
|
+
let crt0Rel;
|
|
851
|
+
if (crt0) {
|
|
852
|
+
const isSm83 = platform === "gb" || platform === "gbc";
|
|
853
|
+
const { runSdasgb, runSdasz80 } = await import("../../toolchains/sdcc/sdcc.js");
|
|
854
|
+
const asm = isSm83 ? await runSdasgb({ source: crt0 }) : await runSdasz80({ source: crt0 });
|
|
855
|
+
if (!asm.rel) throw new Error(`crt0 assembly failed:\n${asm.log}`);
|
|
856
|
+
crt0Rel = asm.rel;
|
|
857
|
+
}
|
|
858
|
+
|
|
687
859
|
// Single-source toolchains (dasm/atari2600, vasm/asm) take `source`, not the
|
|
688
860
|
// multi-TU `sources` map. When the dir has exactly one source file and no
|
|
689
861
|
// preset support sources, pass it as `source` so those targets still build
|
|
690
862
|
// (the original asm-only buildProject behavior).
|
|
691
863
|
const srcNames = Object.keys(sources);
|
|
692
|
-
const singleSource = srcNames.length === 1 && Object.keys(supportSources).length === 0;
|
|
864
|
+
const singleSource = srcNames.length === 1 && Object.keys(supportSources).length === 0 && !crt0Rel;
|
|
693
865
|
const result = await buildForPlatform({
|
|
694
866
|
platform,
|
|
867
|
+
runtime,
|
|
868
|
+
maxmod,
|
|
695
869
|
...(singleSource
|
|
696
870
|
? { source: sources[srcNames[0]], sourceName: srcNames[0] }
|
|
697
871
|
: { sources: mergedSources }),
|
|
698
872
|
includes: Object.keys(includes).length ? includes : undefined,
|
|
699
873
|
binaryIncludes: Object.keys(binaryIncludes).length ? binaryIncludes : undefined,
|
|
700
874
|
linkerConfig: resolvedLinkerConfig,
|
|
875
|
+
crt0: crt0Rel,
|
|
876
|
+
codeLoc,
|
|
701
877
|
});
|
|
702
878
|
if (outputPath && result.binary) {
|
|
703
879
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
#status { font-size: 11px; color: #888; margin-left: auto; }
|
|
29
29
|
#status.connected { color: #4caf50; }
|
|
30
30
|
#status.disconnected { color: #f44336; }
|
|
31
|
+
#log-session {
|
|
32
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
33
|
+
font-size: 12px; color: #6cb6ff; padding: 2px 0 8px;
|
|
34
|
+
word-break: break-all;
|
|
35
|
+
}
|
|
31
36
|
#tabs {
|
|
32
37
|
display: flex; flex-wrap: wrap; gap: 4px;
|
|
33
38
|
padding: 6px 12px; background: #111; border-bottom: 1px solid #333;
|
|
@@ -189,6 +194,7 @@
|
|
|
189
194
|
</div>
|
|
190
195
|
<div id="log-pane">
|
|
191
196
|
<h2>Activity log</h2>
|
|
197
|
+
<div id="log-session"></div>
|
|
192
198
|
<div id="log-list"></div>
|
|
193
199
|
</div>
|
|
194
200
|
</div>
|
|
@@ -201,6 +207,7 @@
|
|
|
201
207
|
const tabsEl = document.getElementById("tabs");
|
|
202
208
|
const imageListEl = document.getElementById("image-list");
|
|
203
209
|
const logListEl = document.getElementById("log-list");
|
|
210
|
+
const logSessionEl = document.getElementById("log-session");
|
|
204
211
|
const emptyEl = document.getElementById("empty");
|
|
205
212
|
|
|
206
213
|
// Per-session state.
|
|
@@ -351,10 +358,22 @@
|
|
|
351
358
|
tab.className = "tab" + (k === activeSessionKey ? " active" : "")
|
|
352
359
|
+ (!s.connected ? " disconnected" : "");
|
|
353
360
|
tab.onclick = () => selectTab(k);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
361
|
+
// Show the FULL session id — agents pick descriptive ids (e.g.
|
|
362
|
+
// "scaffold-nes-platformer") and truncating to 8 chars collapsed distinct
|
|
363
|
+
// tasks into identical-looking tabs ("scaffold" × N). Cap very long ids in
|
|
364
|
+
// the middle so the meaningful head+tail both show; full id in the tooltip.
|
|
365
|
+
const label = k.length > 28 ? k.slice(0, 18) + "…" + k.slice(-8) : k;
|
|
366
|
+
const labelSpan = document.createElement("span");
|
|
367
|
+
labelSpan.textContent = label;
|
|
368
|
+
tab.appendChild(labelSpan);
|
|
369
|
+
if (s.unseen > 0 && k !== activeSessionKey) {
|
|
370
|
+
const badge = document.createElement("span");
|
|
371
|
+
badge.className = "badge";
|
|
372
|
+
badge.textContent = String(s.unseen);
|
|
373
|
+
tab.appendChild(document.createTextNode(" "));
|
|
374
|
+
tab.appendChild(badge);
|
|
375
|
+
}
|
|
376
|
+
tab.title = s.connected ? k : `${k} (session closed)`;
|
|
358
377
|
tabsEl.appendChild(tab);
|
|
359
378
|
}
|
|
360
379
|
}
|
|
@@ -504,6 +523,17 @@
|
|
|
504
523
|
|
|
505
524
|
function renderLog() {
|
|
506
525
|
logListEl.innerHTML = "";
|
|
526
|
+
// Full session id above the log — the tab label can be middle-truncated, so
|
|
527
|
+
// this is the one place that always shows the EXACT id you're looking at.
|
|
528
|
+
if (logSessionEl) {
|
|
529
|
+
if (activeSessionKey) {
|
|
530
|
+
logSessionEl.textContent = "session: " + activeSessionKey;
|
|
531
|
+
logSessionEl.style.display = "";
|
|
532
|
+
} else {
|
|
533
|
+
logSessionEl.textContent = "";
|
|
534
|
+
logSessionEl.style.display = "none";
|
|
535
|
+
}
|
|
536
|
+
}
|
|
507
537
|
if (!activeSessionKey) return;
|
|
508
538
|
const s = sessions.get(activeSessionKey);
|
|
509
539
|
// Newest first — easier to scan when working live.
|
|
@@ -73,19 +73,20 @@ check the live OAM Y bytes for $D0 in a slot before them. That's
|
|
|
73
73
|
still the diagnosis; the runtime just doesn't create the problem
|
|
74
74
|
on its own anymore.
|
|
75
75
|
|
|
76
|
-
### R6 sprite-tile-base default
|
|
77
|
-
|
|
78
|
-
`gg_vdp_init()` sets R6 =
|
|
79
|
-
sprite tile data —
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
### R6 sprite-tile-base: default is $2000 (0xFF)
|
|
77
|
+
|
|
78
|
+
`gg_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
|
|
79
|
+
sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
|
|
80
|
+
from `$2000-$3FFF`, in their **own bank** separate from BG tiles at
|
|
81
|
+
$0000. This is the baseline because every bundled scaffold uploads
|
|
82
|
+
its sprite tiles to `$2000` (`gg_load_tiles(0x2000, …)`) — the
|
|
83
|
+
default and the scaffolds match, so sprites Just Show Up.
|
|
84
|
+
|
|
85
|
+
Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
|
|
86
|
+
(sharing the BG bank). If you ever set R6=0xFB you MUST also upload
|
|
87
|
+
your sprite tiles to $0000, or the VDP reads the empty/BG bank and
|
|
88
|
+
every sprite is invisible — the classic GG/SMS "my sprites don't
|
|
89
|
+
show up" trap.
|
|
89
90
|
|
|
90
91
|
The `sprites({op:'inspect'})` tool's `spriteTileDataBase` field reports the
|
|
91
92
|
address the VDP is actually reading from — trust that over any
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* Writes the 11 mode-4 registers to a sane baseline:
|
|
4
4
|
* display OFF, vblank IRQ off, 192-line mode 4, name table at $3800,
|
|
5
5
|
* BG tile data at $0000, sprite attr table at $3F00, sprite tile data
|
|
6
|
-
* at $
|
|
6
|
+
* at $2000 (R6=0xFF → SA13 set → tiles read from $2000-$3FFF). Call
|
|
7
7
|
* once after reset before uploading palette/tiles/map.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* sprite tiles
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Sprite-tile base: R6 bit 2 (SA13) selects $0000 (clear) vs $2000 (set).
|
|
10
|
+
* We default to R6=0xFF ($2000) because EVERY bundled scaffold uploads its
|
|
11
|
+
* sprite tiles to $2000 (gg_load_tiles(0x2000, ...)) — so the baseline must
|
|
12
|
+
* match what consumers do, or sprites read from the empty/BG bank and render
|
|
13
|
+
* invisible. (Many SMS/GG references say "R6=0xFB → $2000", which is backwards:
|
|
14
|
+
* 0xFB has SA13 CLEAR = $0000.) If you instead keep sprite tiles in the BG
|
|
15
|
+
* bank at $0000, set R6=0xFB. sprites({op:'inspect'}) → spriteTileDataBase
|
|
16
|
+
* shows the address the VDP is actually reading from.
|
|
15
17
|
*
|
|
16
18
|
* After loading assets, enable display by re-writing R1 with bit 6 set:
|
|
17
19
|
* gg_vdp_display_on();
|
|
@@ -33,7 +35,7 @@ void gg_vdp_init(void) {
|
|
|
33
35
|
0xFF, /* R3: color table (ignored in M4) */
|
|
34
36
|
0xFF, /* R4: BG tile data at $0000 */
|
|
35
37
|
0xFF, /* R5: sprite attr table at $3F00 */
|
|
36
|
-
|
|
38
|
+
0xFF, /* R6: sprite tile data at $2000 (SA13 set; scaffolds upload here) */
|
|
37
39
|
0x00, /* R7: border = sprite palette entry 0 */
|
|
38
40
|
0x00, /* R8: BG X scroll */
|
|
39
41
|
0x00, /* R9: BG Y scroll */
|
|
@@ -106,7 +106,7 @@ exactly this.
|
|
|
106
106
|
- `palette({source:'live'})` — V9938 9-bit GRB (or TMS9918 fixed) 16 entries.
|
|
107
107
|
- `sprites({op:'inspect'})` — VRAM sprite-attribute table, up to 32 sprites.
|
|
108
108
|
- `symbols({op:'map', map})` — pass the sdld `.map` (the `symbols` field from
|
|
109
|
-
|
|
109
|
+
build({output:'romWithDebug'})) to see where SDCC placed your variables/code, grouped by
|
|
110
110
|
region (bios / cart_rom / work_ram).
|
|
111
111
|
- `audioDebug({op:'inspect', chip: "ay8910"})` — the AY-3-8910 PSG: 3 square-wave
|
|
112
112
|
channels (tone period→Hz, amplitude, tone/noise enable) + a shared noise
|
|
@@ -140,7 +140,7 @@ Overflow it and there's no error — your globals quietly collide with
|
|
|
140
140
|
the stack or shadow OAM → corrupted state, sprites that flicker to
|
|
141
141
|
garbage, random crashes.
|
|
142
142
|
|
|
143
|
-
**Check the `ramUsage` field in the
|
|
143
|
+
**Check the `ramUsage` field in the build response** —
|
|
144
144
|
it lists your BSS / DATA / ZEROPAGE segment sizes from the linker map.
|
|
145
145
|
If BSS+DATA is approaching the config's RAM region, shrink your state:
|
|
146
146
|
prefer `uint8_t` over `int`, bit-pack flags, use small fixed arrays,
|
|
@@ -56,6 +56,7 @@ volatile uint8_t nmi_counter = 0;
|
|
|
56
56
|
* (so OAM segment placement at $0200 is linker-enforced). oam_index
|
|
57
57
|
* tracks the next free slot for oam_spr(). */
|
|
58
58
|
static uint8_t oam_index = 0;
|
|
59
|
+
static void oam_hide_unused(void); /* fwd decl — used by ppu_wait_nmi (NES-1) */
|
|
59
60
|
|
|
60
61
|
/* ── VRAM write queue ─────────────────────────────────────────────
|
|
61
62
|
* Each entry is { hi, lo, byte }. NMI walks the queue, writes
|
|
@@ -130,7 +131,12 @@ void ppu_wait_vblank(void) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
void ppu_wait_nmi(void) {
|
|
133
|
-
uint8_t target
|
|
134
|
+
uint8_t target;
|
|
135
|
+
/* Hide last frame's now-unused sprite slots BEFORE waiting, so the buffer
|
|
136
|
+
* the NMI's OAM-DMA copies is fully staged (live slots written by oam_spr,
|
|
137
|
+
* stale slots parked off-screen) — never a half-cleared buffer (NES-1). */
|
|
138
|
+
oam_hide_unused();
|
|
139
|
+
target = (uint8_t)(nmi_counter + 1);
|
|
134
140
|
while (nmi_counter != target) { /* spin */ }
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -159,15 +165,31 @@ void palette_load(const uint8_t *pal32) {
|
|
|
159
165
|
|
|
160
166
|
/* ── OAM ──────────────────────────────────────────────────────── */
|
|
161
167
|
|
|
168
|
+
/* High-water mark: the largest oam_index reached last frame. Lets us blank
|
|
169
|
+
* ONLY the slots a frame stopped using, instead of the whole 256-byte buffer
|
|
170
|
+
* every frame. */
|
|
171
|
+
static uint8_t oam_high = 0;
|
|
172
|
+
|
|
162
173
|
void oam_clear(void) {
|
|
174
|
+
/* NES-1 FIX: do NOT blank the whole shadow buffer here. The old full clear
|
|
175
|
+
* wrote slot 0's Y=$FF FIRST and took ~hundreds of cycles; if the NMI's
|
|
176
|
+
* OAM-DMA fired mid-clear it copied a HALF-CLEARED buffer → the live sprite
|
|
177
|
+
* vanished every other frame (the classic "sprite flickers to black"
|
|
178
|
+
* sprite-light scaffold bug). Instead we just reset the staging index here;
|
|
179
|
+
* ppu_wait_nmi() hides the slots this frame stopped using, so the DMA only
|
|
180
|
+
* ever sees a fully-staged buffer. */
|
|
181
|
+
oam_index = 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* Hide slots [oam_index .. oam_high] (the ones used last frame but not this
|
|
185
|
+
* frame) by parking their Y off-screen. Called from ppu_wait_nmi AFTER the
|
|
186
|
+
* game has staged its live sprites, so live slots are never blanked. */
|
|
187
|
+
static void oam_hide_unused(void) {
|
|
163
188
|
uint16_t i;
|
|
164
|
-
for (i =
|
|
189
|
+
for (i = oam_index; i < (uint16_t)oam_high + 4 && i < 256; i += 4) {
|
|
165
190
|
shadow_oam[i] = 0xFF; /* Y off-screen */
|
|
166
|
-
shadow_oam[i + 1] = 0; /* tile */
|
|
167
|
-
shadow_oam[i + 2] = 0; /* attr */
|
|
168
|
-
shadow_oam[i + 3] = 0; /* X */
|
|
169
191
|
}
|
|
170
|
-
|
|
192
|
+
oam_high = oam_index;
|
|
171
193
|
}
|
|
172
194
|
|
|
173
195
|
void oam_spr(uint8_t x, uint8_t y, uint8_t tile, uint8_t attr) {
|
|
@@ -90,7 +90,7 @@ screen. Keep at least one (2+ byte) global. See TROUBLESHOOTING.md.
|
|
|
90
90
|
- `background({view:'renderState'})` — VDC R5 screen-enable, BG scroll, SATB source.
|
|
91
91
|
- `palette({source:'live'})` — VCE 512-entry 9-bit GRB (area:'bg'|'sprite').
|
|
92
92
|
- `sprites({op:'inspect'})` — SATB 64 sprites (x/y/tile/palette/size/flip).
|
|
93
|
-
- `symbols({op:'map'})` — where cc65 placed your variables (after
|
|
93
|
+
- `symbols({op:'map'})` — where cc65 placed your variables (after build({output:'romWithDebug'})).
|
|
94
94
|
- `audioDebug({op:'inspect', chip: "pce"})` — the HuC6280 PSG: 6 wavetable channels
|
|
95
95
|
(per-channel freq/volume/wave; channels 4-5 can also do noise) + main amplitude
|
|
96
96
|
+ LFO.
|
|
@@ -96,6 +96,7 @@ void vdc_set_reg(u8 reg, u16 val); /* select reg, write 16-bit valu
|
|
|
96
96
|
void vram_set_write_addr(u16 addr); /* point MAWR + arm VWR streaming */
|
|
97
97
|
void vram_write(u16 addr, const u16 *data, u16 n); /* upload n words to VRAM[addr] */
|
|
98
98
|
void vce_set_color(u16 idx, u16 grb); /* set VCE palette entry (0..511) */
|
|
99
|
+
void vdc_init(void); /* program VDC display timing (256x224 NTSC); auto-run by *_enable */
|
|
99
100
|
void bg_enable(void); /* VDC R5: background on + VBlank IRQ (so waitvsync works) */
|
|
100
101
|
void spr_enable(void); /* VDC R5: sprites on + VBlank IRQ */
|
|
101
102
|
void disp_enable(void); /* VDC R5: BG + SPR + VBlank IRQ on at once */
|
|
@@ -70,17 +70,43 @@ void vblank_irq_enable(void) {
|
|
|
70
70
|
vdc_set_reg(VDC_CR, _pce_cr);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* Program the VDC display-timing registers for a standard NTSC 256x224 (H32)
|
|
74
|
+
* screen. WITHOUT this the geargrafx core falls back to power-on register
|
|
75
|
+
* defaults that composite the 32-row BAT into the display DOUBLED (the scene
|
|
76
|
+
* drawn twice, top + bottom halves, with a black right margin) — the PCE-1
|
|
77
|
+
* "doubled picture" bug. Values match cc65's pce.lib / standard PCE homebrew:
|
|
78
|
+
* MWR R9 = 32x32 virtual screen, 256px-wide BAT
|
|
79
|
+
* HSR R10 / HDR R11 = 256px (32 char) horizontal display
|
|
80
|
+
* VPR R12 / VDW R13 / VCR R14 = 224-line vertical window
|
|
81
|
+
* Called automatically the first time the display is enabled (idempotent). */
|
|
82
|
+
static u8 _pce_vdc_inited = 0;
|
|
83
|
+
void vdc_init(void) {
|
|
84
|
+
if (_pce_vdc_inited) return;
|
|
85
|
+
_pce_vdc_inited = 1;
|
|
86
|
+
vdc_set_reg(VDC_MWR, 0x0010); /* 32x32 virtual map, 256px BAT */
|
|
87
|
+
vdc_set_reg(VDC_BXR, 0x0000); /* BG X scroll = 0 */
|
|
88
|
+
vdc_set_reg(VDC_BYR, 0x0000); /* BG Y scroll = 0 */
|
|
89
|
+
vdc_set_reg(VDC_HSR, 0x0202); /* horizontal sync width/start */
|
|
90
|
+
vdc_set_reg(VDC_HDR, 0x031F); /* horizontal display = 32 chars (256px) */
|
|
91
|
+
vdc_set_reg(VDC_VPR, 0x0F02); /* vertical sync */
|
|
92
|
+
vdc_set_reg(VDC_VDW, 0x00DF); /* vertical display = 224 lines */
|
|
93
|
+
vdc_set_reg(VDC_VCR, 0x00EE); /* vertical display end */
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
void bg_enable(void) {
|
|
97
|
+
vdc_init();
|
|
74
98
|
_pce_cr |= (VDC_CR_BG_ON | VDC_CR_VBLANK_IRQ);
|
|
75
99
|
vdc_set_reg(VDC_CR, _pce_cr);
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
void spr_enable(void) {
|
|
103
|
+
vdc_init();
|
|
79
104
|
_pce_cr |= (VDC_CR_SPR_ON | VDC_CR_VBLANK_IRQ);
|
|
80
105
|
vdc_set_reg(VDC_CR, _pce_cr);
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
void disp_enable(void) {
|
|
109
|
+
vdc_init();
|
|
84
110
|
_pce_cr |= (VDC_CR_BG_ON | VDC_CR_SPR_ON | VDC_CR_VBLANK_IRQ);
|
|
85
111
|
vdc_set_reg(VDC_CR, _pce_cr);
|
|
86
112
|
}
|
|
@@ -78,7 +78,7 @@ R1 = 0x80 display OFF, vblank IRQ off, 192-line
|
|
|
78
78
|
R2 = 0xFF name table at $3800
|
|
79
79
|
R4 = 0xFF BG tile data at $0000
|
|
80
80
|
R5 = 0xFF sprite attr table at $3F00
|
|
81
|
-
R6 =
|
|
81
|
+
R6 = 0xFF sprite tile data at $2000 (own bank; scaffolds upload here)
|
|
82
82
|
R7 = 0x00 border colour
|
|
83
83
|
```
|
|
84
84
|
|
|
@@ -142,19 +142,19 @@ buffer in WRAM and uploads it to the SAT each vblank.
|
|
|
142
142
|
`sprites({op:'inspect'})` shows the live OAM bytes + reports
|
|
143
143
|
`spriteTileDataBase` — trust it over comments when sprites misbehave.
|
|
144
144
|
|
|
145
|
-
### R6 sprite-tile-base default
|
|
145
|
+
### R6 sprite-tile-base: default is $2000 (0xFF)
|
|
146
146
|
|
|
147
|
-
`sms_vdp_init()` sets R6 =
|
|
148
|
-
sprite tile data —
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
`sms_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
|
|
148
|
+
sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
|
|
149
|
+
from `$2000-$3FFF`, their **own bank** separate from BG tiles at
|
|
150
|
+
$0000. This matches every bundled scaffold, which uploads sprite
|
|
151
|
+
tiles to `$2000` (`sms_load_tiles(0x2000, …)`) — default and
|
|
152
|
+
scaffolds agree, so sprites render.
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
|
|
155
|
+
(shared with the BG bank). If you set R6=0xFB you MUST upload your
|
|
156
|
+
sprite tiles to $0000 too, or the VDP reads the empty/BG bank and
|
|
157
|
+
every sprite is invisible.
|
|
158
158
|
|
|
159
159
|
## Palette (CRAM)
|
|
160
160
|
|