romdevtools 0.44.0 → 0.70.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 +572 -0
- package/README.md +9 -7
- package/examples/dreamcast/hello/main.c +24 -0
- package/examples/n64/platformer/main.c +158 -0
- package/examples/n64/puzzle/main.c +117 -0
- package/examples/n64/racing/main.c +147 -0
- package/examples/n64/shmup/main.c +122 -0
- package/examples/n64/sports/main.c +127 -0
- package/examples/ps1/platformer/main.c +158 -0
- package/examples/ps1/puzzle/main.c +125 -0
- package/examples/ps1/racing/main.c +147 -0
- package/examples/ps1/shmup/main.c +192 -0
- package/examples/ps1/sports/main.c +127 -0
- package/examples/ps1/sprite_move/main.c +38 -0
- package/package.json +11 -2
- package/src/analysis/analyze.js +224 -29
- package/src/analysis/decompile.js +7 -0
- package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
- package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
- package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
- package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
- package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
- package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
- package/src/analysis/rizin.js +24 -1
- package/src/cores/capabilities.js +122 -2
- package/src/cores/registry.js +17 -0
- package/src/host/LibretroGL.js +273 -0
- package/src/host/LibretroGLBridge.js +836 -0
- package/src/host/LibretroHost.js +203 -3
- package/src/host/callbacks.js +22 -2
- package/src/host/coreLoader.js +65 -5
- package/src/host/cpu-state.js +27 -0
- package/src/host/framebuffer.js +14 -1
- package/src/host/glOptionalDep.js +60 -0
- package/src/host/n64-ai-state.js +43 -0
- package/src/host/ps1-spu-state.js +65 -0
- package/src/host/retroConstants.js +14 -0
- package/src/mcp/tools/disasm.js +2 -0
- package/src/mcp/tools/frame.js +18 -9
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +20 -4
- package/src/mcp/tools/platforms.js +38 -4
- package/src/mcp/tools/project.js +116 -0
- package/src/mcp/tools/rendering-context.js +2 -1
- package/src/mcp/tools/toolchain.js +1 -1
- package/src/platforms/n64/lib/c/n64.c +196 -0
- package/src/platforms/n64/lib/c/n64.h +68 -0
- package/src/platforms/ps1/lib/c/psx.c +200 -0
- package/src/platforms/ps1/lib/c/psx.h +83 -0
- package/src/toolchains/index.js +65 -0
- package/src/toolchains/mips-c/lib/be/libc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libm.a +0 -0
- package/src/toolchains/mips-c/lib/el/libc.a +0 -0
- package/src/toolchains/mips-c/lib/el/libm.a +0 -0
- package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
- package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
- package/src/toolchains/mips-c/lib/n64.ld +15 -0
- package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
- package/src/toolchains/mips-c/lib/ps1.ld +15 -0
- package/src/toolchains/mips-c/lib/softint.c +37 -0
- package/src/toolchains/mips-c/mips-c.js +155 -0
- package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
- package/src/toolchains/sh-c/lib/dc-crt0.s +23 -0
- package/src/toolchains/sh-c/lib/dc.h +102 -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 +101 -0
- package/src/toolchains/sh-elf-gcc/gcc.js +122 -0
|
@@ -70,3 +70,17 @@ export const RETRO_LOG_ERROR = 3;
|
|
|
70
70
|
// Sentinels
|
|
71
71
|
/** Video refresh data pointer meaning "GL framebuffer is valid". */
|
|
72
72
|
export const RETRO_HW_FRAME_BUFFER_VALID = -1 >>> 0; // unsigned -1
|
|
73
|
+
|
|
74
|
+
// HW render context types (retro_hw_context_type) — the GL flavors the
|
|
75
|
+
// HW-render cores (n64/ps1) request via RETRO_ENVIRONMENT_SET_HW_RENDER.
|
|
76
|
+
export const RETRO_HW_CONTEXT_NONE = 0;
|
|
77
|
+
export const RETRO_HW_CONTEXT_OPENGL = 1; // OpenGL 2.x (compat)
|
|
78
|
+
export const RETRO_HW_CONTEXT_OPENGLES2 = 2; // GLES 2.0
|
|
79
|
+
export const RETRO_HW_CONTEXT_OPENGL_CORE = 3; // OpenGL 3+ core
|
|
80
|
+
export const RETRO_HW_CONTEXT_OPENGLES3 = 4; // GLES 3.0
|
|
81
|
+
export const RETRO_HW_CONTEXT_OPENGLES_VERSION = 5; // GLES with explicit version
|
|
82
|
+
|
|
83
|
+
// romdev-internal sentinel: a HW-render readback frame is already RGBA8888 (from
|
|
84
|
+
// glReadPixels GL_RGBA), NOT the BGRA byte order libretro's XRGB8888 uses. The
|
|
85
|
+
// framebuffer decoder special-cases this so R/B aren't swapped.
|
|
86
|
+
export const ROMDEV_PIXEL_FORMAT_RGBA8888 = 0x52474241; // 'RGBA'
|
package/src/mcp/tools/disasm.js
CHANGED
|
@@ -1609,6 +1609,8 @@ function sniffPlatformFromPath(p) {
|
|
|
1609
1609
|
if (/\.prg$/i.test(p)) return "c64";
|
|
1610
1610
|
if (/\.(lnx|lyx)$/i.test(p)) return "lynx";
|
|
1611
1611
|
if (/\.gba$/i.test(p)) return "gba";
|
|
1612
|
+
if (/\.(z64|n64|v64)$/i.test(p)) return "n64";
|
|
1613
|
+
if (/\.(psexe|psx)$/i.test(p)) return "ps1"; // .exe/.bin are ambiguous — pass platform explicitly
|
|
1612
1614
|
if (/\.(gen|md|bin)$/i.test(p)) return "genesis";
|
|
1613
1615
|
return null;
|
|
1614
1616
|
}
|
package/src/mcp/tools/frame.js
CHANGED
|
@@ -389,10 +389,14 @@ export function registerFrameTools(server, z, sessionKey) {
|
|
|
389
389
|
async function shootAscii({ cols, rows, symbols, colors, path: outPath, inline }) {
|
|
390
390
|
const host = getHost(sessionKey);
|
|
391
391
|
const { width, height, rgba } = host.screenshotRgba();
|
|
392
|
-
// Default
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
392
|
+
// Default to ONE cell per 8×8 tile (so a 256×224 NES frame → 32×28, legible
|
|
393
|
+
// game state). The old /16 default (16×14 for NES) was too coarse to read
|
|
394
|
+
// anything — feedback 0.44.0 #1. The terminal symbol's own subcell shape adds
|
|
395
|
+
// detail back, so this stays cheap.
|
|
396
|
+
if (cols == null) cols = Math.max(8, Math.floor(width / 8));
|
|
397
|
+
if (rows == null) rows = Math.max(8, Math.floor(height / 8));
|
|
398
|
+
// Warn when the caller forced a grid so coarse it can't show game state.
|
|
399
|
+
const tooCoarse = cols < Math.floor(width / 8) / 2 || rows < Math.floor(height / 8) / 2;
|
|
396
400
|
const { renderRgbaToAnsi } = await import("../../host/chafa-render.js");
|
|
397
401
|
const ansi = await renderRgbaToAnsi(rgba, width, height, { cols, rows, symbols, colors });
|
|
398
402
|
// Livestream sidebands: the human sees BOTH the real PNG and the ANSI.
|
|
@@ -401,15 +405,20 @@ export function registerFrameTools(server, z, sessionKey) {
|
|
|
401
405
|
_observerImages: [{ kind: "image", mimeType: "image/png", base64: shot.pngBase64 }],
|
|
402
406
|
_observerAnsi: ansi,
|
|
403
407
|
};
|
|
408
|
+
const coarseNote = tooCoarse
|
|
409
|
+
? `NOTE: ${cols}x${rows} is too coarse to read game state from this ${width}x${height} frame. `
|
|
410
|
+
+ `For a pass/fail check ("are we in gameplay?"), a memory({op:'read'}) byte assertion is cheaper and exact — `
|
|
411
|
+
+ `ascii is for a rough visual, not state.`
|
|
412
|
+
: null;
|
|
404
413
|
let result;
|
|
405
414
|
if (!inline) {
|
|
406
415
|
await writeFile(outPath, ansi, "utf-8");
|
|
407
|
-
result = jsonContent({ path: outPath, framebuffer: { width, height }, terminal: { cols, rows, symbols, colors }, ansiBytes: Buffer.byteLength(ansi, "utf-8") });
|
|
416
|
+
result = jsonContent({ path: outPath, framebuffer: { width, height }, terminal: { cols, rows, symbols, colors }, ansiBytes: Buffer.byteLength(ansi, "utf-8"), ...(coarseNote ? { note: coarseNote } : {}) });
|
|
408
417
|
} else {
|
|
409
418
|
result = {
|
|
410
419
|
content: [
|
|
411
420
|
{ type: "text", text: ansi },
|
|
412
|
-
{ type: "text", text: `framebuffer ${width}x${height} → terminal ${cols}x${rows} (${symbols}/${colors}, ${Buffer.byteLength(ansi, "utf-8")}B)` },
|
|
421
|
+
{ type: "text", text: `framebuffer ${width}x${height} → terminal ${cols}x${rows} (${symbols}/${colors}, ${Buffer.byteLength(ansi, "utf-8")}B)` + (coarseNote ? `\n${coarseNote}` : "") },
|
|
413
422
|
],
|
|
414
423
|
};
|
|
415
424
|
}
|
|
@@ -857,10 +866,10 @@ export function registerFrameTools(server, z, sessionKey) {
|
|
|
857
866
|
inline: z.boolean().default(false).describe("op=screenshot/stepAndShot: return the image in the response instead of writing to disk."),
|
|
858
867
|
overlayBoxes: z.boolean().default(false).describe("op=screenshot png: draw a colored bounding box per visible sprite (SNES+NES only)."),
|
|
859
868
|
scale: z.number().gt(0).max(16).refine((s) => s <= 1 || Number.isInteger(s), { message: "scale must be 0<scale≤1 (downscale) or an integer ≥2 (upscale)" }).optional().describe("op=screenshot png: nearest-neighbor resample factor. DEFAULT (unset/1) = NATIVE resolution — perfect pixels, the accurate representation; use this. 0<scale<1 DOWNscales (0.5 ≈ 75% fewer image tokens — useful for cheap 'did it change?' checks). integer scale≥2 UPscales by pixel-duplication (e.g. scale:4 → GB 160x144 → 640x576): it adds NO information (same pixels enlarged), costs MORE image tokens, and since VLM encoders resize to their own fixed resolution it may not change what the model sees and can slightly degrade it. Only for clients that render tiny images too small to use and can't zoom."),
|
|
860
|
-
cols: z.number().int().min(4).max(640).optional().describe("op=screenshot ascii: terminal columns (default fb_width/
|
|
861
|
-
rows: z.number().int().min(4).max(480).optional().describe("op=screenshot ascii: terminal rows (default fb_height/
|
|
869
|
+
cols: z.number().int().min(4).max(640).optional().describe("op=screenshot ascii: terminal columns (default fb_width/8 — one cell per 8×8 tile, legible game state)."),
|
|
870
|
+
rows: z.number().int().min(4).max(480).optional().describe("op=screenshot ascii: terminal rows (default fb_height/8)."),
|
|
862
871
|
symbols: z.enum(["ascii", "halfblock", "block", "quad", "sextant"]).default("ascii").describe("op=screenshot ascii: chafa symbol set."),
|
|
863
|
-
colors: z.enum(["true", "256", "16", "fgbg"]).default("
|
|
872
|
+
colors: z.enum(["true", "256", "16", "fgbg"]).default("256").describe("op=screenshot ascii: color depth. Default '256' (indexed) — far fewer ANSI escape bytes than 'true' (truecolor per cell) for a near-identical read. Use 'true' only when exact color matters."),
|
|
864
873
|
},
|
|
865
874
|
safeTool(async (args) => {
|
|
866
875
|
switch (args.op) {
|
|
@@ -21,7 +21,7 @@ export function registerLifecycleTools(server, z, sessionKey) {
|
|
|
21
21
|
if (path && base64) throw new Error("loadMedia: provide `path` OR `base64`, not both.");
|
|
22
22
|
const slotB = slot === "b";
|
|
23
23
|
const host = slotB ? resetHostB(sessionKey) : resetHost(sessionKey);
|
|
24
|
-
await host.loadCore(resolved.jsPath, resolved.wasmPath);
|
|
24
|
+
await host.loadCore(resolved.jsPath, resolved.wasmPath, { hwRender: resolved.hwRender });
|
|
25
25
|
const bytes = base64 ? new Uint8Array(Buffer.from(base64, "base64")) : undefined;
|
|
26
26
|
await host.loadMedia({
|
|
27
27
|
platform,
|
|
@@ -6,6 +6,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { PNG } from "pngjs";
|
|
8
8
|
import { getHost } from "../state.js";
|
|
9
|
+
import { naReason } from "../../cores/capabilities.js";
|
|
9
10
|
import { imageContent, jsonContent, unsupported } from "../util.js";
|
|
10
11
|
|
|
11
12
|
// Consolidation: several handlers in this big shared file are extracted as
|
|
@@ -39,6 +40,8 @@ import { decodeGbApu, decodeGbaApu } from "../../host/gb-apu-state.js";
|
|
|
39
40
|
import { decodeC64Sid } from "../../host/c64-sid-state.js";
|
|
40
41
|
import { decodeLynxMikey, decodeLynxPalette } from "../../host/lynx-mikey-state.js";
|
|
41
42
|
import { getPcePsgState } from "../../host/pce-psg-state.js";
|
|
43
|
+
import { decodePs1Spu } from "../../host/ps1-spu-state.js";
|
|
44
|
+
import { decodeN64Ai } from "../../host/n64-ai-state.js";
|
|
42
45
|
import { getMsxAyState } from "../../host/msx-ay-state.js";
|
|
43
46
|
import { decodeGbaSprites, decodeGbaPalette } from "../../host/gba-video-state.js";
|
|
44
47
|
|
|
@@ -339,7 +342,7 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
unsupported(p, "inspectPalette", {
|
|
342
|
-
reason: "no palette decoder for this platform",
|
|
345
|
+
reason: naReason(p, "inspectPalette") ?? "no palette decoder for this platform",
|
|
343
346
|
alternative: "platform({op:'capabilities'}) to see what's wired",
|
|
344
347
|
});
|
|
345
348
|
};
|
|
@@ -424,7 +427,20 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
424
427
|
if (!ay) throw new Error("getAudioState chip:'ay8910' — no PSG region (load an MSX ROM into the patched blueMSX core).");
|
|
425
428
|
return { platform: "msx", ...ay };
|
|
426
429
|
}
|
|
427
|
-
|
|
430
|
+
if (chip === "spu") {
|
|
431
|
+
// PS1 SPU — 24 ADPCM voices (volume/pitch/ADSR + key-on/off).
|
|
432
|
+
const regs = host.getSpuRegs?.();
|
|
433
|
+
if (!regs) throw new Error("getAudioState chip:'spu' — no SPU region (load a PS1 program into the rebuilt pcsx_rearmed core).");
|
|
434
|
+
return { platform: "ps1", ...decodePs1Spu(regs) };
|
|
435
|
+
}
|
|
436
|
+
if (chip === "ai") {
|
|
437
|
+
// N64 AI — the audio OUTPUT state (sample rate + playing + DMA source). N64
|
|
438
|
+
// audio is RSP-mixed, so there are no per-voice registers to decode.
|
|
439
|
+
const regs = host.getAiRegs?.();
|
|
440
|
+
if (!regs) throw new Error("getAudioState chip:'ai' — no AI region (load an N64 ROM into the rebuilt parallel_n64 core).");
|
|
441
|
+
return { platform: "n64", ...decodeN64Ai(regs) };
|
|
442
|
+
}
|
|
443
|
+
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), or 'spu' (PS1).`);
|
|
428
444
|
}
|
|
429
445
|
|
|
430
446
|
getAudioStateCore = async ({ chip }, callerSessionKey) => jsonContent(readAudioChip(chip, callerSessionKey));
|
|
@@ -701,7 +717,7 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
701
717
|
}
|
|
702
718
|
|
|
703
719
|
unsupported(p, "inspectSprites", {
|
|
704
|
-
reason: "no sprite decoder for this platform",
|
|
720
|
+
reason: naReason(p, "inspectSprites") ?? "no sprite decoder for this platform",
|
|
705
721
|
alternative: "memory({op:'read'}) the raw OAM/sprite-attribute region, or platform({op:'capabilities'}) to see what's wired",
|
|
706
722
|
});
|
|
707
723
|
};
|
|
@@ -813,7 +829,7 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
813
829
|
return emitImage(r.png, `SNES BG map composite (${r.width}×${r.height}, ${r.mapWidth}×${r.mapHeight} tiles, ${r.bpp}bpp, tilemap@0x${tilemapBaseByte.toString(16)}, tiles@0x${tileBaseByte.toString(16)}). ${r.note}`);
|
|
814
830
|
}
|
|
815
831
|
unsupported(p, "inspectBackground", {
|
|
816
|
-
reason: "no background-map snapshotter for this platform",
|
|
832
|
+
reason: naReason(p, "inspectBackground") ?? "no background-map snapshotter for this platform",
|
|
817
833
|
alternative: "background({view:'renderState'}) for the register-level context, or memory({op:'read'}) the raw VRAM/nametable region",
|
|
818
834
|
});
|
|
819
835
|
};
|
|
@@ -2,7 +2,7 @@ import { CORES, listAvailableCores, resolveCore } from "../../cores/registry.js"
|
|
|
2
2
|
import { TOOLCHAINS } from "../../toolchains/registry.js";
|
|
3
3
|
import { getLanguageOptions } from "../../toolchains/index.js";
|
|
4
4
|
import { jsonContent, safeTool } from "../util.js";
|
|
5
|
-
import { CAPABILITIES,
|
|
5
|
+
import { CAPABILITIES, capabilitiesFor, OP_KEYS, naReason } from "../../cores/capabilities.js";
|
|
6
6
|
import { listToolchainsCore, installToolchainCore } from "./toolchain.js";
|
|
7
7
|
import { listPlatformDocsCore, getPlatformDocCore } from "./platform-docs.js";
|
|
8
8
|
|
|
@@ -66,6 +66,30 @@ const PLATFORM_QUIRKS = {
|
|
|
66
66
|
],
|
|
67
67
|
starterSnippets: ["hello_msx.c", "msx_crt0.s"],
|
|
68
68
|
},
|
|
69
|
+
ps1: {
|
|
70
|
+
multiBank: false,
|
|
71
|
+
maxRomBytesPerBank: 0, // disc/EXE based, not a fixed cart
|
|
72
|
+
headerLocation: "PS-EXE: 'PS-X EXE' magic at 0, entry (pc0) at +0x10, load addr (t_addr) at +0x18; 2048-byte header then code",
|
|
73
|
+
notes: [
|
|
74
|
+
"32-bit MIPS R3000 (little-endian), framebuffer GPU. Runs via PCSX-ReARMed with its BUILT-IN HLE BIOS — no firmware file needed, software-rendered. Load a .exe (PS-EXE), or a disc image if you have one.",
|
|
75
|
+
"WORKS NOW: build (buildSource language:'c' → mips-elf-gcc → a PS-EXE the HLE BIOS runs; write GPU/SPU registers directly, no SDK yet), run, frame({op:'screenshot'}), cpu({op:'read'}) (live R3000 registers), cheats, readMemory/writeMemory (system_ram = 2MB main RAM), disasm + decompile (Ghidra MIPS C), the live-debug tools breakpoint({on:'pc'/'write'/'read'}) + watch({on:'range'}), AND getAudioState({chip:'spu'}) (24-voice SPU). MIPS is little-endian here.",
|
|
76
|
+
"The bare build path: a minimal crt0 sets the stack + clears .bss + calls main(); code loads at 0x80010000. No PSn00bSDK runtime yet — drive the GPU at ports 0x1F801810/0x1F801814. The framebuffer renderer has no tile/sprite inspectors — use screenshot + memory.",
|
|
77
|
+
"For higher fidelity (GL hardware renderer) a real PS1 BIOS + the beetle_psx_hw core is the alternative, but the HLE pcsx_rearmed path ships clean with zero firmware.",
|
|
78
|
+
],
|
|
79
|
+
starterSnippets: [],
|
|
80
|
+
},
|
|
81
|
+
n64: {
|
|
82
|
+
multiBank: false,
|
|
83
|
+
maxRomBytesPerBank: 0,
|
|
84
|
+
headerLocation: ".z64 (big-endian) magic 80 37 12 40 at 0; entry point (big-endian word) at +0x08; 0x1000-byte IPL3 bootcode then game code. .v64/.n64 byte orders are auto-normalized.",
|
|
85
|
+
notes: [
|
|
86
|
+
"32-bit MIPS R4300 (big-endian), 3D RDP/RSP. Runs via ParaLLEl-N64 with the glide64 GL renderer — HW-rendered through the OPTIONAL native GL stack (native-gles + webgl-node). Those are optionalDependencies: install them for N64; the other platforms don't need them.",
|
|
87
|
+
"WORKS NOW: build (buildSource language:'c' → mips-elf-gcc → a big-endian MIPS image; bare crt0, no libdragon yet so it's logic-only for now), run, frame({op:'screenshot'}) (real 3D frames, headless via FBO readback), cpu({op:'read'}) (live R4300 registers), cheats, readMemory/writeMemory (system_ram = 8MB RDRAM, 0x80xxxxxx maps to offset 0), disasm + decompile (Ghidra MIPS C), breakpoint + watch (live-debug instrumentation), AND getAudioState({chip:'ai'}) (audio output: sample rate + playing). MIPS is big-endian here.",
|
|
88
|
+
"build caveat: a fully BOOTABLE N64 ROM needs the IPL3 bootcode + a libdragon-style header (libdragon SDK forthcoming). The bare build compiles+links your C to a flat image — great for logic/RE, not yet a self-booting cart. 3D renderer has no tile/sprite inspectors — use screenshot + memory.",
|
|
89
|
+
"If frame() errors with an install hint, the optional GL module isn't installed: `npm install native-gles webgl-node`.",
|
|
90
|
+
],
|
|
91
|
+
starterSnippets: [],
|
|
92
|
+
},
|
|
69
93
|
};
|
|
70
94
|
|
|
71
95
|
/** op:'list' — every platform with core/toolchains/languages/quirks. */
|
|
@@ -129,10 +153,20 @@ export function registerPlatformTools(server, z) {
|
|
|
129
153
|
case "capabilities": {
|
|
130
154
|
if (args.platform) {
|
|
131
155
|
const cap = capabilitiesFor(args.platform);
|
|
132
|
-
if (!cap) throw new Error(`platform({op:'capabilities'}): unknown platform '${args.platform}'. Known: ${
|
|
133
|
-
|
|
156
|
+
if (!cap) throw new Error(`platform({op:'capabilities'}): unknown platform '${args.platform}'. Known: ${Object.keys(CAPABILITIES).join(", ")}.`);
|
|
157
|
+
// Call out WHY each unsupported op is N/A by hardware (a framebuffer/3D
|
|
158
|
+
// renderer has no tile/sprite tables; a disc system has no cart) so an
|
|
159
|
+
// agent reading the manifest sees "can't, because hardware" — not a
|
|
160
|
+
// bare `false` it might mistake for "not built yet".
|
|
161
|
+
const naReasons = {};
|
|
162
|
+
for (const op of OP_KEYS) {
|
|
163
|
+
if (!cap.ops[op]) { const r = naReason(args.platform, op); if (r) naReasons[op] = r; }
|
|
164
|
+
}
|
|
165
|
+
return jsonContent({ platform: args.platform, ...cap, ...(Object.keys(naReasons).length ? { naReasons } : {}) });
|
|
134
166
|
}
|
|
135
|
-
|
|
167
|
+
// All platforms incl. the partial MIPS tier (ps1/n64) — so an agent can
|
|
168
|
+
// discover their capability map and see which ops are live vs not-yet.
|
|
169
|
+
return jsonContent({ platforms: Object.keys(CAPABILITIES), capabilities: CAPABILITIES });
|
|
136
170
|
}
|
|
137
171
|
case "resolve": {
|
|
138
172
|
if (!args.platform) throw new Error("platform({op:'resolve'}): `platform` is required.");
|
package/src/mcp/tools/project.js
CHANGED
|
@@ -546,6 +546,122 @@ const TEMPLATES = {
|
|
|
546
546
|
},
|
|
547
547
|
};
|
|
548
548
|
})(),
|
|
549
|
+
|
|
550
|
+
// ── PlayStation (mips-elf-gcc R3000) — software 3D engine helper lib + examples ──
|
|
551
|
+
// The PS1 is a 3D machine: the helper lib (psx.{h,c}) is a real software 3D
|
|
552
|
+
// pipeline (fixed-point transform + perspective + cull). 4 examples are 3D; the
|
|
553
|
+
// puzzle is 2D (a flat grid is the right idiom even on 3D hardware).
|
|
554
|
+
ps1: (() => {
|
|
555
|
+
const PS1_RUNTIME = [
|
|
556
|
+
{ src: "lib/c/psx.h", dst: "psx.h" },
|
|
557
|
+
{ src: "lib/c/psx.c", dst: "psx.c" },
|
|
558
|
+
];
|
|
559
|
+
const mk = (name, describe) => ({ main: `${name}/main.c`, runtime: PS1_RUNTIME, lang: "C (mips-elf-gcc)", ext: ".exe", describe });
|
|
560
|
+
return {
|
|
561
|
+
default: mk("shmup", "STARFALL — the canonical PS1 3D starter: a vertical shooter where enemies fly in from depth and grow under perspective projection. Exercises the whole 3D helper lib (camera, model transform, culled cubes) + SIO pad + HUD. Same as 'shmup'."),
|
|
562
|
+
shmup: {
|
|
563
|
+
...mk("shmup", "STARFALL — a 3D PlayStation vertical shooter. The playfield recedes into the screen; enemy cubes fly in from the far distance and scale up under perspective as they approach. Stream bullets into Z, AABB collision, xorshift wave spawner, parallax starfield, title/play/game-over with score + lives. Built on the software 3D pipeline (psx_camera/psx_model/psx_quad3d, 16.16 fixed point)."),
|
|
564
|
+
players: "1 (PS1 has two pad ports; a 2P round can hook port 2 later)",
|
|
565
|
+
sram: "none in this starter — PS1 saves go to a Memory Card via the BIOS; the helper lib keeps hi-score in-session (stated in-file).",
|
|
566
|
+
mechanics: ["3D perspective playfield", "depth-scaled enemies", "projectile pools", "wave spawner", "AABB collision", "lives + score", "title/play/game-over state machine"],
|
|
567
|
+
techniques: ["software 3D: fixed-point camera + model transform", "perspective projection + back-face cull", "GPU flat-shaded quads (GP0 0x20/0x60)", "SIO controller polling", "blocky HUD number font"],
|
|
568
|
+
},
|
|
569
|
+
racing: {
|
|
570
|
+
...mk("racing", "POLE BENDER — a 3D PlayStation racer. The road is a ribbon of perspective quads receding to the horizon and bending with a sine curve; you steer a car between the verges as the world scrolls toward you, rival cubes growing as you close on them. The PS1 signature: a real 3D track, not pseudo-3D scaling. Distance score, collision spin-out, title/race/results."),
|
|
571
|
+
players: "1",
|
|
572
|
+
sram: "none (in-session best time/score; Memory Card path is the real-hardware save).",
|
|
573
|
+
mechanics: ["3D curved road (perspective quads)", "throttle/brake/steer physics", "rival traffic + collision", "distance scoring", "title/race/results state machine"],
|
|
574
|
+
techniques: ["receding road segments with depth-driven centerline curve", "no-cull ground-plane quads (psx_quad3d_nc)", "chase camera with downward tilt", "16.16 fixed-point world scroll"],
|
|
575
|
+
},
|
|
576
|
+
platformer: {
|
|
577
|
+
...mk("platformer", "BLOCK HOP — a 3D PlayStation platformer. A cube hero runs and jumps across floating platforms drawn in perspective; gravity + jump physics in 16.16 fixed point, AABB landing on platform tops, coins to collect, a lethal pit, a chase camera that follows the hero. Title/play/game-over, score + lives."),
|
|
578
|
+
players: "1",
|
|
579
|
+
sram: "none (in-session hi-score; Memory Card is the real save path).",
|
|
580
|
+
mechanics: ["gravity + jump physics", "platform-top AABB landing", "coin pickups", "lethal pit + lives", "follow camera", "title/play/game-over"],
|
|
581
|
+
techniques: ["3D platforms as no-cull flat-topped boxes", "culled hero/coin cubes", "smooth-follow camera (psx_camera per frame)", "fixed-point physics"],
|
|
582
|
+
},
|
|
583
|
+
sports: {
|
|
584
|
+
...mk("sports", "SLAM COURT — a 3D PlayStation sports game (air-hockey / pong). A perspective court seen down its length; you control the near paddle, the CPU the far one, and the ball bounces in 3D (X across, Z into the screen), its size changing with depth. First to 7. Title/match/game-over."),
|
|
585
|
+
players: "1 (vs CPU; a 2P split-paddle round can hook port 2 later)",
|
|
586
|
+
sram: "none (match score is in-session).",
|
|
587
|
+
mechanics: ["3D ball physics (X/Z)", "player + CPU paddles", "wall bounces + scoring", "tracking CPU AI", "first-to-7 match", "title/match/game-over"],
|
|
588
|
+
techniques: ["perspective court floor (no-cull quad)", "depth-scaled ball", "paddle/ball cubes at world positions", "capped CPU tracking"],
|
|
589
|
+
},
|
|
590
|
+
puzzle: {
|
|
591
|
+
...mk("puzzle", "DROP GRID — the one 2D game in the PS1 set (a flat grid is the right idiom even on 3D hardware). A falling-block puzzle: colored blocks drop down a well, full rows clear and score, speed ramps up, stack the top = game over. Drawn with the GPU's 2D rectangle primitive. Title/play/game-over."),
|
|
592
|
+
players: "1",
|
|
593
|
+
sram: "none (in-session hi-score).",
|
|
594
|
+
mechanics: ["integer grid model", "falling block move/drop", "full-row clear + scoring", "ramping fall speed", "stack-out game over", "title/play/game-over"],
|
|
595
|
+
techniques: ["2D GPU rectangles (GP0 0x60 variable-size rect)", "integer board logic (no 3D needed)", "SIO pad edge detection", "row-shift clear"],
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
})(),
|
|
599
|
+
|
|
600
|
+
// ── Nintendo 64 (mips-elf-gcc R4300) — software 3D engine helper lib + examples ──
|
|
601
|
+
// The N64 was a 3D-first machine, so ALL 5 examples are 3D (even the puzzle —
|
|
602
|
+
// rendered as a 3D well of cubes). Same software-3D lib as PS1 (n64.{h,c}), a
|
|
603
|
+
// framebuffer backend the headless-angrylion core scans out.
|
|
604
|
+
n64: (() => {
|
|
605
|
+
const N64_RUNTIME = [
|
|
606
|
+
{ src: "lib/c/n64.h", dst: "n64.h" },
|
|
607
|
+
{ src: "lib/c/n64.c", dst: "n64.c" },
|
|
608
|
+
];
|
|
609
|
+
const mk = (name, describe) => ({ main: `${name}/main.c`, runtime: N64_RUNTIME, lang: "C (mips-elf-gcc)", ext: ".z64", describe });
|
|
610
|
+
return {
|
|
611
|
+
default: mk("shmup", "STARFALL 64 — the canonical N64 3D starter: a vertical shooter where enemies fly in from depth and scale up under perspective. Exercises the whole software-3D lib (camera, transform, culled cubes) + SI/PIF pad + HUD, presented through angrylion's VI scanout. Same as 'shmup'."),
|
|
612
|
+
shmup: {
|
|
613
|
+
...mk("shmup", "STARFALL 64 — a 3D Nintendo 64 vertical shooter (the N64 twin of the PS1 STARFALL). Enemy cubes fly in from the far distance and grow under perspective; stream bullets into Z, AABB collision, xorshift wave spawner, starfield, title/play/game-over with score + lives. Software 3D pipeline (n64_camera/n64_model/n64_quad3d) rendered to an RDRAM framebuffer the headless-angrylion core scans out."),
|
|
614
|
+
players: "1 (N64 has 4 controller ports; 2-4P can hook the other ports later)",
|
|
615
|
+
sram: "none in this starter — N64 saves go to Controller Pak / EEPROM / SRAM via the PI; the lib keeps hi-score in-session (stated in-file).",
|
|
616
|
+
mechanics: ["3D perspective playfield", "depth-scaled enemies", "projectile pools", "wave spawner", "AABB collision", "lives + score", "title/play/game-over state machine"],
|
|
617
|
+
techniques: ["software 3D: fixed-point camera + model transform", "perspective projection + back-face cull", "software triangle rasterizer → RDRAM framebuffer", "VI scanout (correct VI register setup)", "SI/PIF controller poll"],
|
|
618
|
+
},
|
|
619
|
+
racing: {
|
|
620
|
+
...mk("racing", "POLE BENDER 64 — a 3D Nintendo 64 racer. A perspective road ribbon recedes to the horizon and bends with a sine curve; steer between the verges as the world scrolls toward you, rival cubes growing as you close. Distance score, collision spin-out, title/race/results. The N64's 3D heritage on display."),
|
|
621
|
+
players: "1",
|
|
622
|
+
sram: "none (in-session best; Controller Pak/EEPROM is the real-hardware save).",
|
|
623
|
+
mechanics: ["3D curved road (perspective quads)", "throttle/brake/steer", "rival traffic + collision", "distance scoring", "title/race/results"],
|
|
624
|
+
techniques: ["receding road segments with depth-driven curve", "no-cull ground-plane quads", "chase camera", "16.16 fixed-point world scroll"],
|
|
625
|
+
},
|
|
626
|
+
platformer: {
|
|
627
|
+
...mk("platformer", "BLOCK HOP 64 — a 3D Nintendo 64 platformer. A cube hero runs and jumps across floating platforms in perspective; gravity + jump physics (16.16 fixed point), AABB landing on platform tops, coins, a lethal pit, a follow camera. Title/play/game-over, score + lives."),
|
|
628
|
+
players: "1",
|
|
629
|
+
sram: "none (in-session hi-score).",
|
|
630
|
+
mechanics: ["gravity + jump physics", "platform-top AABB landing", "coin pickups", "lethal pit + lives", "follow camera", "title/play/game-over"],
|
|
631
|
+
techniques: ["3D platforms as flat-topped boxes", "culled hero/coin cubes", "smooth-follow camera", "fixed-point physics"],
|
|
632
|
+
},
|
|
633
|
+
sports: {
|
|
634
|
+
...mk("sports", "SLAM COURT 64 — a 3D Nintendo 64 sports game (air-hockey / pong). A perspective court down its length; you control the near paddle, the CPU the far, the ball bounces in 3D (X across, Z into the screen) and scales with depth. First to 7. Title/match/game-over."),
|
|
635
|
+
players: "1 (vs CPU; the N64's extra ports can host 2-4P later)",
|
|
636
|
+
sram: "none (match score is in-session).",
|
|
637
|
+
mechanics: ["3D ball physics (X/Z)", "player + CPU paddles", "wall bounces + scoring", "tracking CPU AI", "first-to-7", "title/match/game-over"],
|
|
638
|
+
techniques: ["perspective court floor (no-cull quad)", "depth-scaled ball", "paddle/ball cubes", "capped CPU tracking"],
|
|
639
|
+
},
|
|
640
|
+
puzzle: {
|
|
641
|
+
...mk("puzzle", "DROP GRID 64 — a 3D Nintendo 64 falling-block puzzle. Unlike the flat-2D PS1 puzzle, this is rendered in 3D (the N64 was a 3D-first machine): the well is a perspective box of cube walls and the blocks are shaded cubes you watch fall in depth. Move/drop, full-row clear + scoring, ramping speed, stack-out = game over. Title/play/game-over."),
|
|
642
|
+
players: "1",
|
|
643
|
+
sram: "none (in-session hi-score).",
|
|
644
|
+
mechanics: ["integer grid model", "falling block move/drop", "full-row clear + scoring", "ramping fall speed", "stack-out game over", "title/play/game-over"],
|
|
645
|
+
techniques: ["3D-rendered well (cube walls + floor)", "blocks as shaded 3D cubes", "tilted camera into the well", "integer board logic", "SI/PIF pad edge detection"],
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
})(),
|
|
649
|
+
|
|
650
|
+
// ── Sega Dreamcast (sh-elf-gcc SH-4) — bare PowerVR2 framebuffer via the bundled
|
|
651
|
+
// dc.h helper; the output ELF boots DIRECTLY on Flycast's reios HLE BIOS (no GD-ROM
|
|
652
|
+
// image, no firmware) and renders on the real GPU through native-gles. dc.h is
|
|
653
|
+
// auto-bundled by the toolchain, so the example is a single main.c. No KallistiOS
|
|
654
|
+
// and no genre scaffolds yet — `hello` is the verified renderable starting point. ──
|
|
655
|
+
dreamcast: {
|
|
656
|
+
default: { main: "hello/main.c", runtime: [], lang: "C (sh-elf-gcc)", ext: ".elf",
|
|
657
|
+
describe: "DCHELLO — the canonical Dreamcast starter: bring up the PowerVR2 640x480 RGB565 framebuffer (via the auto-bundled dc.h: FB_R_CTRL/SIZE/SOF1 + SPG) and paint a test pattern (dark-blue field, red/green/blue bars, white frame). Proves the SH-4 build → Flycast reios HLE boot → native-gles GPU render pipeline end-to-end. The base to grow your own DC graphics from. Same as 'hello'." },
|
|
658
|
+
hello: { main: "hello/main.c", runtime: [], lang: "C (sh-elf-gcc)", ext: ".elf",
|
|
659
|
+
describe: "DCHELLO — a minimal Dreamcast homebrew. dc_video_init() programs the PowerVR2 framebuffer registers for 640x480 RGB565 at VRAM 0xA5000000; then dc_clear/dc_rect paint a recognizable pattern. Boots directly on Flycast's reios HLE BIOS (no firmware) and presents through native-gles with flycast_emulate_framebuffer — no TA display list needed.",
|
|
660
|
+
players: "1 (Dreamcast has 4 controller ports; the bare path doesn't wire the Maple bus yet)",
|
|
661
|
+
sram: "none in this starter — DC saves go to the VMU via the Maple bus; not wired in the bare path.",
|
|
662
|
+
mechanics: ["direct framebuffer paint (clear + solid rects)", "recognizable test pattern"],
|
|
663
|
+
techniques: ["PowerVR2 framebuffer bring-up (FB_R_CTRL/FB_R_SIZE/FB_R_SOF1 + SPG)", "640x480 RGB565 at VRAM 0xA5000000", "SH-4 bare crt0 (stack + .bss + main)", "boots on Flycast reios HLE — no firmware"] },
|
|
664
|
+
},
|
|
549
665
|
};
|
|
550
666
|
// R37: GBC has its own scaffold tree at examples/gbc/templates/ +
|
|
551
667
|
// src/platforms/gbc/lib/c/. Same runtime files as GB (the APU + Z80 +
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { getHost } from "../state.js";
|
|
12
12
|
import { jsonContent, safeTool, unsupported } from "../util.js";
|
|
13
|
+
import { naReason } from "../../cores/capabilities.js";
|
|
13
14
|
import { inspectBackgroundMapCore } from "./platform-tools.js";
|
|
14
15
|
import { whichTilesAreRenderedCore } from "./which-tiles.js";
|
|
15
16
|
|
|
@@ -533,7 +534,7 @@ export async function getRenderingContextCore({ platform, area = "all", sessionK
|
|
|
533
534
|
case "msx": return msxContext(host, area);
|
|
534
535
|
default:
|
|
535
536
|
unsupported(p, "renderingContext", {
|
|
536
|
-
reason: "no rendering-context decoder for this platform",
|
|
537
|
+
reason: naReason(p, "renderingContext") ?? "no rendering-context decoder for this platform",
|
|
537
538
|
alternative: "platform({op:'capabilities'}) to see what's wired",
|
|
538
539
|
});
|
|
539
540
|
}
|
|
@@ -603,7 +603,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
603
603
|
}
|
|
604
604
|
|
|
605
605
|
const host = resetHost(sessionKey);
|
|
606
|
-
await host.loadCore(resolved.jsPath, resolved.wasmPath);
|
|
606
|
+
await host.loadCore(resolved.jsPath, resolved.wasmPath, { hwRender: resolved.hwRender });
|
|
607
607
|
// Pass projectName as a virtualName so the playtest window titles itself
|
|
608
608
|
// with the game name (the SDL window reads host.status.mediaPath). Keep
|
|
609
609
|
// a platform-correct extension so shared cores still resolve the system.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/* n64.c — N64 helpers + software 3D pipeline (see n64.h). Framebuffer backend. */
|
|
2
|
+
#include "n64.h"
|
|
3
|
+
|
|
4
|
+
/* Two 320x240x16bpp framebuffers in RDRAM (double-buffered), cached kseg0. */
|
|
5
|
+
#define FB0 0xA0100000u /* uncached kseg1 — writes go straight to RDRAM */
|
|
6
|
+
#define FB1 0xA0120000u
|
|
7
|
+
static volatile unsigned short *fb; /* the back buffer we draw into */
|
|
8
|
+
static unsigned int front;
|
|
9
|
+
|
|
10
|
+
/* VI registers (0xA4400000). */
|
|
11
|
+
#define VI(n) (*(volatile unsigned int*)(0xA4400000u + (n)*4))
|
|
12
|
+
#define VI_STATUS 0
|
|
13
|
+
#define VI_ORIGIN 1
|
|
14
|
+
#define VI_WIDTH 2
|
|
15
|
+
#define VI_V_CURRENT 4
|
|
16
|
+
#define VI_H_START 9
|
|
17
|
+
#define VI_V_START 10
|
|
18
|
+
#define VI_X_SCALE 12
|
|
19
|
+
#define VI_Y_SCALE 13
|
|
20
|
+
|
|
21
|
+
void n64_init(void)
|
|
22
|
+
{
|
|
23
|
+
/* NTSC 320x240 16bpp — register indices per the N64 VI map:
|
|
24
|
+
0=STATUS 1=ORIGIN 2=WIDTH 3=V_INTR 4=CURRENT 5=BURST 6=V_SYNC 7=H_SYNC
|
|
25
|
+
8=LEAP 9=H_START 10=V_START 11=V_BURST 12=X_SCALE 13=Y_SCALE. */
|
|
26
|
+
VI(0) = 0x0000320E; /* STATUS: 16bpp(type=2) + AA + dither + pixel_advance */
|
|
27
|
+
VI(2) = 320; /* WIDTH */
|
|
28
|
+
VI(3) = 2; /* V_INTR */
|
|
29
|
+
VI(4) = 0; /* CURRENT */
|
|
30
|
+
VI(5) = 0x03E52239; /* BURST (NTSC) */
|
|
31
|
+
VI(6) = 0x0000020D; /* V_SYNC = 525 */
|
|
32
|
+
VI(7) = 0x00000C15; /* H_SYNC */
|
|
33
|
+
VI(8) = 0x0C150C15; /* LEAP */
|
|
34
|
+
VI(9) = 0x006C02EC; /* H_START: 108..748 */
|
|
35
|
+
VI(10) = 0x002501FF; /* V_START: 37..511 */
|
|
36
|
+
VI(11) = 0x000E0204; /* V_BURST */
|
|
37
|
+
VI(12) = 0x00000200; /* X_SCALE (320) */
|
|
38
|
+
VI(13) = 0x00000400; /* Y_SCALE (240) */
|
|
39
|
+
front = FB0;
|
|
40
|
+
fb = (volatile unsigned short *)FB1;
|
|
41
|
+
VI(1) = FB0 & 0x1FFFFFFF; /* ORIGIN */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static inline void putpx(int x, int y, unsigned short c)
|
|
45
|
+
{
|
|
46
|
+
if ((unsigned)x < SCREEN_W && (unsigned)y < SCREEN_H) fb[y * SCREEN_W + x] = c;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
void n64_clear(unsigned short col)
|
|
50
|
+
{
|
|
51
|
+
int i; for (i = 0; i < SCREEN_W * SCREEN_H; i++) fb[i] = col;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void n64_rect(int x, int y, int w, int h, unsigned short col)
|
|
55
|
+
{
|
|
56
|
+
int r, c;
|
|
57
|
+
for (r = 0; r < h; r++) for (c = 0; c < w; c++) putpx(x + c, y + r, col);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* edge function for the rasterizer */
|
|
61
|
+
static inline int edge(int ax, int ay, int bx, int by, int cx, int cy)
|
|
62
|
+
{ return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); }
|
|
63
|
+
|
|
64
|
+
void n64_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col)
|
|
65
|
+
{
|
|
66
|
+
int minx = x0, maxx = x0, miny = y0, maxy = y0, px, py, w0, w1, w2, area;
|
|
67
|
+
if (x1 < minx) minx = x1; if (x1 > maxx) maxx = x1;
|
|
68
|
+
if (x2 < minx) minx = x2; if (x2 > maxx) maxx = x2;
|
|
69
|
+
if (y1 < miny) miny = y1; if (y1 > maxy) maxy = y1;
|
|
70
|
+
if (y2 < miny) miny = y2; if (y2 > maxy) maxy = y2;
|
|
71
|
+
if (minx < 0) minx = 0; if (miny < 0) miny = 0;
|
|
72
|
+
if (maxx >= SCREEN_W) maxx = SCREEN_W - 1; if (maxy >= SCREEN_H) maxy = SCREEN_H - 1;
|
|
73
|
+
area = edge(x0, y0, x1, y1, x2, y2);
|
|
74
|
+
if (area == 0) return;
|
|
75
|
+
for (py = miny; py <= maxy; py++) {
|
|
76
|
+
for (px = minx; px <= maxx; px++) {
|
|
77
|
+
w0 = edge(x1, y1, x2, y2, px, py);
|
|
78
|
+
w1 = edge(x2, y2, x0, y0, px, py);
|
|
79
|
+
w2 = edge(x0, y0, x1, y1, px, py);
|
|
80
|
+
if (area > 0) { if (w0 >= 0 && w1 >= 0 && w2 >= 0) fb[py * SCREEN_W + px] = col; }
|
|
81
|
+
else { if (w0 <= 0 && w1 <= 0 && w2 <= 0) fb[py * SCREEN_W + px] = col; }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
void n64_flip(void)
|
|
87
|
+
{
|
|
88
|
+
/* swap buffers + point VI at the freshly drawn one, then wait for a frame. */
|
|
89
|
+
unsigned int newfront = (unsigned int)(unsigned long)fb;
|
|
90
|
+
VI(1) = newfront & 0x1FFFFFFF;
|
|
91
|
+
fb = (volatile unsigned short *)(front);
|
|
92
|
+
front = newfront;
|
|
93
|
+
{ volatile int i; for (i = 0; i < 200000; i++) { } }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── input: read controller port 0 via the SI/PIF. The PIF command 0x01 polls
|
|
97
|
+
the pad; the 16-bit button word is at PIF RAM. We use the simple JoyBus poll. ── */
|
|
98
|
+
static unsigned int read_pad(void)
|
|
99
|
+
{
|
|
100
|
+
volatile unsigned int *pif = (volatile unsigned int *)0xBFC007C0; /* PIF RAM */
|
|
101
|
+
volatile unsigned int *si = (volatile unsigned int *)0xA4800000;
|
|
102
|
+
unsigned int buttons;
|
|
103
|
+
/* command block: read controller 0 (1 byte cmd 0x01, 1 byte send, ...) */
|
|
104
|
+
pif[0] = 0xFF010401; pif[1] = 0xFFFFFFFF; pif[2] = 0xFFFFFFFF;
|
|
105
|
+
pif[3] = 0xFE000000; pif[4] = 0; pif[5] = 0; pif[6] = 0; pif[7] = 1;
|
|
106
|
+
si[1] = 0x1FC007C0; /* SI_PIF_ADDR_RD64B: kick the PIF read */
|
|
107
|
+
{ volatile int t; for (t = 0; t < 5000; t++) { } }
|
|
108
|
+
buttons = pif[1] >> 16; /* the button half-word */
|
|
109
|
+
return (~0u) & buttons; /* JoyBus buttons are active-high already */
|
|
110
|
+
}
|
|
111
|
+
unsigned int n64_pad(void) { return read_pad(); }
|
|
112
|
+
int n64_pressed(unsigned int mask) { return (n64_pad() & mask) ? 1 : 0; }
|
|
113
|
+
|
|
114
|
+
/* ── trig (shared with PS1 lib: 256-step binary angle) ── */
|
|
115
|
+
static const short SINTAB[64] = {
|
|
116
|
+
0,804,1608,2410,3212,4011,4808,5602,6393,7179,7962,8739,9512,10278,11039,11793,
|
|
117
|
+
12539,13279,14010,14732,15446,16151,16846,17530,18204,18868,19519,20159,20787,21403,
|
|
118
|
+
22005,22594,23170,23731,24279,24811,25329,25832,26319,26790,27245,27683,28105,28510,
|
|
119
|
+
28898,29268,29621,29956,30273,30571,30852,31113,31356,31580,31785,31971,32137,32285,
|
|
120
|
+
32412,32521,32609,32678,32728,32757 };
|
|
121
|
+
static fix sin_lut(int idx) {
|
|
122
|
+
int q = (idx >> 6) & 3, p = idx & 63; int v;
|
|
123
|
+
if (q == 0) v = SINTAB[p]; else if (q == 1) v = SINTAB[63 - p];
|
|
124
|
+
else if (q == 2) v = -SINTAB[p]; else v = -SINTAB[63 - p];
|
|
125
|
+
return ((fix)v << 16) / 32768;
|
|
126
|
+
}
|
|
127
|
+
fix n64_sin(fix a) { return sin_lut((F2I(a)) & 255); }
|
|
128
|
+
fix n64_cos(fix a) { return sin_lut((F2I(a) + 64) & 255); }
|
|
129
|
+
|
|
130
|
+
/* ── camera + model (identical to the PS1 lib) ── */
|
|
131
|
+
static fix cam_x, cam_y, cam_z, cam_cy, cam_sy, cam_cp, cam_sp;
|
|
132
|
+
static fix mdl_x, mdl_y, mdl_z, mdl_cy, mdl_sy;
|
|
133
|
+
void n64_camera(fix ex, fix ey, fix ez, fix yaw, fix pitch)
|
|
134
|
+
{ cam_x=ex;cam_y=ey;cam_z=ez; cam_cy=n64_cos(yaw);cam_sy=n64_sin(yaw); cam_cp=n64_cos(pitch);cam_sp=n64_sin(pitch); }
|
|
135
|
+
void n64_model(fix tx, fix ty, fix tz, fix yaw)
|
|
136
|
+
{ mdl_x=tx;mdl_y=ty;mdl_z=tz; mdl_cy=n64_cos(yaw);mdl_sy=n64_sin(yaw); }
|
|
137
|
+
|
|
138
|
+
static Vec3 to_cam(Vec3 v)
|
|
139
|
+
{
|
|
140
|
+
fix mx = FMUL(v.x, mdl_cy) + FMUL(v.z, mdl_sy);
|
|
141
|
+
fix mz = -FMUL(v.x, mdl_sy) + FMUL(v.z, mdl_cy);
|
|
142
|
+
fix wx = mx + mdl_x, wy = v.y + mdl_y, wz = mz + mdl_z;
|
|
143
|
+
fix rx = wx - cam_x, ry = wy - cam_y, rz = wz - cam_z;
|
|
144
|
+
fix cx = FMUL(rx, cam_cy) - FMUL(rz, cam_sy);
|
|
145
|
+
fix cz = FMUL(rx, cam_sy) + FMUL(rz, cam_cy);
|
|
146
|
+
fix cy = FMUL(ry, cam_cp) - FMUL(cz, cam_sp);
|
|
147
|
+
fix cz2 = FMUL(ry, cam_sp) + FMUL(cz, cam_cp);
|
|
148
|
+
Vec3 o; o.x = cx; o.y = cy; o.z = cz2; return o;
|
|
149
|
+
}
|
|
150
|
+
#define NEAR FIXF(0.5f)
|
|
151
|
+
#define FOV FIX(220)
|
|
152
|
+
static int project(Vec3 c, int *sx, int *sy)
|
|
153
|
+
{
|
|
154
|
+
if (c.z <= NEAR) return 0;
|
|
155
|
+
*sx = (SCREEN_W / 2) + F2I(FDIV(FMUL(c.x, FOV), c.z));
|
|
156
|
+
*sy = (SCREEN_H / 2) - F2I(FDIV(FMUL(c.y, FOV), c.z));
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
void n64_tri3d(Vec3 a, Vec3 b, Vec3 c, unsigned short col)
|
|
160
|
+
{
|
|
161
|
+
Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c);
|
|
162
|
+
int x0,y0,x1,y1,x2,y2;
|
|
163
|
+
if (!project(ca,&x0,&y0)||!project(cb,&x1,&y1)||!project(cc,&x2,&y2)) return;
|
|
164
|
+
if ((x1-x0)*(y2-y0)-(x2-x0)*(y1-y0) <= 0) return; /* back-face cull */
|
|
165
|
+
n64_tri2d(x0,y0,x1,y1,x2,y2,col);
|
|
166
|
+
}
|
|
167
|
+
void n64_quad3d(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col)
|
|
168
|
+
{ n64_tri3d(a,b,c,col); n64_tri3d(a,c,d,col); }
|
|
169
|
+
void n64_tri3d_nc(Vec3 a, Vec3 b, Vec3 c, unsigned short col)
|
|
170
|
+
{
|
|
171
|
+
Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c);
|
|
172
|
+
int x0,y0,x1,y1,x2,y2;
|
|
173
|
+
if (!project(ca,&x0,&y0)||!project(cb,&x1,&y1)||!project(cc,&x2,&y2)) return;
|
|
174
|
+
n64_tri2d(x0,y0,x1,y1,x2,y2,col);
|
|
175
|
+
}
|
|
176
|
+
void n64_quad3d_nc(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col)
|
|
177
|
+
{ n64_tri3d_nc(a,b,c,col); n64_tri3d_nc(a,c,d,col); }
|
|
178
|
+
|
|
179
|
+
/* ── RNG ── */
|
|
180
|
+
static unsigned int rng = 0x12345678u;
|
|
181
|
+
void n64_srand(unsigned int s) { rng = s ? s : 1; }
|
|
182
|
+
unsigned int n64_rand(void) { unsigned int x=rng; x^=x<<13; x^=x>>17; x^=x<<5; return rng=x; }
|
|
183
|
+
|
|
184
|
+
/* ── HUD number (3x5 cells scaled) ── */
|
|
185
|
+
static const unsigned char DIG[10][5] = {
|
|
186
|
+
{0x7,0x5,0x5,0x5,0x7},{0x2,0x6,0x2,0x2,0x7},{0x7,0x1,0x7,0x4,0x7},{0x7,0x1,0x3,0x1,0x7},
|
|
187
|
+
{0x5,0x5,0x7,0x1,0x1},{0x7,0x4,0x7,0x1,0x7},{0x7,0x4,0x7,0x5,0x7},{0x7,0x1,0x1,0x1,0x1},
|
|
188
|
+
{0x7,0x5,0x7,0x5,0x7},{0x7,0x5,0x7,0x1,0x7} };
|
|
189
|
+
static void digit(int x,int y,int d,unsigned short col)
|
|
190
|
+
{ int r,c; for(r=0;r<5;r++)for(c=0;c<3;c++) if(DIG[d][r]&(1<<(2-c))) n64_rect(x+c*3,y+r*3,3,3,col); }
|
|
191
|
+
void n64_number(int x, int y, unsigned int value, unsigned short col)
|
|
192
|
+
{
|
|
193
|
+
char buf[10]; int n=0,i;
|
|
194
|
+
if(!value)buf[n++]=0; while(value&&n<10){buf[n++]=value%10;value/=10;}
|
|
195
|
+
for(i=0;i<n;i++) digit(x+(n-1-i)*12,y,buf[i],col);
|
|
196
|
+
}
|