romdevtools 0.41.0 → 0.43.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 +94 -0
- package/package.json +3 -1
- package/src/analysis/analyze.js +1 -1
- package/src/analysis/backtrace.js +198 -0
- package/src/analysis/nes-ppu-runtime.js +220 -0
- package/src/analysis/nes-ppu-shim.js +263 -0
- package/src/analysis/pointer-table.js +84 -0
- package/src/analysis/recompile/emit-65816.js +105 -0
- package/src/analysis/recompile/emit-m68k.js +295 -0
- package/src/analysis/recompile/index.js +169 -0
- package/src/analysis/recompile/ir.js +123 -0
- package/src/analysis/recompile/lift-6502.js +176 -0
- package/src/analysis/recompile-65816.js +533 -0
- package/src/cheats/gamegenie.js +14 -2
- package/src/cores/registry.js +43 -51
- package/src/mcp/state.js +91 -2
- package/src/mcp/tools/cheats.js +10 -1
- package/src/mcp/tools/disasm.js +329 -21
- package/src/mcp/tools/find-references.js +93 -2
- package/src/mcp/tools/frame.js +440 -4
- package/src/mcp/tools/index.js +34 -1
- package/src/mcp/tools/lifecycle.js +53 -24
- package/src/mcp/tools/memory.js +1 -1
- package/src/mcp/tools/playtest.js +24 -1
- package/src/mcp/tools/project.js +1 -1
- package/src/mcp/tools/rendering-context.js +4 -2
- package/src/mcp/tools/watch-memory.js +87 -9
- package/src/mcp/util.js +45 -0
- package/src/playtest/playtest.js +115 -46
- package/src/toolchains/cc65/da65.js +7 -0
- package/src/cores/wasm/bluemsx_libretro.js +0 -2
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +0 -2
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +0 -2
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +0 -2
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +0 -2
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +0 -2
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +0 -2
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +0 -2
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +0 -2
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +0 -2
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +0 -2
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// NES-PPU-on-SNES runtime shim — the piece that fills the STUBBED seam left by
|
|
2
|
+
// the NES→SNES recompile backend (recompile-65816.js). Without it, a recompiled
|
|
3
|
+
// port boots and runs the 6502 logic but renders BLANK, because every NES PPU
|
|
4
|
+
// write the logic makes is trapped to rts. This shim makes the boot picture
|
|
5
|
+
// actually appear on SNES hardware.
|
|
6
|
+
//
|
|
7
|
+
// STATUS: WORKING (phase-1 static boot picture). The CONVERSION half
|
|
8
|
+
// (nesTileToSnes4bpp / nesColorToBgr555 / buildSnesAssets) is correct and
|
|
9
|
+
// unit-tested, and the emitted 65816 UPLOAD routine (emitPpuShim →
|
|
10
|
+
// NES_SHIM_PRESENT) now DMAs tiles+tilemap+palette to SNES VRAM/CGRAM and turns
|
|
11
|
+
// the screen on — verified end-to-end on snes9x (test/recompile-shim-render.test.js
|
|
12
|
+
// asserts the converted assets land in VRAM/CGRAM; a recompiled NES boot screen
|
|
13
|
+
// renders in color). The recompile op still gates the shim behind withShim:true
|
|
14
|
+
// because phase 1 only draws the STATIC first screen — the recompiled NES logic
|
|
15
|
+
// then runs in emulation mode against a STUBBED PPU seam, so animation/scroll/
|
|
16
|
+
// sprites are not maintained yet (next layer; see the design note below).
|
|
17
|
+
//
|
|
18
|
+
// The bug that made this "experimental" for a while: `rep #$10` makes X 16-bit at
|
|
19
|
+
// runtime, but asar sizes index immediates by the literal and assembled a bare
|
|
20
|
+
// `cpx #32` (the small CGRAM count) as an 8-bit instruction — the CPU then decoded
|
|
21
|
+
// 3 bytes, ate the next opcode, and the whole routine derailed (blank screen +
|
|
22
|
+
// CPU runaway). Fix: `cpx.w` on every loop. The compareRender/findDiverge oracles
|
|
23
|
+
// were the tools that localized it.
|
|
24
|
+
//
|
|
25
|
+
// DESIGN (phase 1 — the STATIC boot picture). Rather than hand-write a full NES
|
|
26
|
+
// PPU emulator in 65816 asm, the shim is generated at recompile time from the
|
|
27
|
+
// LIVE NES PPU state: the recompiler already boots the original ROM, so we read
|
|
28
|
+
// its CHR (tiles), nametable (the background map), and palette AFTER boot,
|
|
29
|
+
// CONVERT them to SNES formats in JS, and emit:
|
|
30
|
+
// - SNES tile data (NES 2bpp → SNES 4bpp), as bytes to incbin/db
|
|
31
|
+
// - a SNES tilemap (from the NES nametable), and
|
|
32
|
+
// - a SNES palette (NES indices → BGR555 CGRAM)
|
|
33
|
+
// - a small fixed 65816 routine that DMAs all three to VRAM/CGRAM and turns
|
|
34
|
+
// the screen on.
|
|
35
|
+
// The result: the port shows the same first screen as the original. This is the
|
|
36
|
+
// static path; animated/scrolling/sprite presentation is the next layer (the
|
|
37
|
+
// shim would then run per-frame off the seam's maintained NES-VRAM mirror).
|
|
38
|
+
//
|
|
39
|
+
// Plain JS ESM + JSDoc. Pairs with recompile-65816.js + the NES PPU read/decode
|
|
40
|
+
// helpers in platforms/nes/ppu.js.
|
|
41
|
+
|
|
42
|
+
import { decodeTile, nesPaletteIndexToRgb } from "../platforms/nes/ppu.js";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert one NES 2bpp tile (16 bytes) to one SNES 4bpp tile (32 bytes).
|
|
46
|
+
*
|
|
47
|
+
* NES tile: 8 bytes plane0 then 8 bytes plane1 (bit per pixel each), giving a
|
|
48
|
+
* 2-bit (0-3) index per pixel. SNES 4bpp tile: 32 bytes as TWO bitplane pairs —
|
|
49
|
+
* bytes 0-15 are planes 0&1 row-interleaved (lo,hi,lo,hi,... per row), bytes
|
|
50
|
+
* 16-31 are planes 2&3 the same way. We map the NES 2-bit index straight into
|
|
51
|
+
* the low two SNES planes; planes 2&3 stay 0 (NES only has 4 colors per tile).
|
|
52
|
+
*
|
|
53
|
+
* @param {Uint8Array} nesTile16
|
|
54
|
+
* @returns {Uint8Array} 32-byte SNES 4bpp tile
|
|
55
|
+
*/
|
|
56
|
+
export function nesTileToSnes4bpp(nesTile16) {
|
|
57
|
+
const px = decodeTile(nesTile16); // 64 entries, each 0-3
|
|
58
|
+
const out = new Uint8Array(32);
|
|
59
|
+
for (let y = 0; y < 8; y++) {
|
|
60
|
+
let p0 = 0, p1 = 0;
|
|
61
|
+
for (let x = 0; x < 8; x++) {
|
|
62
|
+
const v = px[y * 8 + x];
|
|
63
|
+
const bit = 7 - x;
|
|
64
|
+
if (v & 1) p0 |= 1 << bit;
|
|
65
|
+
if (v & 2) p1 |= 1 << bit;
|
|
66
|
+
}
|
|
67
|
+
// planes 0&1 row-interleaved in the first 16 bytes
|
|
68
|
+
out[y * 2] = p0;
|
|
69
|
+
out[y * 2 + 1] = p1;
|
|
70
|
+
// planes 2&3 (bytes 16-31) remain 0
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert a NES palette byte (a 6-bit NES color index) to a SNES BGR555 uint16.
|
|
77
|
+
* NES index → RGB888 (via the canonical NES palette) → BGR555 (5 bits each,
|
|
78
|
+
* blue high). This is the CGRAM word format.
|
|
79
|
+
* @param {number} nesIndex
|
|
80
|
+
* @returns {number} 0..0x7FFF
|
|
81
|
+
*/
|
|
82
|
+
export function nesColorToBgr555(nesIndex) {
|
|
83
|
+
const [r, g, b] = nesPaletteIndexToRgb(nesIndex & 0x3f);
|
|
84
|
+
const r5 = (r >> 3) & 0x1f;
|
|
85
|
+
const g5 = (g >> 3) & 0x1f;
|
|
86
|
+
const b5 = (b >> 3) & 0x1f;
|
|
87
|
+
return (b5 << 10) | (g5 << 5) | r5;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the SNES presentation assets from live NES PPU state.
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} state
|
|
94
|
+
* @param {Uint8Array} state.chr NES pattern data (>=4096 bytes; the BG
|
|
95
|
+
* pattern table — we take the first 256 tiles / 4KB).
|
|
96
|
+
* @param {Uint8Array} state.nametable one 32x30 NES nametable's tile indices
|
|
97
|
+
* (960 bytes; the attribute bytes that follow are ignored in phase 1).
|
|
98
|
+
* @param {Uint8Array} state.palette 32-byte NES palette ($3F00-$3F1F). We use
|
|
99
|
+
* the BG palette (first 16 bytes) for CGRAM.
|
|
100
|
+
* @returns {{ tiles: Uint8Array, tilemap: Uint8Array, cgram: Uint8Array,
|
|
101
|
+
* tileCount: number, mapEntries: number }}
|
|
102
|
+
*/
|
|
103
|
+
export function buildSnesAssets({ chr, nametable, palette }) {
|
|
104
|
+
// Tiles: first 256 NES tiles (4KB) → 256 SNES 4bpp tiles (8KB).
|
|
105
|
+
const tileCount = Math.min(256, Math.floor(chr.length / 16));
|
|
106
|
+
const tiles = new Uint8Array(tileCount * 32);
|
|
107
|
+
for (let t = 0; t < tileCount; t++) {
|
|
108
|
+
const nes = chr.subarray(t * 16, t * 16 + 16);
|
|
109
|
+
tiles.set(nesTileToSnes4bpp(nes), t * 32);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Tilemap: NES nametable is 32x30 of 1-byte tile indices. SNES BG map entries
|
|
113
|
+
// are 16-bit (tile# in low 10 bits + palette/priority/flip in the high bits).
|
|
114
|
+
// Phase 1: palette 0, no flip/priority → entry = tile index. SNES BG map is
|
|
115
|
+
// 32x32; we fill the first 30 rows and leave the bottom 2 as tile 0.
|
|
116
|
+
const mapEntries = 32 * 32;
|
|
117
|
+
const tilemap = new Uint8Array(mapEntries * 2);
|
|
118
|
+
for (let row = 0; row < 30; row++) {
|
|
119
|
+
for (let col = 0; col < 32; col++) {
|
|
120
|
+
const nesTile = nametable[row * 32 + col] ?? 0;
|
|
121
|
+
const e = (row * 32 + col) * 2;
|
|
122
|
+
tilemap[e] = nesTile; // low byte = tile index
|
|
123
|
+
tilemap[e + 1] = 0; // high byte = palette 0, no flip/prio
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// CGRAM: NES BG palette (16 bytes) → 16 BGR555 colors. NES color 0 ($3F00) is
|
|
128
|
+
// the universal backdrop → CGRAM index 0.
|
|
129
|
+
const cgram = new Uint8Array(16 * 2);
|
|
130
|
+
for (let i = 0; i < 16; i++) {
|
|
131
|
+
const bgr = nesColorToBgr555(palette[i] ?? 0);
|
|
132
|
+
cgram[i * 2] = bgr & 0xff;
|
|
133
|
+
cgram[i * 2 + 1] = (bgr >> 8) & 0xff;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { tiles, tilemap, cgram, tileCount, mapEntries };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Format a byte array as asar `db $xx,$xx,...` lines (16 per line). */
|
|
140
|
+
function dbLines(bytes, indent = " ") {
|
|
141
|
+
const lines = [];
|
|
142
|
+
for (let i = 0; i < bytes.length; i += 16) {
|
|
143
|
+
const chunk = Array.from(bytes.subarray(i, i + 16))
|
|
144
|
+
.map((b) => "$" + b.toString(16).padStart(2, "0"))
|
|
145
|
+
.join(",");
|
|
146
|
+
lines.push(`${indent}db ${chunk}`);
|
|
147
|
+
}
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Emit the SNES PPU shim as asar source: the converted data tables + a fixed
|
|
153
|
+
* 65816 routine `NES_SHIM_PRESENT` that DMAs tiles→VRAM, tilemap→VRAM,
|
|
154
|
+
* palette→CGRAM, sets BG mode 0 / bases, and turns the screen on. The
|
|
155
|
+
* recompiled reset wrapper calls NES_SHIM_PRESENT once after the game's boot
|
|
156
|
+
* logic has run (so the NES state is fully set up before we snapshot+convert).
|
|
157
|
+
*
|
|
158
|
+
* VRAM layout (word addresses): tiles at $0000, BG1 tilemap at $4000.
|
|
159
|
+
*
|
|
160
|
+
* @param {ReturnType<typeof buildSnesAssets>} assets
|
|
161
|
+
* @returns {string} asar source (define NES_SHIM_PRESENT + the data)
|
|
162
|
+
*/
|
|
163
|
+
export function emitPpuShim(assets) {
|
|
164
|
+
const { tiles, tilemap, cgram, tileCount } = assets;
|
|
165
|
+
return [
|
|
166
|
+
"; ── NES-PPU-on-SNES shim (phase 1: static boot picture) ──────────────",
|
|
167
|
+
"; Converted from the original ROM's live PPU state at recompile time:",
|
|
168
|
+
`; ${tileCount} tiles (NES 2bpp → SNES 4bpp), a 32x30 BG tilemap, 16 colors.`,
|
|
169
|
+
"; NES_SHIM_PRESENT DMAs them to VRAM/CGRAM and enables BG1. Call it once",
|
|
170
|
+
"; after the game's boot logic has set up its PPU state.",
|
|
171
|
+
"",
|
|
172
|
+
"; SNES PPU registers used by the shim",
|
|
173
|
+
"!INIDISP = $2100",
|
|
174
|
+
"!BGMODE = $2105",
|
|
175
|
+
"!BG1SC = $2107 ; BG1 map base + size",
|
|
176
|
+
"!BG12NBA = $210B ; BG1/BG2 char base",
|
|
177
|
+
"!VMAIN = $2115",
|
|
178
|
+
"!VMADDL = $2116",
|
|
179
|
+
"!VMDATAL = $2118",
|
|
180
|
+
"!CGADD = $2121",
|
|
181
|
+
"!CGDATA = $2122",
|
|
182
|
+
"!TM = $212C",
|
|
183
|
+
"",
|
|
184
|
+
"NES_SHIM_PRESENT:",
|
|
185
|
+
" php",
|
|
186
|
+
" sep #$20 ; 8-bit A for register writes",
|
|
187
|
+
" rep #$10 ; 16-bit X/Y for loop indices",
|
|
188
|
+
"",
|
|
189
|
+
" lda #$80",
|
|
190
|
+
" sta !INIDISP ; forced blank during upload",
|
|
191
|
+
" lda #$80",
|
|
192
|
+
" sta !VMAIN ; VRAM addr +1 word after the HIGH-byte write",
|
|
193
|
+
"",
|
|
194
|
+
" ; ── tiles → VRAM word $0000 ── (word writes via VMDATAL/H)",
|
|
195
|
+
" ldx #$0000",
|
|
196
|
+
" stx !VMADDL ; VRAM word address $0000 (16-bit store)",
|
|
197
|
+
" ldx #$0000",
|
|
198
|
+
"- lda.l NES_SHIM_TILES,x",
|
|
199
|
+
" sta !VMDATAL ; low byte",
|
|
200
|
+
" inx",
|
|
201
|
+
" lda.l NES_SHIM_TILES,x",
|
|
202
|
+
" sta !VMDATAL+1 ; high byte → triggers +1 word",
|
|
203
|
+
" inx",
|
|
204
|
+
// `cpx.w` forces the 16-bit immediate form. We ran `rep #$10`, so X is
|
|
205
|
+
// 16-bit at RUNTIME, but asar does NOT track register width across rep/sep —
|
|
206
|
+
// it sizes index immediates by the literal, defaulting <256 to 8-bit. A bare
|
|
207
|
+
// `cpx #32` would assemble to 2 bytes while the CPU decodes 3, eating the
|
|
208
|
+
// next opcode and derailing the routine. The big tile/map counts (>255)
|
|
209
|
+
// happen to force 16-bit anyway; the small CGRAM count (32) is the one that
|
|
210
|
+
// bit us — so make ALL three explicit with `.w` and never rely on the value.
|
|
211
|
+
` cpx.w #${tiles.length}`,
|
|
212
|
+
" bne -",
|
|
213
|
+
"",
|
|
214
|
+
" ; ── tilemap → VRAM word $4000 ──",
|
|
215
|
+
" ldx #$4000",
|
|
216
|
+
" stx !VMADDL ; VRAM word address $4000",
|
|
217
|
+
" ldx #$0000",
|
|
218
|
+
"- lda.l NES_SHIM_MAP,x",
|
|
219
|
+
" sta !VMDATAL",
|
|
220
|
+
" inx",
|
|
221
|
+
" lda.l NES_SHIM_MAP,x",
|
|
222
|
+
" sta !VMDATAL+1",
|
|
223
|
+
" inx",
|
|
224
|
+
` cpx.w #${tilemap.length}`,
|
|
225
|
+
" bne -",
|
|
226
|
+
"",
|
|
227
|
+
" ; ── palette → CGRAM index 0 ──",
|
|
228
|
+
" lda #$00",
|
|
229
|
+
" sta !CGADD",
|
|
230
|
+
" ldx #$0000",
|
|
231
|
+
`- lda.l NES_SHIM_CGRAM,x`,
|
|
232
|
+
" sta !CGDATA ; CGADD auto-increments after each byte",
|
|
233
|
+
" inx",
|
|
234
|
+
` cpx.w #${cgram.length} ; .w: 16-bit X at runtime (see note above)`,
|
|
235
|
+
" bne -",
|
|
236
|
+
"",
|
|
237
|
+
" ; ── BG setup + screen on ──",
|
|
238
|
+
" lda #$00",
|
|
239
|
+
" sta !BGMODE ; mode 0, 8x8 tiles",
|
|
240
|
+
" lda #$40",
|
|
241
|
+
" sta !BG1SC ; BG1 map base = VRAM word $4000 ((reg>>2)<<10)",
|
|
242
|
+
" lda #$00",
|
|
243
|
+
" sta !BG12NBA ; BG1 char base = VRAM word $0000",
|
|
244
|
+
" lda #$01",
|
|
245
|
+
" sta !TM ; enable BG1 on main screen",
|
|
246
|
+
" lda #$0f",
|
|
247
|
+
" sta !INIDISP ; full brightness, blank off",
|
|
248
|
+
" plp",
|
|
249
|
+
" rts",
|
|
250
|
+
"",
|
|
251
|
+
"; Shim data in its OWN bank so `lda.l TABLE,x` indexing never straddles a",
|
|
252
|
+
"; LoROM 32KB bank boundary (the 8KB tile table would otherwise wrap). Bank 2",
|
|
253
|
+
"; ($02:8000) is free after the code (bank 0) and the recompiled body.",
|
|
254
|
+
"org $028000",
|
|
255
|
+
"NES_SHIM_TILES:",
|
|
256
|
+
dbLines(tiles),
|
|
257
|
+
"NES_SHIM_MAP:",
|
|
258
|
+
dbLines(tilemap),
|
|
259
|
+
"NES_SHIM_CGRAM:",
|
|
260
|
+
dbLines(cgram),
|
|
261
|
+
"",
|
|
262
|
+
].join("\n");
|
|
263
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Static pointer/jump-table decoder. The literal "which index dispatches to this
|
|
2
|
+
// handler?" map, the static complement to the LIVE breakpoint({on:'jumptable'})
|
|
3
|
+
// resolver. Handles the three forms a real dispatcher uses (v0.41.0 feedback N2):
|
|
4
|
+
//
|
|
5
|
+
// 1. CONTIGUOUS little-endian words — `dw handler0, handler1, …` at one base.
|
|
6
|
+
// 2. SPLIT lo/hi — low bytes in one array, high bytes in a SEPARATE array at a
|
|
7
|
+
// different base: handler = (hi[i] << 8) | lo[i]. (Zanac's form.)
|
|
8
|
+
// 3. The 6502 RTS-trick (`+1`): the stored value is handler-1 (push, rts → +1),
|
|
9
|
+
// so the real handler = storedWord + 1.
|
|
10
|
+
//
|
|
11
|
+
// Plus a REVERSE lookup: handler address → the dispatch index/indices that reach
|
|
12
|
+
// it ("what object state triggers this routine?").
|
|
13
|
+
//
|
|
14
|
+
// Operates on the raw ROM image + a CPU-address→file-offset mapper (so it works
|
|
15
|
+
// on banked carts). Plain JS ESM + JSDoc.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Decode a pointer/jump table into index → handler entries.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} opts
|
|
21
|
+
* @param {Uint8Array} opts.data raw ROM image (incl. header)
|
|
22
|
+
* @param {(cpuAddr:number)=>number} opts.toOffset map a CPU address to a file
|
|
23
|
+
* offset into `data` (banked-cart aware; throws/returns -1 if unmapped).
|
|
24
|
+
* @param {number} opts.count number of entries.
|
|
25
|
+
* @param {number} [opts.loBase] CPU address of the low-byte array. For the
|
|
26
|
+
* contiguous form, this is the table base (entries are 2 bytes each).
|
|
27
|
+
* @param {number} [opts.hiBase] CPU address of the high-byte array (SPLIT
|
|
28
|
+
* form). Omit for the contiguous form (hi byte follows each lo byte).
|
|
29
|
+
* @param {"direct"|"rts+1"} [opts.convention="direct"] add 1 to each stored word
|
|
30
|
+
* for the 6502 RTS-trick.
|
|
31
|
+
* @param {"LE"|"BE"} [opts.endian="LE"] byte order of the CONTIGUOUS form (the
|
|
32
|
+
* split form is inherently lo-array/hi-array so endian doesn't apply).
|
|
33
|
+
* @returns {{ entries: Array<{index:number, handler:number, storedWord:number}>,
|
|
34
|
+
* form: string }}
|
|
35
|
+
*/
|
|
36
|
+
export function decodePointerTable({ data, toOffset, count, loBase, hiBase, convention = "direct", endian = "LE" }) {
|
|
37
|
+
if (!Number.isInteger(count) || count <= 0) throw new Error("decodePointerTable: count must be a positive integer.");
|
|
38
|
+
if (loBase == null) throw new Error("decodePointerTable: loBase (the table / low-byte base) is required.");
|
|
39
|
+
const add = convention === "rts+1" ? 1 : 0;
|
|
40
|
+
const byteAt = (cpuAddr) => {
|
|
41
|
+
const off = toOffset(cpuAddr);
|
|
42
|
+
if (off == null || off < 0 || off >= data.length) {
|
|
43
|
+
throw new Error(`decodePointerTable: CPU address $${cpuAddr.toString(16)} maps outside the ROM (offset ${off}).`);
|
|
44
|
+
}
|
|
45
|
+
return data[off];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const entries = [];
|
|
49
|
+
const split = hiBase != null;
|
|
50
|
+
for (let i = 0; i < count; i++) {
|
|
51
|
+
let stored;
|
|
52
|
+
if (split) {
|
|
53
|
+
const lo = byteAt(loBase + i);
|
|
54
|
+
const hi = byteAt(hiBase + i);
|
|
55
|
+
stored = (hi << 8) | lo;
|
|
56
|
+
} else if (endian === "BE") {
|
|
57
|
+
const hi = byteAt(loBase + i * 2);
|
|
58
|
+
const lo = byteAt(loBase + i * 2 + 1);
|
|
59
|
+
stored = (hi << 8) | lo;
|
|
60
|
+
} else {
|
|
61
|
+
const lo = byteAt(loBase + i * 2);
|
|
62
|
+
const hi = byteAt(loBase + i * 2 + 1);
|
|
63
|
+
stored = (lo | (hi << 8));
|
|
64
|
+
}
|
|
65
|
+
const handler = (stored + add) & 0xffff;
|
|
66
|
+
entries.push({ index: i, handler, storedWord: stored });
|
|
67
|
+
}
|
|
68
|
+
const form = split
|
|
69
|
+
? `split lo@$${loBase.toString(16).toUpperCase()}/hi@$${hiBase.toString(16).toUpperCase()}`
|
|
70
|
+
: `contiguous ${endian}@$${loBase.toString(16).toUpperCase()}`;
|
|
71
|
+
return { entries, form: form + (add ? " (rts+1)" : "") };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reverse lookup: which dispatch index/indices land on `handler`. Returns all
|
|
76
|
+
* matches (a handler can be shared by several states).
|
|
77
|
+
* @param {ReturnType<typeof decodePointerTable>["entries"]} entries
|
|
78
|
+
* @param {number} handler
|
|
79
|
+
* @returns {number[]} indices
|
|
80
|
+
*/
|
|
81
|
+
export function reverseLookup(entries, handler) {
|
|
82
|
+
const h = handler & 0xffff;
|
|
83
|
+
return entries.filter((e) => e.handler === h).map((e) => e.index);
|
|
84
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// IR → 65816 (SNES asar) emitter. The 65816 boots in 6502 EMULATION mode, so a
|
|
2
|
+
// 6502-sourced IR is "near-1:1": reg/branch/jump/call/return nodes re-emit the
|
|
3
|
+
// original mnemonic verbatim, and hwreg (the MMIO seam) becomes a call into the
|
|
4
|
+
// NES-PPU-on-SNES runtime. This emitter reproduces the byte-for-byte body the
|
|
5
|
+
// original monolithic recompile-65816 produced — it's the same output, reached
|
|
6
|
+
// through the generic IR so OTHER source ISAs (whose lifters emit the same IR)
|
|
7
|
+
// could target 65816 too, and so 6502 can target OTHER CPUs via a different emitter.
|
|
8
|
+
//
|
|
9
|
+
// Plain JS ESM + JSDoc.
|
|
10
|
+
|
|
11
|
+
import { IR } from "./ir.js";
|
|
12
|
+
import { NES_REGISTERS } from "../../platforms/common/registers.js";
|
|
13
|
+
|
|
14
|
+
/** The native 6502 mnemonic for each IR `reg` node is carried on the node itself
|
|
15
|
+
* (node.mnemonic); for 65816 emulation mode we re-emit it unchanged. */
|
|
16
|
+
function emitRegNode(node) {
|
|
17
|
+
const { mnemonic, operand } = node;
|
|
18
|
+
return ` ${mnemonic}${operand ? " " + operand : ""}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Emit the seam call(s) for one hwreg access — identical to the original
|
|
22
|
+
* emitSeamAccess: A holds the value (writes) / receives it (reads); the register
|
|
23
|
+
* low byte goes in X so one seam routine handles the whole file. */
|
|
24
|
+
function emitHwReg(node) {
|
|
25
|
+
const { access, reg, via } = node;
|
|
26
|
+
const regName = NES_REGISTERS[reg] || `REG_${reg.toString(16)}`;
|
|
27
|
+
const lowByte = `#$${(reg & 0xff).toString(16).padStart(2, "0")}`;
|
|
28
|
+
const lines = [` ; seam: ${via} $${reg.toString(16)} (${regName})`];
|
|
29
|
+
if (access === "write") {
|
|
30
|
+
if (via === "sta") {
|
|
31
|
+
lines.push(` ldx ${lowByte}`, ` jsr NES_PPU_WRITE`);
|
|
32
|
+
} else { // stx / sty
|
|
33
|
+
lines.push(` txa`, ` ldx ${lowByte}`, ` jsr NES_PPU_WRITE`);
|
|
34
|
+
}
|
|
35
|
+
} else { // read: lda / ldx / ldy / bit
|
|
36
|
+
lines.push(` ldx ${lowByte}`, ` jsr NES_PPU_READ`);
|
|
37
|
+
if (via === "bit") lines.push(` ; (bit set N/V from the read value)`);
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Emit the 65816 body from an IR program. Returns the body text (functions only;
|
|
44
|
+
* the LoROM wrapper/seam/vectors are added by the orchestrator via emitMainAsm).
|
|
45
|
+
* A leading label on a node is emitted as its own `label:` line, matching the
|
|
46
|
+
* original output exactly so the existing NES→SNES tests stay green.
|
|
47
|
+
* @param {Array<object>} ir
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function emit65816Body(ir) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const node of ir) {
|
|
53
|
+
// node-level label (rides on instruction/branch/etc nodes)
|
|
54
|
+
if (node.label && node.op !== IR.LABEL) out.push(`${node.label}:`);
|
|
55
|
+
switch (node.op) {
|
|
56
|
+
case IR.LABEL:
|
|
57
|
+
out.push(`${node.name}:`);
|
|
58
|
+
break;
|
|
59
|
+
case IR.REG:
|
|
60
|
+
out.push(emitRegNode(node));
|
|
61
|
+
break;
|
|
62
|
+
case IR.BRANCH:
|
|
63
|
+
// 6502 branch mnemonics ARE valid 65816 — re-emit verbatim. node.raw holds
|
|
64
|
+
// the original "bne L8016"; recover the mnemonic from it for fidelity.
|
|
65
|
+
out.push(` ${branchMnemonic(node)} ${node.target}`);
|
|
66
|
+
break;
|
|
67
|
+
case IR.JUMP:
|
|
68
|
+
out.push(` jmp ${node.target}`);
|
|
69
|
+
break;
|
|
70
|
+
case IR.CALL:
|
|
71
|
+
out.push(` jsr ${node.target}`);
|
|
72
|
+
break;
|
|
73
|
+
case IR.RET:
|
|
74
|
+
out.push(node.kind === "interrupt" ? " rti" : " rts");
|
|
75
|
+
break;
|
|
76
|
+
case IR.HWREG:
|
|
77
|
+
out.push(...emitHwReg(node));
|
|
78
|
+
break;
|
|
79
|
+
case IR.PASSTHROUGH:
|
|
80
|
+
out.push(node.text);
|
|
81
|
+
break;
|
|
82
|
+
case IR.REFUSE:
|
|
83
|
+
// Visible marker so the asm still SHOWS where logic was dropped (residue
|
|
84
|
+
// is also reported structurally by the orchestrator).
|
|
85
|
+
out.push(` ; UNTRANSLATED: ${(node.raw || "").trim()} (${node.reason})`);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Recover the 6502 branch mnemonic for an IR branch node from its raw text (the
|
|
95
|
+
* cond→mnemonic map is 1:1 for 65816 emulation mode). */
|
|
96
|
+
function branchMnemonic(node) {
|
|
97
|
+
const m = (node.raw || "").trim().match(/^[a-zA-Z_]\w*:\s*([a-z]{3})|^([a-z]{3})/);
|
|
98
|
+
if (m) return (m[1] || m[2]);
|
|
99
|
+
// fall back from the ISA-neutral cond
|
|
100
|
+
return { eq: "beq", ne: "bne", cc: "bcc", cs: "bcs", mi: "bmi", pl: "bpl", vc: "bvc", vs: "bvs" }[node.cond] || "bne";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** This emitter's target ISA + a human label (for the registry + diagnostics). */
|
|
104
|
+
export const TARGET_ISA = "65816";
|
|
105
|
+
export const TARGET_PLATFORM = "snes";
|