romdevtools 0.56.1 → 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 +279 -0
- package/README.md +9 -7
- package/examples/dreamcast/hello/main.c +24 -0
- package/package.json +5 -3
- package/src/analysis/analyze.js +60 -5
- package/src/analysis/decompile.js +3 -0
- package/src/analysis/rizin.js +3 -1
- package/src/cores/capabilities.js +40 -8
- package/src/cores/registry.js +13 -8
- package/src/host/LibretroGL.js +26 -23
- package/src/host/LibretroHost.js +60 -1
- package/src/host/callbacks.js +4 -0
- package/src/host/coreLoader.js +17 -4
- package/src/mcp/tools/project.js +16 -0
- package/src/toolchains/index.js +30 -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
package/src/host/LibretroGL.js
CHANGED
|
@@ -174,13 +174,20 @@ export class LibretroGL {
|
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
176
|
* Read back the rendered frame as RGBA pixels.
|
|
177
|
+
* @param {number} [cropW] active width the core reported (e.g. DC 640); when smaller
|
|
178
|
+
* than the FBO, read back only that bottom-left region instead of the whole viewport
|
|
179
|
+
* (the GL bottom-left origin puts the active framebuffer in the lower-left corner).
|
|
180
|
+
* @param {number} [cropH] active height the core reported (e.g. DC 480).
|
|
177
181
|
* Returns { pixels, width, height } or null if not active.
|
|
178
182
|
*/
|
|
179
|
-
readbackFrame() {
|
|
183
|
+
readbackFrame(cropW, cropH) {
|
|
180
184
|
if (!this.active) return null;
|
|
181
185
|
|
|
182
186
|
gl.glFinish();
|
|
183
|
-
|
|
187
|
+
// Crop to the core's active resolution when it's a sub-region of the FBO; this
|
|
188
|
+
// strips the dead border a fixed-size GL FBO leaves around a smaller native frame.
|
|
189
|
+
const w = (cropW > 0 && cropW <= this.fboW) ? cropW : this.fboW;
|
|
190
|
+
const h = (cropH > 0 && cropH <= this.fboH) ? cropH : this.fboH;
|
|
184
191
|
const GL_RGBA = 0x1908, GL_UNSIGNED_BYTE = 0x1401;
|
|
185
192
|
|
|
186
193
|
if (!this._glPixels || this._glPixels.length !== w * h * 4) {
|
|
@@ -188,6 +195,7 @@ export class LibretroGL {
|
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
gl.glBindFramebuffer(0x8D40, 0); // GL_FRAMEBUFFER — read from default FBO
|
|
198
|
+
// x=0,y=0 is the bottom-left of the FBO, which is where the native frame is drawn.
|
|
191
199
|
gl.glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, this._glPixels);
|
|
192
200
|
|
|
193
201
|
|
|
@@ -225,32 +233,27 @@ export class LibretroGL {
|
|
|
225
233
|
return this._procAddressCache.get(name);
|
|
226
234
|
}
|
|
227
235
|
|
|
228
|
-
//
|
|
236
|
+
// PREFERRED: cores built with emscripten's own WebGL layer (GL_ENABLE_GET_PROC_
|
|
237
|
+
// ADDRESS=1, e.g. Flycast) resolve GL through emscripten_GetProcAddress, which
|
|
238
|
+
// returns a REAL function-table pointer into the WebGL JS implementation —
|
|
239
|
+
// correct signature, no stub. Use it when the core exports it. (The old
|
|
240
|
+
// native-gles `gl[name]` + addFunction(...,'i') stub path traps the moment the
|
|
241
|
+
// core calls a multi-arg GL fn through a 0-arg pointer — that's the
|
|
242
|
+
// context_reset "null function or function signature mismatch".)
|
|
243
|
+
if (typeof mod._emscripten_GetProcAddress === "function") {
|
|
244
|
+
const ptr = mod._emscripten_GetProcAddress(symPtr) >>> 0;
|
|
245
|
+
this._procAddressCache.set(name, ptr);
|
|
246
|
+
return ptr;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fallback (native-gles bridge cores, e.g. the glide64 N64 build): the GL fns
|
|
250
|
+
// are linked directly into the core, so a non-zero "available" marker suffices —
|
|
251
|
+
// the core calls its own linked function, not this pointer.
|
|
229
252
|
const fn = gl[name];
|
|
230
253
|
if (!fn) {
|
|
231
|
-
// Not found — some cores probe for extension functions
|
|
232
254
|
this._procAddressCache.set(name, 0);
|
|
233
255
|
return 0;
|
|
234
256
|
}
|
|
235
|
-
|
|
236
|
-
// We need to create an Emscripten addFunction wrapper that bridges
|
|
237
|
-
// the WASM call to the native GL call. The challenge is that each
|
|
238
|
-
// GL function has a different signature.
|
|
239
|
-
//
|
|
240
|
-
// For libretro cores compiled with Emscripten, they typically call
|
|
241
|
-
// GL functions directly (linked at compile time) rather than through
|
|
242
|
-
// get_proc_address. get_proc_address is mainly used for extension
|
|
243
|
-
// functions. We return 0 for now if they're probing — the core's
|
|
244
|
-
// Emscripten build should have the GL functions linked directly.
|
|
245
|
-
//
|
|
246
|
-
// If a core truly needs runtime-resolved GL, we'd need a signature
|
|
247
|
-
// table mapping GL function names to their parameter types.
|
|
248
|
-
|
|
249
|
-
// For common GL functions, return a stub that indicates "available"
|
|
250
|
-
// The actual calls go through the Emscripten-linked GL functions.
|
|
251
|
-
// Returning non-zero tells the core the function exists.
|
|
252
|
-
// We create a minimal no-op wrapper since the core will call the
|
|
253
|
-
// directly-linked version, not this pointer.
|
|
254
257
|
const stub = mod.addFunction(() => { return 0; }, 'i');
|
|
255
258
|
this._procAddressCache.set(name, stub);
|
|
256
259
|
return stub;
|
package/src/host/LibretroHost.js
CHANGED
|
@@ -36,6 +36,17 @@ import {
|
|
|
36
36
|
* to function headlessly (no menu = no user pick). Empty today — all
|
|
37
37
|
* shipped cores load with their defaults.
|
|
38
38
|
*/
|
|
39
|
+
// Core filename stem → platform, for the cores whose renderer (or other boot-time
|
|
40
|
+
// behavior) is chosen from an option at retro_init. loadCore uses this to pre-seed
|
|
41
|
+
// PLATFORM_CORE_OPTIONS before the core registers its variables, so the override
|
|
42
|
+
// wins over the core's default (e.g. glide64-GL over angrylion-software for N64).
|
|
43
|
+
const CORE_STEM_TO_PLATFORM = {
|
|
44
|
+
parallel_n64: "n64",
|
|
45
|
+
pcsx_rearmed: "ps1",
|
|
46
|
+
flycast: "dreamcast",
|
|
47
|
+
beetle_psx_hw: "ps1",
|
|
48
|
+
};
|
|
49
|
+
|
|
39
50
|
const PLATFORM_CORE_OPTIONS = {
|
|
40
51
|
// blueMSX defaults its machine to "SEGA - SC-3000" (an SG-1000 clone, wrong for
|
|
41
52
|
// MSX carts). Force the open MSX2+ C-BIOS machine — a superset that also runs
|
|
@@ -55,6 +66,24 @@ const PLATFORM_CORE_OPTIONS = {
|
|
|
55
66
|
// beetle_psx_hw defaults to the software renderer; force hardware_gl so it
|
|
56
67
|
// renders through the GL context the host set up (otherwise no FBO frames).
|
|
57
68
|
ps1: { beetle_psx_hw_renderer: "hardware_gl" },
|
|
69
|
+
// Flycast defaults to THREADED rendering — the render thread + the main thread's
|
|
70
|
+
// wait on it makes retro_run unwind under emscripten pthreads (yields to the event
|
|
71
|
+
// loop, incompatible with our synchronous frame-step). Disable it so render() runs
|
|
72
|
+
// synchronously on the calling thread (Emulator::render → run(), no cross-thread
|
|
73
|
+
// wait). Also keep per-frame sync (no auto frame-skip) so one retro_run = one frame.
|
|
74
|
+
// hle_bios (reios) MUST be on: it's flycast's HLE BIOS, and only the reios boot
|
|
75
|
+
// path loads a raw homebrew .elf (reios_loadElf copies PT_LOAD segments to their
|
|
76
|
+
// vaddr + jumps). With it off (the default), flycast wants a real dc_boot.bin we
|
|
77
|
+
// don't ship → the .elf never loads (RAM stays empty, CPU never runs our code).
|
|
78
|
+
// emulate_framebuffer scans out the DC framebuffer directly on every VBlank (the 2D
|
|
79
|
+
// path, no TA list) — so simple homebrew that writes RGB565 pixels to VRAM presents
|
|
80
|
+
// reliably without building a full PowerVR2 tile list. (Threaded rendering is also
|
|
81
|
+
// force-disabled in the core itself; the option here is belt-and-suspenders.)
|
|
82
|
+
dreamcast: {
|
|
83
|
+
flycast_threaded_rendering: "disabled",
|
|
84
|
+
flycast_hle_bios: "enabled",
|
|
85
|
+
flycast_emulate_framebuffer: "enabled",
|
|
86
|
+
},
|
|
58
87
|
// VICE mounts a .d64/.tap/.crt but, with autostart off, just sits at the BASIC
|
|
59
88
|
// `READY.` prompt — the agent would see a blue boot screen, not the game. Force
|
|
60
89
|
// autostart so a disk/tape image runs the first program automatically (same as
|
|
@@ -252,6 +281,16 @@ export class LibretroHost {
|
|
|
252
281
|
* optional native GL stack and creates a headless context BEFORE the core boots
|
|
253
282
|
* (GL calls happen during init/load). The 14 software cores omit this.
|
|
254
283
|
*/
|
|
284
|
+
/** Infer the platform from a core's js filename stem (e.g.
|
|
285
|
+
* ".../parallel_n64_libretro.js" → "n64"), so loadCore can pre-seed that
|
|
286
|
+
* platform's option overrides without the caller threading the platform
|
|
287
|
+
* through. Covers the cores whose boot-time renderer choice depends on an
|
|
288
|
+
* option; returns null otherwise (the overrides are then a no-op anyway). */
|
|
289
|
+
_platformForCore(jsPath) {
|
|
290
|
+
const stem = (jsPath.split("/").pop() || "").replace(/_libretro\.js$/, "");
|
|
291
|
+
return CORE_STEM_TO_PLATFORM[stem] ?? null;
|
|
292
|
+
}
|
|
293
|
+
|
|
255
294
|
async loadCore(jsPath, wasmPath, opts = {}) {
|
|
256
295
|
if (this.mod) throw new Error("core already loaded; create a new host");
|
|
257
296
|
|
|
@@ -287,6 +326,24 @@ export class LibretroHost {
|
|
|
287
326
|
}
|
|
288
327
|
|
|
289
328
|
registerCallbacks({ mod, state: this.state, log: this.log });
|
|
329
|
+
|
|
330
|
+
// Pre-seed per-platform core-option overrides BEFORE _retro_init. The core
|
|
331
|
+
// registers its variables (SET_VARIABLES) during retro_init and decides its
|
|
332
|
+
// renderer THEN — e.g. parallel_n64 reads `parallel-n64-gfxplugin` to pick
|
|
333
|
+
// glide64 (GL/HW-render) vs angrylion (software). Seeding the override here
|
|
334
|
+
// (not in loadMedia, which is too late) makes the SET_VARIABLES handler keep
|
|
335
|
+
// our value instead of the core's default, so HW render engages from boot.
|
|
336
|
+
const platform = opts.platform ?? this._platformForCore(jsPath);
|
|
337
|
+
if (platform) {
|
|
338
|
+
const overrides = PLATFORM_CORE_OPTIONS[platform];
|
|
339
|
+
if (overrides) {
|
|
340
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
341
|
+
this.state.coreVariables.set(key, { value });
|
|
342
|
+
}
|
|
343
|
+
this.state.variablesUpdated = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
290
347
|
mod._retro_init();
|
|
291
348
|
this.status.corePath = jsPath;
|
|
292
349
|
}
|
|
@@ -588,7 +645,9 @@ export class LibretroHost {
|
|
|
588
645
|
_afterRun() {
|
|
589
646
|
if (this.state.hwFramePending && this.hwRender?.active) {
|
|
590
647
|
this.state.hwFramePending = false;
|
|
591
|
-
|
|
648
|
+
// Crop the GL FBO to the core's reported active resolution (e.g. DC 640x480 in an
|
|
649
|
+
// 853x853 FBO) so screenshots don't carry the dead viewport border.
|
|
650
|
+
const frame = this.hwRender.readbackFrame(this.state.hwFrameW, this.state.hwFrameH);
|
|
592
651
|
if (frame) {
|
|
593
652
|
this.state.lastFrame = {
|
|
594
653
|
width: frame.width, height: frame.height,
|
package/src/host/callbacks.js
CHANGED
|
@@ -172,6 +172,10 @@ export function registerCallbacks(args) {
|
|
|
172
172
|
const uPtr = dataPtr >>> 0;
|
|
173
173
|
if (uPtr === RETRO_HW_FRAME_BUFFER_VALID && state.hwRender?.active) {
|
|
174
174
|
state.hwFramePending = true;
|
|
175
|
+
// Remember the core's reported active resolution (e.g. DC 640x480) so the
|
|
176
|
+
// readback can crop the (often larger, e.g. 853x853) GL FBO to just the
|
|
177
|
+
// rendered region instead of returning the whole viewport with dead borders.
|
|
178
|
+
if (w > 0 && h > 0) { state.hwFrameW = w; state.hwFrameH = h; }
|
|
175
179
|
return;
|
|
176
180
|
}
|
|
177
181
|
if (dataPtr === 0 || uPtr === RETRO_HW_FRAME_BUFFER_VALID) return;
|
package/src/host/coreLoader.js
CHANGED
|
@@ -37,10 +37,15 @@ export async function loadLibretroCore(args) {
|
|
|
37
37
|
|
|
38
38
|
/** @type {Record<string, unknown>} */
|
|
39
39
|
const opts = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
printErr: () => {},
|
|
40
|
+
print: process.env.ROMDEV_CORE_LOG ? (s) => console.error("[core]", s) : () => {},
|
|
41
|
+
printErr: process.env.ROMDEV_CORE_LOG ? (s) => console.error("[core:err]", s) : () => {},
|
|
43
42
|
};
|
|
43
|
+
// The cores are linked with INVOKE_RUN=0, so main() never auto-runs regardless.
|
|
44
|
+
// Do NOT set noInitialRun on GL (canvas) cores: it suppresses the Emscripten GL
|
|
45
|
+
// runtime init, so `Module.GL` (and thus the WebGL2/native-gles context the core's
|
|
46
|
+
// SET_HW_RENDER path needs) is never created. For non-GL cores it's harmless either
|
|
47
|
+
// way; we set it only there to keep their startup identical to before.
|
|
48
|
+
if (!glCanvas) opts.noInitialRun = true;
|
|
44
49
|
|
|
45
50
|
// HW-render cores: minified glue uses GLctx = canvas.getContext('webgl2'), so we
|
|
46
51
|
// hand it a webgl-node mock canvas + install the WebGL2 globals Emscripten probes.
|
|
@@ -84,8 +89,16 @@ export async function loadLibretroCore(args) {
|
|
|
84
89
|
});
|
|
85
90
|
return {};
|
|
86
91
|
};
|
|
87
|
-
} else if (wasmPath) {
|
|
92
|
+
} else if (wasmPath && !glCanvas) {
|
|
88
93
|
opts.wasmBinary = await readFile(wasmPath);
|
|
94
|
+
} else if (wasmPath && glCanvas) {
|
|
95
|
+
// GL (canvas) cores: do NOT hand Emscripten a pre-read wasmBinary. Passing it
|
|
96
|
+
// routes instantiation through a path that skips the GL runtime init, so
|
|
97
|
+
// `Module.GL` (the WebGL2/native-gles context the SET_HW_RENDER path needs) is
|
|
98
|
+
// never created. Let Emscripten locate `<name>.wasm` next to the `.js` itself
|
|
99
|
+
// (via locateFile) — the same way retroemu loads parallel_n64/flycast.
|
|
100
|
+
const wasmDir = wasmPath.slice(0, wasmPath.lastIndexOf("/") + 1);
|
|
101
|
+
opts.locateFile = (file) => wasmDir + file;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
const mod = await factory(opts);
|
package/src/mcp/tools/project.js
CHANGED
|
@@ -646,6 +646,22 @@ const TEMPLATES = {
|
|
|
646
646
|
},
|
|
647
647
|
};
|
|
648
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
|
+
},
|
|
649
665
|
};
|
|
650
666
|
// R37: GBC has its own scaffold tree at examples/gbc/templates/ +
|
|
651
667
|
// src/platforms/gbc/lib/c/. Same runtime files as GB (the APU + Z80 +
|
package/src/toolchains/index.js
CHANGED
|
@@ -91,6 +91,9 @@ const LANGUAGE_TOOLCHAIN = {
|
|
|
91
91
|
n64: {
|
|
92
92
|
c: { toolchain: "mips-elf-gcc", available: true, note: "C for N64 via gcc 14.2.0 + binutils + newlib (mips-elf, big-endian R4300), compiled to WASM. Bare path: minimal crt0 (stack + .bss + main()) → flat code image at 0x80000400. NOTE: a fully bootable N64 ROM needs the IPL3 bootcode + a libdragon-style header (libdragon SDK forthcoming) — this path exercises the toolchain + is the basis for the SDK." },
|
|
93
93
|
},
|
|
94
|
+
dreamcast: {
|
|
95
|
+
c: { toolchain: "sh-elf-gcc", available: true, note: "C for Dreamcast via gcc 14.2.0 + binutils + newlib (sh-elf, little-endian SH-4, m4-single-only FP), compiled to WASM. Bare path: a minimal crt0 sets the stack + clears .bss + calls main(); the output is an ELF that Flycast's reios HLE BIOS boots DIRECTLY (no GD-ROM/CDI image, no firmware). No KallistiOS yet — bring up the PowerVR2 framebuffer yourself: program FB_R_CTRL/FB_R_SIZE/FB_R_SOF1 + SPG for 640x480 RGB565 at VRAM 0xA5000000, then write pixels (see the dc.h helper). With flycast_emulate_framebuffer on, a plain pixel-writing program presents — no TA list needed." },
|
|
96
|
+
},
|
|
94
97
|
};
|
|
95
98
|
|
|
96
99
|
/**
|
|
@@ -123,6 +126,7 @@ const PLATFORM_DEFAULT_LANGUAGE = {
|
|
|
123
126
|
msx: "c",
|
|
124
127
|
ps1: "c",
|
|
125
128
|
n64: "c",
|
|
129
|
+
dreamcast: "c",
|
|
126
130
|
};
|
|
127
131
|
|
|
128
132
|
/**
|
|
@@ -502,6 +506,32 @@ export async function buildForPlatform(args) {
|
|
|
502
506
|
};
|
|
503
507
|
}
|
|
504
508
|
|
|
509
|
+
if (args.platform === "dreamcast") {
|
|
510
|
+
// Dreamcast SH-4 C: the bare gcc+newlib+libgcc path through the sh-elf-gcc WASM
|
|
511
|
+
// toolchain (cc1→as→ld). The output is an ELF that Flycast's reios HLE BIOS boots
|
|
512
|
+
// directly (no GD-ROM/CDI image). Bring up the PowerVR2 framebuffer yourself (see
|
|
513
|
+
// the dc.h helper); language defaults to "c".
|
|
514
|
+
const { buildShC } = await import("./sh-c/sh-c.js");
|
|
515
|
+
const r = await buildShC({
|
|
516
|
+
source: args.source,
|
|
517
|
+
sources: args.sources,
|
|
518
|
+
headers: args.includes,
|
|
519
|
+
cc1Options: args.options,
|
|
520
|
+
});
|
|
521
|
+
return {
|
|
522
|
+
ok: r.ok,
|
|
523
|
+
binary: r.binary,
|
|
524
|
+
listing: "",
|
|
525
|
+
symbols: r.symbols ?? "",
|
|
526
|
+
log: r.log,
|
|
527
|
+
issues: parseBuildLog(r.log),
|
|
528
|
+
exitCode: r.exitCode,
|
|
529
|
+
toolchain: "sh-elf-gcc",
|
|
530
|
+
stage: r.stage,
|
|
531
|
+
...(r.crash ? { crash: r.crash } : {}),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
505
535
|
if (args.platform === "gba") {
|
|
506
536
|
// R24 + R28: language:"c" routes through the arm-none-eabi gcc +
|
|
507
537
|
// binutils WASM toolchain (cc1-arm → as → ld → objcopy). Three
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.section .text.start, "ax"
|
|
2
|
+
.global _start
|
|
3
|
+
_start:
|
|
4
|
+
mov.l stack_top, r15 ! stack = top of 16MB DC RAM
|
|
5
|
+
mov.l bss_start, r0 ! clear .bss
|
|
6
|
+
mov.l bss_end, r1
|
|
7
|
+
mov #0, r2
|
|
8
|
+
1: cmp/hs r1, r0
|
|
9
|
+
bt 2f
|
|
10
|
+
mov.l r2, @r0
|
|
11
|
+
add #4, r0
|
|
12
|
+
bra 1b
|
|
13
|
+
nop
|
|
14
|
+
2: mov.l main_addr, r0 ! call main()
|
|
15
|
+
jsr @r0
|
|
16
|
+
nop
|
|
17
|
+
hang: bra hang
|
|
18
|
+
nop
|
|
19
|
+
.align 4
|
|
20
|
+
stack_top: .long 0x8d000000
|
|
21
|
+
bss_start: .long __bss_start
|
|
22
|
+
bss_end: .long _end
|
|
23
|
+
main_addr: .long _main
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* dc.h — minimal Dreamcast helper for romdev homebrew.
|
|
2
|
+
*
|
|
3
|
+
* Brings up the PowerVR2 video output for a plain 640x480 RGB565 framebuffer and
|
|
4
|
+
* exposes pixel/fill/rect primitives. No KallistiOS dependency — just the registers
|
|
5
|
+
* the Flycast (reios HLE) core needs to scan out a direct framebuffer.
|
|
6
|
+
*
|
|
7
|
+
* Memory map:
|
|
8
|
+
* PVR/HOLLY registers : 0xA05F8000 (uncached)
|
|
9
|
+
* VRAM (64-bit area) : 0xA5000000 (16 MB) — the framebuffer lives here
|
|
10
|
+
*
|
|
11
|
+
* The DC has two VRAM views; the 64-bit area (0xA5000000) is the linear one we draw to.
|
|
12
|
+
*/
|
|
13
|
+
#ifndef ROMDEV_DC_H
|
|
14
|
+
#define ROMDEV_DC_H
|
|
15
|
+
|
|
16
|
+
typedef unsigned char u8;
|
|
17
|
+
typedef unsigned short u16;
|
|
18
|
+
typedef unsigned int u32;
|
|
19
|
+
|
|
20
|
+
#define DC_W 640
|
|
21
|
+
#define DC_H 480
|
|
22
|
+
|
|
23
|
+
/* PVR register access */
|
|
24
|
+
#define PVR_BASE 0xA05F8000u
|
|
25
|
+
#define PVR_REG(off) (*(volatile u32 *)(PVR_BASE + (off)))
|
|
26
|
+
|
|
27
|
+
/* The framebuffer we draw to (offset 0 in the 64-bit VRAM area). */
|
|
28
|
+
#define DC_FB_OFFSET 0x000000u
|
|
29
|
+
#define DC_VRAM ((volatile u16 *)(0xA5000000u + DC_FB_OFFSET))
|
|
30
|
+
|
|
31
|
+
/* RGB565 helper */
|
|
32
|
+
static inline u16 dc_rgb(u8 r, u8 g, u8 b) {
|
|
33
|
+
return (u16)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Bring up 640x480 RGB565 video. Call once at start. The register values are the
|
|
37
|
+
* documented PowerVR2 settings for a progressive NTSC 640x480 RGB565 framebuffer;
|
|
38
|
+
* Flycast's reios + EmulateFramebuffer scanout reads FB_R_CTRL/FB_R_SIZE/FB_R_SOF1. */
|
|
39
|
+
static inline void dc_video_init(void) {
|
|
40
|
+
/* FB_R_CTRL: fb_enable=1, fb_depth=1 (RGB565), fb_concat=4 (fill low bits). */
|
|
41
|
+
PVR_REG(0x044) = 0x00000001u | (1u << 2) | (4u << 4); /* = 0x45 */
|
|
42
|
+
/* FB_W_CTRL: fb_packmode=1 (RGB565). */
|
|
43
|
+
PVR_REG(0x048) = 0x00000001u;
|
|
44
|
+
/* FB_R_SOF1 / FB_W_SOF1: framebuffer start = DC_FB_OFFSET. */
|
|
45
|
+
PVR_REG(0x050) = DC_FB_OFFSET; /* FB_R_SOF1 */
|
|
46
|
+
PVR_REG(0x060) = DC_FB_OFFSET; /* FB_W_SOF1 */
|
|
47
|
+
/* FB_R_SIZE: fb_x_size = (line bytes / 4) - 1, fb_y_size = lines - 1, modulus = 1.
|
|
48
|
+
* 640px * 2 bytes = 1280 bytes/line -> 1280/4 - 1 = 319. */
|
|
49
|
+
PVR_REG(0x05C) = (u32)(DC_W * 2 / 4 - 1) /* fb_x_size = 319 */
|
|
50
|
+
| ((u32)(DC_H - 1) << 10) /* fb_y_size = 479 */
|
|
51
|
+
| ((u32)1u << 20); /* fb_modulus = 1 */
|
|
52
|
+
/* FB_W_LINESTRIDE: line stride in 64-bit (8-byte) units = 1280/8 = 160. */
|
|
53
|
+
PVR_REG(0x11C) = (u32)(DC_W * 2 / 8);
|
|
54
|
+
/* VO_CONTROL: pixel double off, normal output. */
|
|
55
|
+
PVR_REG(0x0E8) = 0x00000000u;
|
|
56
|
+
/* VO_BORDER_COL: black border. */
|
|
57
|
+
PVR_REG(0x040) = 0x00000000u;
|
|
58
|
+
/* SPG_LOAD / SPG_CONTROL: NTSC 640x480 progressive timing. */
|
|
59
|
+
PVR_REG(0x0D8) = (524u << 16) | 857u; /* SPG_LOAD: vcount, hcount */
|
|
60
|
+
PVR_REG(0x0D0) = 0x00000000u; /* SPG_CONTROL: NTSC, non-interlace */
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Plot a pixel (no bounds clamp on the hot path; caller keeps in range). */
|
|
64
|
+
static inline void dc_plot(int x, int y, u16 c) {
|
|
65
|
+
DC_VRAM[y * DC_W + x] = c;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Fill the whole framebuffer with one color. */
|
|
69
|
+
static inline void dc_clear(u16 c) {
|
|
70
|
+
int i;
|
|
71
|
+
for (i = 0; i < DC_W * DC_H; i++)
|
|
72
|
+
DC_VRAM[i] = c;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Fill an axis-aligned rectangle (clipped to the screen). */
|
|
76
|
+
static inline void dc_rect(int x0, int y0, int w, int h, u16 c) {
|
|
77
|
+
int x, y;
|
|
78
|
+
if (x0 < 0) { w += x0; x0 = 0; }
|
|
79
|
+
if (y0 < 0) { h += y0; y0 = 0; }
|
|
80
|
+
if (x0 + w > DC_W) w = DC_W - x0;
|
|
81
|
+
if (y0 + h > DC_H) h = DC_H - y0;
|
|
82
|
+
for (y = 0; y < h; y++)
|
|
83
|
+
for (x = 0; x < w; x++)
|
|
84
|
+
DC_VRAM[(y0 + y) * DC_W + (x0 + x)] = c;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ── Controller input (Maple bus, port 0) ─────────────────────────────────────
|
|
88
|
+
* The DC Maple controller digital-button bit layout. Full Maple DMA setup is
|
|
89
|
+
* non-trivial; the romdev host injects input (setInput) that the core maps onto
|
|
90
|
+
* Maple, so these constants document the bit layout for game logic. */
|
|
91
|
+
#define DC_BTN_C 0x0001
|
|
92
|
+
#define DC_BTN_B 0x0002
|
|
93
|
+
#define DC_BTN_A 0x0004
|
|
94
|
+
#define DC_BTN_START 0x0008
|
|
95
|
+
#define DC_BTN_UP 0x0010
|
|
96
|
+
#define DC_BTN_DOWN 0x0020
|
|
97
|
+
#define DC_BTN_LEFT 0x0040
|
|
98
|
+
#define DC_BTN_RIGHT 0x0080
|
|
99
|
+
#define DC_BTN_Y 0x0200
|
|
100
|
+
#define DC_BTN_X 0x0400
|
|
101
|
+
|
|
102
|
+
#endif /* ROMDEV_DC_H */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ENTRY(_start)
|
|
2
|
+
MEMORY { ram (rwx) : ORIGIN = 0x8c010000, LENGTH = 0x800000 }
|
|
3
|
+
SECTIONS {
|
|
4
|
+
.text : { *(.text.start) *(.text*) *(.rodata*) } > ram
|
|
5
|
+
.data : { *(.data*) } > ram
|
|
6
|
+
. = ALIGN(4);
|
|
7
|
+
__bss_start = .;
|
|
8
|
+
.bss : { *(.bss*) *(COMMON) } > ram
|
|
9
|
+
. = ALIGN(4);
|
|
10
|
+
_end = .;
|
|
11
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// sh-c — Dreamcast (SH-4) C build driver.
|
|
2
|
+
//
|
|
3
|
+
// buildShC({ source | sources, headers }) → { ok, binary (ELF), log, symbols }
|
|
4
|
+
//
|
|
5
|
+
// gcc-the-driver can't fork/exec under emscripten, so we orchestrate the stages
|
|
6
|
+
// directly (cc1 → as → ld → objcopy-not-needed). The output is an ELF that Flycast's
|
|
7
|
+
// reios HLE BIOS boots directly (no GD-ROM/CDI image, no scrambling): reios_loadElf
|
|
8
|
+
// copies the PT_LOAD segments to their vaddr (0x8c010000) and jumps to _start.
|
|
9
|
+
//
|
|
10
|
+
// Bare path: a small crt0 sets the stack (top of 16 MB DC RAM), zeroes .bss, and
|
|
11
|
+
// calls main(). newlib libc/libm + libgcc are linked (SH-4, little-endian). The
|
|
12
|
+
// romdev DC helper (rom-games/dreamcast/.../dc.h) brings up the PowerVR2 framebuffer.
|
|
13
|
+
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
import { runCc1sh, runShAs, runShLd } from "../sh-elf-gcc/gcc.js";
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = path.dirname(__filename);
|
|
22
|
+
const LIB = path.join(__dirname, "lib");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compile + link Dreamcast SH-4 C to a bootable ELF.
|
|
26
|
+
* @param {object} args
|
|
27
|
+
* @param {string} [args.source] single C source (shorthand for sources["main.c"])
|
|
28
|
+
* @param {Record<string,string>} [args.sources] name → source (.c and .s)
|
|
29
|
+
* @param {Record<string,string>} [args.headers] name → header text
|
|
30
|
+
* @param {string[]} [args.cc1Options] extra cc1 flags
|
|
31
|
+
*/
|
|
32
|
+
export async function buildShC(args) {
|
|
33
|
+
// The bundled DC helper (dc.h) is auto-available so `#include "dc.h"` just works —
|
|
34
|
+
// it brings up the PowerVR2 framebuffer (FB_R_CTRL/SIZE/SOF1 + SPG for 640x480 RGB565)
|
|
35
|
+
// that Flycast presents. A caller-supplied "dc.h" wins (override the bundled one).
|
|
36
|
+
const bundledDcH = await readFile(path.join(LIB, "dc.h"), "utf-8").catch(() => null);
|
|
37
|
+
const headers = { ...(bundledDcH != null ? { "dc.h": bundledDcH } : {}), ...(args.headers ?? {}) };
|
|
38
|
+
// -m4-single-only is passed by the cc1 wrapper; here the language/codegen knobs.
|
|
39
|
+
// Default to -O2, but let a user-supplied -O<level> win (gcc honors the LAST -O, so a
|
|
40
|
+
// default appended AFTER the user's would clobber it — only add -O2 if absent).
|
|
41
|
+
const userOpts = args.cc1Options ?? [];
|
|
42
|
+
const hasOpt = userOpts.some((o) => /^-O/.test(o));
|
|
43
|
+
const cc1Options = [...(hasOpt ? [] : ["-O2"]), ...userOpts, "-ffreestanding", "-fno-builtin", "-Wall"];
|
|
44
|
+
const sources = args.sources ?? (args.source != null ? { "main.c": args.source } : {});
|
|
45
|
+
let log = "";
|
|
46
|
+
|
|
47
|
+
/** @type {Record<string, Uint8Array>} */
|
|
48
|
+
const userObjs = {};
|
|
49
|
+
for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
|
|
50
|
+
const cc = await runCc1sh({ source: sources[cName], headers, options: cc1Options });
|
|
51
|
+
log += `--- cc1 (${cName}) ---\n${cc.log || "(ok)"}\n`;
|
|
52
|
+
if (cc.exitCode !== 0 || !cc.asmSource)
|
|
53
|
+
return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${cName})`, ...(cc.crash ? { crash: cc.crash } : {}) };
|
|
54
|
+
const as = await runShAs({ source: cc.asmSource });
|
|
55
|
+
log += `--- as (${cName}) ---\n${as.log || "(ok)"}\n`;
|
|
56
|
+
if (as.exitCode !== 0 || !as.object)
|
|
57
|
+
return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, ...(as.crash ? { crash: as.crash } : {}) };
|
|
58
|
+
userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
|
|
59
|
+
}
|
|
60
|
+
// raw .s sources too
|
|
61
|
+
for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
|
|
62
|
+
const as = await runShAs({ source: sources[sName] });
|
|
63
|
+
log += `--- as (${sName}) ---\n${as.log || "(ok)"}\n`;
|
|
64
|
+
if (as.exitCode !== 0 || !as.object)
|
|
65
|
+
return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${sName})`, ...(as.crash ? { crash: as.crash } : {}) };
|
|
66
|
+
userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = as.object;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// crt0 (stack + .bss clear + main()).
|
|
70
|
+
const crt0Src = await readFile(path.join(LIB, "dc-crt0.s"), "utf-8");
|
|
71
|
+
const crt0As = await runShAs({ source: crt0Src });
|
|
72
|
+
log += `--- as (dc-crt0.s) ---\n${crt0As.log || "(ok)"}\n`;
|
|
73
|
+
if (crt0As.exitCode !== 0 || !crt0As.object)
|
|
74
|
+
return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (crt0)", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
|
|
75
|
+
|
|
76
|
+
// link: crt0 + user objects + newlib (libc/libm) + libgcc.
|
|
77
|
+
const linkScript = await readFile(path.join(LIB, "dc.ld"), "utf-8");
|
|
78
|
+
const [libc, libm, libgcc] = await Promise.all([
|
|
79
|
+
readFile(path.join(LIB, "libc.a")),
|
|
80
|
+
readFile(path.join(LIB, "libm.a")),
|
|
81
|
+
readFile(path.join(LIB, "libgcc.a")),
|
|
82
|
+
]);
|
|
83
|
+
const ld = await runShLd({
|
|
84
|
+
objects: { "crt0.o": crt0As.object, ...userObjs },
|
|
85
|
+
linkScript,
|
|
86
|
+
archives: {
|
|
87
|
+
"libc.a": new Uint8Array(libc),
|
|
88
|
+
"libm.a": new Uint8Array(libm),
|
|
89
|
+
"libgcc.a": new Uint8Array(libgcc),
|
|
90
|
+
},
|
|
91
|
+
libraries: ["c", "m", "gcc"],
|
|
92
|
+
libraryPaths: ["/work"],
|
|
93
|
+
options: ["--no-warn-rwx-segments"],
|
|
94
|
+
});
|
|
95
|
+
log += `--- ld ---\n${ld.log || "(ok)"}\n`;
|
|
96
|
+
if (ld.exitCode !== 0 || !ld.elf)
|
|
97
|
+
return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", ...(ld.crash ? { crash: ld.crash } : {}) };
|
|
98
|
+
|
|
99
|
+
// The ELF IS the deliverable — reios boots it directly.
|
|
100
|
+
return { ok: true, binary: ld.elf, log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
|
|
101
|
+
}
|