@tmustier/pi-nes 0.2.21 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -0
- package/README.md +12 -0
- package/TODO_INVENTORY.md +116 -0
- package/extensions/nes/config.ts +19 -5
- package/extensions/nes/index.ts +10 -18
- package/extensions/nes/native/nes-core/index.d.ts +0 -3
- package/extensions/nes/native/nes-core/index.node +0 -0
- package/extensions/nes/native/nes-core/native.d.ts +0 -2
- package/extensions/nes/native/nes-core/src/lib.rs +0 -10
- package/extensions/nes/native/nes-core/vendor/nes_rust/TODO_INVENTORY.md +60 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/VENDOR.md +24 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/ppu.rs +14 -44
- package/extensions/nes/nes-component.ts +67 -32
- package/extensions/nes/nes-core.ts +3 -6
- package/extensions/nes/nes-session.ts +14 -1
- package/extensions/nes/renderer.ts +77 -47
- package/extensions/nes/roms.ts +0 -13
- package/extensions/nes/saves.ts +8 -1
- package/package.json +11 -1
- package/scripts/update-vendor-nes-rust.sh +30 -0
- package/spec.md +2 -1
- package/tests/README.md +122 -0
- package/tests/config.test.ts +96 -0
- package/tests/core-smoke.test.ts +124 -0
- package/tests/debug-game.ts +203 -0
- package/tests/game-scripts.ts +123 -0
- package/tests/input-map.test.ts +46 -0
- package/tests/paths.test.ts +78 -0
- package/tests/regression.test.ts +243 -0
- package/tests/roms.test.ts +27 -0
- package/tests/saves.test.ts +32 -0
package/AGENTS.md
CHANGED
|
@@ -36,6 +36,13 @@ pi-nes/
|
|
|
36
36
|
|
|
37
37
|
The emulator uses the [`nes_rust`](https://crates.io/crates/nes_rust) crate (vendored + patched in `native/nes-core/vendor/nes_rust` for SRAM helpers) with [napi-rs](https://napi.rs) bindings.
|
|
38
38
|
|
|
39
|
+
### Vendored `nes_rust` workflow
|
|
40
|
+
|
|
41
|
+
- Source of truth is the fork (intended): `https://github.com/tmustier/nes-rust`.
|
|
42
|
+
- Make changes in the fork first, then re-vendor via `scripts/update-vendor-nes-rust.sh`.
|
|
43
|
+
- Update `extensions/nes/native/nes-core/vendor/nes_rust/VENDOR.md` with the fork commit + date + patch summary.
|
|
44
|
+
- Keep TODO inventory in `extensions/nes/native/nes-core/vendor/nes_rust/TODO_INVENTORY.md`.
|
|
45
|
+
|
|
39
46
|
**API exposed to JavaScript:**
|
|
40
47
|
- `new NativeNes()` - Create emulator instance
|
|
41
48
|
- `setRom(Uint8Array)` - Load ROM data
|
package/README.md
CHANGED
|
@@ -86,6 +86,12 @@ Config is stored at `~/.pi/nes/config.json`. Use `/nes config` for quick setup.
|
|
|
86
86
|
| `imageQuality` | `"balanced"` | `"balanced"` (30 fps) or `"high"` (60 fps) |
|
|
87
87
|
| `pixelScale` | `1.0` | Display scale (0.5–4.0) |
|
|
88
88
|
|
|
89
|
+
## Saves
|
|
90
|
+
|
|
91
|
+
Battery-backed SRAM is saved to `<saveDir>/<rom-name>-<hash>.sav` where the hash is derived from the full ROM path to avoid collisions. Old `<rom-name>.sav` files are ignored.
|
|
92
|
+
|
|
93
|
+
Saves are flushed on quit and periodically during play.
|
|
94
|
+
|
|
89
95
|
## Terminal Support
|
|
90
96
|
|
|
91
97
|
**Best experience:** a Kitty-protocol terminal like Ghostty, Kitty, or WezTerm (image protocol + key-up events).
|
|
@@ -100,6 +106,12 @@ Set `"renderer": "text"` if you prefer the ANSI renderer or have display issues.
|
|
|
100
106
|
- **No audio** — Sound is not currently supported
|
|
101
107
|
- **No save states** — Only battery-backed SRAM saves work
|
|
102
108
|
|
|
109
|
+
## Vendored Dependencies
|
|
110
|
+
|
|
111
|
+
- `nes_rust` is vendored under `extensions/nes/native/nes-core/vendor/nes_rust`.
|
|
112
|
+
- Fork: https://github.com/tmustier/nes-rust (upstream: https://github.com/takahirox/nes-rust)
|
|
113
|
+
- Update helper: `scripts/update-vendor-nes-rust.sh`
|
|
114
|
+
|
|
103
115
|
---
|
|
104
116
|
|
|
105
117
|
## Building from Source
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# TODO Inventory Review
|
|
2
|
+
|
|
3
|
+
Scope: `extensions/nes/native/nes-core/vendor/nes_rust` (vendored emulator core). Each TODO reviewed for necessity.
|
|
4
|
+
|
|
5
|
+
## Action Plan (prioritized)
|
|
6
|
+
|
|
7
|
+
### P0 — Correctness (act)
|
|
8
|
+
- [ ] APU timing fixes (sample_period, sampling timing, frame sequencer timing, sweep negation, DMC stall timing)
|
|
9
|
+
- [ ] PPU fetch timing + scroll updates (cycles 257–340, subcycle fetch, attribute fetch, pixel alignment)
|
|
10
|
+
- [ ] Mapper IRQ/MMC1 correctness (MMC3 IRQ timing/placement, MMC1 32KB banking fix)
|
|
11
|
+
- [ ] CPU correctness/timing (ADC 0x71 page-cross, NMI vs IRQ priority, BIT/JMP/JSR/RTI/RTS/SBC logic, relative addressing sign extension, DMA stall timing)
|
|
12
|
+
|
|
13
|
+
### P1 — Architecture cleanup
|
|
14
|
+
- [ ] Move MMC3 IRQ handling out of ROM/PPU into mapper layer
|
|
15
|
+
- [ ] Replace invalid-register/addressing TODOs with explicit no-op or `unreachable!()`
|
|
16
|
+
- [ ] Remove/clarify doc-only TODOs (audio example note, greyscale comment, PPU master/slave select if intentionally ignored)
|
|
17
|
+
|
|
18
|
+
### P2 — Optional refactors/optimizations
|
|
19
|
+
- [ ] Opcode table refactor
|
|
20
|
+
- [ ] Register<u8>/Register<u16> merge
|
|
21
|
+
- [ ] Audio buffer cleanup + constant for 4096
|
|
22
|
+
- [ ] Header caching + sprite eval/pos calculation optimizations
|
|
23
|
+
|
|
24
|
+
## extensions/nes/native/nes-core/vendor/nes_rust/src/lib.rs
|
|
25
|
+
| Line | TODO | Assessment |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| 57 | Audio buffer sample code is T.B.D. (doc example) | Not needed for runtime; either remove the TODO or replace with a short note that audio output is omitted in the example. |
|
|
28
|
+
|
|
29
|
+
## extensions/nes/native/nes-core/vendor/nes_rust/src/apu.rs
|
|
30
|
+
| Line | TODO | Assessment |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| 53 | Fix sample_period (1764000 / 44100) | Needed for accurate audio timing; keep until audio timing is corrected. |
|
|
33
|
+
| 67 | Implement reset properly | Needed for accurate APU reset behavior; keep. |
|
|
34
|
+
| 79 | More precise sampling timing | Needed for correct audio output timing; keep. |
|
|
35
|
+
| 93 | Add note (DMC timer) | Not needed; either add a clarifying comment or remove the TODO. |
|
|
36
|
+
| 102 | More precise frame sequencer timing | Needed for correct APU frame sequencing; keep. |
|
|
37
|
+
| 168 | Check IRQ timing when sending | Needed for correct IRQ behavior; keep. |
|
|
38
|
+
| 276 | DMC CPU memory workaround is hacky; simplify | Optional refactor; keep if you want cleanup, otherwise can remove. |
|
|
39
|
+
| 400 | Throw an error on invalid pulse register | Not needed; invalid register writes are typically ignored. Consider removing TODO or clarifying no-op behavior. |
|
|
40
|
+
| 467 | Fix negated sweep behavior | Needed for accurate sweep; keep. |
|
|
41
|
+
| 612 | Throw an error on invalid triangle register | Not needed; invalid writes can be ignored. Consider removing TODO. |
|
|
42
|
+
| 775 | Throw an error on invalid noise register | Not needed; invalid writes can be ignored. Consider removing TODO. |
|
|
43
|
+
| 949 | DMC invalid register case | Not needed; either document as ignored or remove TODO. |
|
|
44
|
+
| 975 | Remove DMC CPU memory workaround | Optional refactor; keep if you want to eliminate the workaround. |
|
|
45
|
+
|
|
46
|
+
## extensions/nes/native/nes-core/vendor/nes_rust/src/default_audio.rs
|
|
47
|
+
| Line | TODO | Assessment |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| 29 | Remove side effect in copy_sample_buffer | Optional cleanup; keep if you plan to revisit buffer semantics. |
|
|
50
|
+
| 31 | Remove magic number (4096) | Needed for maintainability; replace with a named constant if audio is used. |
|
|
51
|
+
|
|
52
|
+
## extensions/nes/native/nes-core/vendor/nes_rust/src/ppu.rs
|
|
53
|
+
| Line | TODO | Assessment |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| 119 | Support data bus decay | Optional accuracy improvement; keep if fidelity matters. |
|
|
56
|
+
| 322 | Support greyscale if needed | Likely optional; greyscale masking is already applied in `load_palette`. Consider removing or clarifying if register-read greyscale behavior is desired. |
|
|
57
|
+
| 495 | Investigate `cycle - 1` vs `cycle - 2` pixel alignment | Needed for render correctness; keep. |
|
|
58
|
+
| 594 | Cycle 257-320 behavior | Needed for correct PPU fetch timing; keep. |
|
|
59
|
+
| 595 | Cycle 321-336 behavior | Needed for correct PPU fetch timing; keep. |
|
|
60
|
+
| 596 | Cycle 337-340 behavior | Needed for correct PPU fetch timing; keep. |
|
|
61
|
+
| 615 | 0-1 subcycle fetch details | Needed for correctness; keep. |
|
|
62
|
+
| 616 | 2-3 subcycle fetch details | Needed for correctness; keep. |
|
|
63
|
+
| 617 | 4-5 subcycle fetch details | Needed for correctness; keep. |
|
|
64
|
+
| 618 | 6-7 subcycle fetch details | Needed for correctness; keep. |
|
|
65
|
+
| 660 | Implement attribute fetch properly | Needed for PPU accuracy; keep. |
|
|
66
|
+
| 690 | Optimize pos calculation | Optional performance cleanup; not required. |
|
|
67
|
+
| 758 | Check MMC3 IRQ timing | Needed for mapper IRQ accuracy; keep. |
|
|
68
|
+
| 759 | MMC3-specific IRQ hook location | Optional refactor; keep if you plan to move this into mapper layer. |
|
|
69
|
+
| 804 | Only increment scroll if rendering enabled? | Needed for correctness; keep. |
|
|
70
|
+
| 830 | Only copy scroll if rendering enabled? | Needed for correctness; keep. |
|
|
71
|
+
| 863 | Optimize sprite evaluation | Optional performance cleanup; not required. |
|
|
72
|
+
| 1020 | Implement color emphasis properly | Needed if emphasis bits should affect output; keep. |
|
|
73
|
+
| 1069 | Implement PPU master/slave select | Likely not needed (unused on NES). Consider removing TODO or documenting it as intentionally ignored. |
|
|
74
|
+
|
|
75
|
+
## extensions/nes/native/nes-core/vendor/nes_rust/src/register.rs
|
|
76
|
+
| Line | TODO | Assessment |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 5 | Combine Register<u8> with Register<u16> | Optional refactor; not required. Consider removing if you don’t plan to refactor. |
|
|
79
|
+
|
|
80
|
+
## extensions/nes/native/nes-core/vendor/nes_rust/src/mapper.rs
|
|
81
|
+
| Line | TODO | Assessment |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| 45 | MMC3-specific `drive_irq_counter` in trait | Optional architecture cleanup; keep if you want a mapper-specific IRQ interface. |
|
|
84
|
+
| 149 | MMC1 32KB banking fix | Needed for correct MMC1 behavior; keep. |
|
|
85
|
+
|
|
86
|
+
## extensions/nes/native/nes-core/vendor/nes_rust/src/cpu.rs
|
|
87
|
+
| Line | TODO | Assessment |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| 44 | Throw error for unknown button mapping | Not needed; enum should be exhaustive. Replace with `unreachable!()` or remove TODO. |
|
|
90
|
+
| 242 | Replace opcode match with static array | Optional refactor; not required. |
|
|
91
|
+
| 620 | Add +1 cycle if page crossed (ADC 0x71) | Needed for accurate timing; keep. |
|
|
92
|
+
| 1254 | Simplify DMC sample handling | Optional refactor; keep if you want cleanup. |
|
|
93
|
+
| 1258 | Fix DMC stall timing (+4 cycles) | Needed for accuracy; keep. |
|
|
94
|
+
| 1271 | More precise frame update detection | Optional; keep if timing fidelity matters. |
|
|
95
|
+
| 1285 | Implement Poweroff input | Optional feature; keep if you plan to support it. |
|
|
96
|
+
| 1313 | Handle NMI vs IRQ priority | Needed for correctness; keep. |
|
|
97
|
+
| 1368 | Clean up operate() if needed | Not needed; remove TODO unless you plan a refactor. |
|
|
98
|
+
| 1417 | Check BIT instruction logic | Needed for correctness; keep. |
|
|
99
|
+
| 1531 | Throw on INV instruction | Not needed for runtime; prefer `unreachable!()` or remove TODO. |
|
|
100
|
+
| 1552 | Check JMP logic | Needed for correctness; keep. |
|
|
101
|
+
| 1557 | Check JSR logic | Needed for correctness; keep. |
|
|
102
|
+
| 1709 | Check RTI logic | Needed for correctness; keep. |
|
|
103
|
+
| 1716 | Check RTS logic | Needed for correctness; keep. |
|
|
104
|
+
| 1732 | Confirm SBC carry/borrow logic | Needed for correctness; keep. |
|
|
105
|
+
| 1738 | Implement correct SBC overflow logic | Needed for correctness; keep. |
|
|
106
|
+
| 1895 | Clean up store() control flow | Not needed; remove TODO unless refactoring. |
|
|
107
|
+
| 1909 | DMA stall cycle timing | Needed for accuracy (513/514 cycle detail); keep. |
|
|
108
|
+
| 1952 | Optimize interrupt handling | Optional; not required. |
|
|
109
|
+
| 1994 | Confirm relative addressing sign extension | Needed for correctness; keep. |
|
|
110
|
+
| 2116 | Throw on unknown addressing mode | Not needed; prefer `unreachable!()` or remove TODO. |
|
|
111
|
+
|
|
112
|
+
## extensions/nes/native/nes-core/vendor/nes_rust/src/rom.rs
|
|
113
|
+
| Line | TODO | Assessment |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| 139 | MMC3-specific `irq_interrupted` in ROM | Optional architecture cleanup; keep if you plan to move IRQ handling into mapper layer. |
|
|
116
|
+
| 145 | Cache RomHeader fields | Optional optimization; not required. |
|
package/extensions/nes/config.ts
CHANGED
|
@@ -47,17 +47,26 @@ export function getConfigPath(): string {
|
|
|
47
47
|
return path.join(os.homedir(), ".pi", "nes", "config.json");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function resolveConfigPath(value: string): string {
|
|
51
|
+
if (path.isAbsolute(value)) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
return path.resolve(path.dirname(getConfigPath()), value);
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
export function normalizeConfig(raw: unknown): NesConfig {
|
|
51
58
|
const parsed = typeof raw === "object" && raw !== null ? (raw as RawConfig) : {};
|
|
52
|
-
const
|
|
59
|
+
const romDirInput =
|
|
53
60
|
typeof parsed.romDir === "string" && parsed.romDir.length > 0
|
|
54
|
-
?
|
|
61
|
+
? parsed.romDir
|
|
55
62
|
: DEFAULT_CONFIG.romDir;
|
|
63
|
+
const romDir = resolveConfigPath(normalizePath(romDirInput, DEFAULT_CONFIG.romDir));
|
|
56
64
|
const saveDirFallback = getDefaultSaveDir(romDir);
|
|
57
|
-
const
|
|
65
|
+
const saveDirInput =
|
|
58
66
|
typeof parsed.saveDir === "string" && parsed.saveDir.length > 0
|
|
59
|
-
?
|
|
67
|
+
? parsed.saveDir
|
|
60
68
|
: saveDirFallback;
|
|
69
|
+
const saveDir = resolveConfigPath(normalizePath(saveDirInput, saveDirFallback));
|
|
61
70
|
const imageQuality = normalizeImageQuality(parsed.imageQuality);
|
|
62
71
|
const pixelScale = normalizePixelScale(parsed.pixelScale);
|
|
63
72
|
return {
|
|
@@ -80,7 +89,12 @@ export async function loadConfig(): Promise<NesConfig> {
|
|
|
80
89
|
let config: NesConfig;
|
|
81
90
|
try {
|
|
82
91
|
const raw = await fs.readFile(configPath, "utf8");
|
|
83
|
-
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
const normalized = normalizeConfig(parsed);
|
|
94
|
+
if (raw.trim() !== formatConfig(normalized)) {
|
|
95
|
+
await saveConfig(normalized);
|
|
96
|
+
}
|
|
97
|
+
config = normalized;
|
|
84
98
|
} catch {
|
|
85
99
|
config = DEFAULT_CONFIG;
|
|
86
100
|
}
|
package/extensions/nes/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ const TEXT_RENDER_INTERVAL_MS = 1000 / 60;
|
|
|
24
24
|
|
|
25
25
|
let activeSession: NesSession | null = null;
|
|
26
26
|
|
|
27
|
+
// ROM selection helpers.
|
|
27
28
|
async function selectRom(
|
|
28
29
|
args: string | undefined,
|
|
29
30
|
romDir: string,
|
|
@@ -60,6 +61,7 @@ async function selectRom(
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
// Command argument parsing.
|
|
63
65
|
function parseArgs(args?: string): { debug: boolean; romArg?: string } {
|
|
64
66
|
if (!args) {
|
|
65
67
|
return { debug: false, romArg: undefined };
|
|
@@ -81,6 +83,7 @@ function parseArgs(args?: string): { debug: boolean; romArg?: string } {
|
|
|
81
83
|
return { debug: false, romArg: trimmed };
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
// ROM directory validation/creation.
|
|
84
87
|
async function ensureRomDir(pathValue: string, ctx: ExtensionCommandContext): Promise<boolean> {
|
|
85
88
|
try {
|
|
86
89
|
const stat = await fs.stat(pathValue);
|
|
@@ -101,20 +104,7 @@ async function ensureRomDir(pathValue: string, ctx: ExtensionCommandContext): Pr
|
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const stat = await fs.stat(pathValue);
|
|
107
|
-
if (!stat.isDirectory()) {
|
|
108
|
-
ctx.ui.notify(`ROM directory is not a folder: ${pathValue}`, "error");
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
} catch {
|
|
113
|
-
ctx.ui.notify(`ROM directory not found: ${pathValue}`, "error");
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
107
|
+
// Config UI.
|
|
118
108
|
async function editConfigJson(
|
|
119
109
|
ctx: ExtensionCommandContext,
|
|
120
110
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
@@ -147,7 +137,7 @@ async function configureWithWizard(
|
|
|
147
137
|
const romDirDefaultLabel = config.romDir === DEFAULT_CONFIG.romDir ? "Use default" : "Use current";
|
|
148
138
|
const romDirOptions = [
|
|
149
139
|
`${romDirDefaultLabel} (${romDirDisplay}) — creates if missing`,
|
|
150
|
-
"Enter a custom path (
|
|
140
|
+
"Enter a custom path (creates if missing)",
|
|
151
141
|
];
|
|
152
142
|
const romDirChoice = await ctx.ui.select("ROM directory", romDirOptions);
|
|
153
143
|
if (!romDirChoice) {
|
|
@@ -166,8 +156,8 @@ async function configureWithWizard(
|
|
|
166
156
|
return false;
|
|
167
157
|
}
|
|
168
158
|
romDir = resolvePathInput(trimmedRomDir, ctx.cwd);
|
|
169
|
-
const
|
|
170
|
-
if (!
|
|
159
|
+
const ensured = await ensureRomDir(romDir, ctx);
|
|
160
|
+
if (!ensured) {
|
|
171
161
|
return false;
|
|
172
162
|
}
|
|
173
163
|
} else {
|
|
@@ -235,6 +225,7 @@ async function editConfig(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
235
225
|
ctx.ui.notify(`Saved config to ${getConfigPath()}`, "info");
|
|
236
226
|
}
|
|
237
227
|
|
|
228
|
+
// Session lifecycle.
|
|
238
229
|
async function createSession(romPath: string, ctx: ExtensionCommandContext, config: Awaited<ReturnType<typeof loadConfig>>): Promise<NesSession | null> {
|
|
239
230
|
let romData: Uint8Array;
|
|
240
231
|
try {
|
|
@@ -251,7 +242,7 @@ async function createSession(romPath: string, ctx: ExtensionCommandContext, conf
|
|
|
251
242
|
const message = error instanceof Error ? error.message : String(error);
|
|
252
243
|
ctx.ui.notify(`Failed to initialize NES core: ${message}`, "error");
|
|
253
244
|
ctx.ui.notify(
|
|
254
|
-
"Build native core: cd
|
|
245
|
+
"Build native core: cd extensions/nes/native/nes-core && npm install && npm run build",
|
|
255
246
|
"warning",
|
|
256
247
|
);
|
|
257
248
|
return null;
|
|
@@ -337,6 +328,7 @@ async function attachSession(
|
|
|
337
328
|
return shouldStop;
|
|
338
329
|
}
|
|
339
330
|
|
|
331
|
+
// Command registration.
|
|
340
332
|
export default function (pi: ExtensionAPI) {
|
|
341
333
|
pi.on("session_shutdown", async () => {
|
|
342
334
|
if (activeSession) {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export function nativeVersion(): string;
|
|
2
|
-
|
|
3
1
|
export interface CpuDebugState {
|
|
4
2
|
pc: number;
|
|
5
3
|
a: number;
|
|
@@ -33,7 +31,6 @@ export class NativeNes {
|
|
|
33
31
|
bootup(): void;
|
|
34
32
|
stepFrame(): void;
|
|
35
33
|
refreshFramebuffer(): void;
|
|
36
|
-
reset(): void;
|
|
37
34
|
pressButton(button: number): void;
|
|
38
35
|
releaseButton(button: number): void;
|
|
39
36
|
hasBatteryBackedRam(): boolean;
|
|
Binary file
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
/* auto-generated by NAPI-RS */
|
|
5
5
|
|
|
6
|
-
export declare function nativeVersion(): string
|
|
7
6
|
export interface CpuDebugState {
|
|
8
7
|
pc: number
|
|
9
8
|
a: number
|
|
@@ -34,7 +33,6 @@ export declare class NativeNes {
|
|
|
34
33
|
bootup(): void
|
|
35
34
|
stepFrame(): void
|
|
36
35
|
refreshFramebuffer(): void
|
|
37
|
-
reset(): void
|
|
38
36
|
pressButton(button: number): void
|
|
39
37
|
releaseButton(button: number): void
|
|
40
38
|
hasBatteryBackedRam(): boolean
|
|
@@ -7,11 +7,6 @@ use nes_rust::display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH};
|
|
|
7
7
|
use nes_rust::rom::Rom;
|
|
8
8
|
use nes_rust::Nes;
|
|
9
9
|
|
|
10
|
-
#[napi]
|
|
11
|
-
pub fn native_version() -> String {
|
|
12
|
-
env!("CARGO_PKG_VERSION").to_string()
|
|
13
|
-
}
|
|
14
|
-
|
|
15
10
|
struct NativeDisplay {
|
|
16
11
|
pixels: Vec<u8>,
|
|
17
12
|
}
|
|
@@ -119,11 +114,6 @@ impl NativeNes {
|
|
|
119
114
|
self.nes.copy_pixels(&mut self.framebuffer);
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
#[napi]
|
|
123
|
-
pub fn reset(&mut self) {
|
|
124
|
-
self.nes.reset();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
117
|
#[napi]
|
|
128
118
|
pub fn press_button(&mut self, button: u8) {
|
|
129
119
|
if let Some(mapped) = map_button(button) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# nes_rust TODO Inventory (vendored)
|
|
2
|
+
|
|
3
|
+
This inventory tracks TODOs in the vendored `nes_rust` snapshot.
|
|
4
|
+
|
|
5
|
+
## src/lib.rs
|
|
6
|
+
- L57: Audio buffer sample code T.B.D. (doc example). Likely remove or replace with note.
|
|
7
|
+
|
|
8
|
+
## src/apu.rs
|
|
9
|
+
- L53: sample_period timing fix needed for audio accuracy.
|
|
10
|
+
- L67: APU reset behavior incomplete.
|
|
11
|
+
- L79: Sampling timing not precise.
|
|
12
|
+
- L93: Add note (DMC timer) — probably remove or add clarification.
|
|
13
|
+
- L102: Frame sequencer timing not precise.
|
|
14
|
+
- L168: IRQ timing needs verification.
|
|
15
|
+
- L276: DMC CPU memory workaround is hacky; optional refactor.
|
|
16
|
+
- L400/612/775/949: Invalid register write handling — probably ignore or document no-op.
|
|
17
|
+
- L467: Sweep negation logic fix needed.
|
|
18
|
+
- L975: Remove DMC memory workaround; optional refactor.
|
|
19
|
+
|
|
20
|
+
## src/default_audio.rs
|
|
21
|
+
- L29: Remove side effect in copy_sample_buffer (optional).
|
|
22
|
+
- L31: Replace magic number 4096 with constant.
|
|
23
|
+
|
|
24
|
+
## src/ppu.rs
|
|
25
|
+
- L119: Data bus decay support (accuracy improvement).
|
|
26
|
+
- L322: Greyscale support comment; likely redundant.
|
|
27
|
+
- L495: Pixel alignment off-by-one investigation.
|
|
28
|
+
- L594-L618: Missing cycle/subcycle fetch behavior.
|
|
29
|
+
- L660: Attribute fetch correctness.
|
|
30
|
+
- L690: Optional optimization.
|
|
31
|
+
- L758-L759: MMC3 IRQ timing/placement.
|
|
32
|
+
- L804/L830: Scroll updates conditional on rendering.
|
|
33
|
+
- L863: Optional optimization.
|
|
34
|
+
- L1020: Color emphasis implementation.
|
|
35
|
+
- L1069: PPU master/slave select; likely ignorable on NES.
|
|
36
|
+
|
|
37
|
+
## src/register.rs
|
|
38
|
+
- L5: Combine Register<u8>/Register<u16> (optional refactor).
|
|
39
|
+
|
|
40
|
+
## src/mapper.rs
|
|
41
|
+
- L45: MMC3 IRQ hook in trait (optional architecture cleanup).
|
|
42
|
+
- L149: MMC1 32KB banking fix needed.
|
|
43
|
+
|
|
44
|
+
## src/cpu.rs
|
|
45
|
+
- L44: Unknown button mapping; replace with unreachable or ignore.
|
|
46
|
+
- L242: Opcode table refactor (optional).
|
|
47
|
+
- L620: Page-cross cycle for ADC 0x71 needed.
|
|
48
|
+
- L1254/L1258: DMC sample handling simplification + stall timing fix.
|
|
49
|
+
- L1271: Frame update detection precision.
|
|
50
|
+
- L1285: Poweroff input handling.
|
|
51
|
+
- L1313: NMI vs IRQ ordering.
|
|
52
|
+
- L1368/1895: Cleanup notes (optional).
|
|
53
|
+
- L1417/1552/1557/1709/1716/1732/1738/1994: CPU logic correctness checks.
|
|
54
|
+
- L1531/L2116: Invalid instruction/addressing mode handling.
|
|
55
|
+
- L1909: DMA stall timing detail.
|
|
56
|
+
- L1952: Interrupt handling optimization (optional).
|
|
57
|
+
|
|
58
|
+
## src/rom.rs
|
|
59
|
+
- L139: MMC3 IRQ in ROM (optional architecture cleanup).
|
|
60
|
+
- L145: Cache header fields (optional).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Vendored Dependency: nes_rust
|
|
2
|
+
|
|
3
|
+
## Upstream
|
|
4
|
+
- Repository: https://github.com/takahirox/nes-rust
|
|
5
|
+
- Upstream state: last known push (per GitHub API) 2020-08-28
|
|
6
|
+
|
|
7
|
+
## Fork
|
|
8
|
+
- Repository: https://github.com/tmustier/nes-rust
|
|
9
|
+
- Purpose: carry project-specific fixes and act as upstream-of-record for this vendor copy.
|
|
10
|
+
|
|
11
|
+
## Current Vendor Snapshot
|
|
12
|
+
- Source commit/tag: `fd5cf3b` (fork master)
|
|
13
|
+
- Vendored on: 2026-02-02
|
|
14
|
+
- Local patch set: SRAM helpers, CHR RAM support, mapper fixes, PPU timing tweaks, debug hooks.
|
|
15
|
+
|
|
16
|
+
## Update Process
|
|
17
|
+
1. Sync fork with upstream if needed.
|
|
18
|
+
2. Apply/maintain project patches on the fork.
|
|
19
|
+
3. Re-vendor from fork at a pinned commit/tag.
|
|
20
|
+
4. Update this file with the new commit/tag + patch notes.
|
|
21
|
+
|
|
22
|
+
## Notes
|
|
23
|
+
- This repo uses a vendored copy under `extensions/nes/native/nes-core/vendor/nes_rust`.
|
|
24
|
+
- Keep patch history in the fork so changes are auditable.
|
|
@@ -271,6 +271,7 @@ impl Ppu {
|
|
|
271
271
|
// ppustatus load
|
|
272
272
|
0x2002 => {
|
|
273
273
|
let value = self.ppustatus.load();
|
|
274
|
+
let was_vblank = (value & 0x80) != 0;
|
|
274
275
|
|
|
275
276
|
// clear vblank after reading 0x2002
|
|
276
277
|
self.ppustatus.clear_vblank();
|
|
@@ -287,10 +288,9 @@ impl Ppu {
|
|
|
287
288
|
// clears the flag, and won't fire NMI
|
|
288
289
|
|
|
289
290
|
// Note: update_flags() which can set vblank is called
|
|
290
|
-
// after this method in the same cycle
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
if self.scanline == 241 && (self.cycle == 0 || self.cycle == 1) {
|
|
291
|
+
// after this method in the same cycle. Only suppress if
|
|
292
|
+
// vblank was already set when read.
|
|
293
|
+
if was_vblank && self.scanline == 241 && self.cycle == 1 {
|
|
294
294
|
self.suppress_vblank = true;
|
|
295
295
|
}
|
|
296
296
|
|
|
@@ -735,9 +735,13 @@ impl Ppu {
|
|
|
735
735
|
fn update_flags(&mut self, rom: &mut Rom) {
|
|
736
736
|
if self.cycle == 1 {
|
|
737
737
|
if self.scanline == 241 {
|
|
738
|
-
//
|
|
738
|
+
// Set vblank and latch NMI at cycle 1 in scanline 241.
|
|
739
|
+
// NMI delivery is still delayed by CPU instruction timing.
|
|
739
740
|
if !self.suppress_vblank {
|
|
740
741
|
self.ppustatus.set_vblank();
|
|
742
|
+
if self.ppuctrl.is_nmi_enabled() {
|
|
743
|
+
self.nmi_interrupted = true;
|
|
744
|
+
}
|
|
741
745
|
}
|
|
742
746
|
self.suppress_vblank = false;
|
|
743
747
|
// Pixels for this frame should be ready so update the display
|
|
@@ -751,41 +755,6 @@ impl Ppu {
|
|
|
751
755
|
}
|
|
752
756
|
}
|
|
753
757
|
|
|
754
|
-
// According to http://wiki.nesdev.com/w/index.php/PPU_frame_timing#VBL_Flag_Timing
|
|
755
|
-
// reading 0x2002 at cycle=2 and scanline=241 can suppress NMI
|
|
756
|
-
// so firing NMI at some cycles away not at cycle=1 so far
|
|
757
|
-
|
|
758
|
-
// There is a chance that CPU 0x2002 read gets the data vblank flag set
|
|
759
|
-
// before CPU starts NMI interrupt routine.
|
|
760
|
-
// CPU instructions take multiple CPU clocks to complete.
|
|
761
|
-
// If CPU starts an operation of an istruction including 0x2002 read right before
|
|
762
|
-
// PPU sets vblank flag and fires NMI,
|
|
763
|
-
// the 0x2002 read gets the data with vblank flag set even before
|
|
764
|
-
// CPU starts NMI routine.
|
|
765
|
-
//
|
|
766
|
-
// CPU PPU
|
|
767
|
-
// 1. instruction operation start
|
|
768
|
-
// 2. - doing something vblank start and fire NMI
|
|
769
|
-
// 3. - read 0x2002 with
|
|
770
|
-
// vblank flag set
|
|
771
|
-
// 4. - doing something
|
|
772
|
-
// 5. Notice NMI and start
|
|
773
|
-
// NMI routine
|
|
774
|
-
//
|
|
775
|
-
// It seems some games rely on this behavior.
|
|
776
|
-
// To simulate this behavior we fire NMI at cycle=20 so far.
|
|
777
|
-
// If CPU reads 0x2002 between PPU cycle 3~20 it gets data
|
|
778
|
-
// vblank flag set before NMI routine.
|
|
779
|
-
// (reading at cycle 1~2 suppresses NMI, see load_register())
|
|
780
|
-
// @TODO: Safer and more appropriate approach.
|
|
781
|
-
|
|
782
|
-
if self.cycle == 20 && self.scanline == 241 {
|
|
783
|
-
if self.ppustatus.is_vblank() &&
|
|
784
|
-
self.ppuctrl.is_nmi_enabled() {
|
|
785
|
-
self.nmi_interrupted = true;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
758
|
// @TODO: check this driving IRQ counter for MMC3Mapper timing is correct
|
|
790
759
|
// @TODO: This is MMC3Mapper specific. Should this be here?
|
|
791
760
|
|
|
@@ -901,7 +870,7 @@ impl Ppu {
|
|
|
901
870
|
// the PPU scans through OAM to determine which sprites
|
|
902
871
|
// to render on the next scanline
|
|
903
872
|
|
|
904
|
-
if self.scanline >= 240 {
|
|
873
|
+
if self.scanline >= 240 && self.scanline != 261 {
|
|
905
874
|
return;
|
|
906
875
|
}
|
|
907
876
|
|
|
@@ -917,16 +886,17 @@ impl Ppu {
|
|
|
917
886
|
} else if self.cycle == 257 {
|
|
918
887
|
// Evaluate at a time at cycle 257 due to performance
|
|
919
888
|
// and simplicity so far
|
|
920
|
-
self.
|
|
889
|
+
let next_scanline = if self.scanline == 261 { 0 } else { self.scanline + 1 };
|
|
890
|
+
self.process_sprite_pixels(next_scanline, rom);
|
|
921
891
|
}
|
|
922
892
|
}
|
|
923
893
|
|
|
924
|
-
fn process_sprite_pixels(&mut self, rom: &Rom) {
|
|
894
|
+
fn process_sprite_pixels(&mut self, scanline: u16, rom: &Rom) {
|
|
925
895
|
for i in 0..self.sprite_availables.len() {
|
|
926
896
|
self.sprite_availables[i] = false;
|
|
927
897
|
}
|
|
928
898
|
|
|
929
|
-
let y =
|
|
899
|
+
let y = scanline as u8;
|
|
930
900
|
let height = self.ppuctrl.sprite_height();
|
|
931
901
|
let mut n = 0;
|
|
932
902
|
|