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.
@@ -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
- const w = this.fboW, h = this.fboH;
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
- // Look up the function on native-gles
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;
@@ -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
- const frame = this.hwRender.readbackFrame();
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,
@@ -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;
@@ -37,10 +37,15 @@ export async function loadLibretroCore(args) {
37
37
 
38
38
  /** @type {Record<string, unknown>} */
39
39
  const opts = {
40
- noInitialRun: true,
41
- print: () => {},
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);
@@ -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 +
@@ -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
+ }