@tmustier/pi-nes 0.2.22 → 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 +6 -0
- package/TODO_INVENTORY.md +116 -0
- package/extensions/nes/native/nes-core/index.node +0 -0
- 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/package.json +11 -1
- package/scripts/update-vendor-nes-rust.sh +30 -0
- 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
|
@@ -106,6 +106,12 @@ Set `"renderer": "text"` if you prefer the ANSI renderer or have display issues.
|
|
|
106
106
|
- **No audio** — Sound is not currently supported
|
|
107
107
|
- **No save states** — Only battery-backed SRAM saves work
|
|
108
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
|
+
|
|
109
115
|
---
|
|
110
116
|
|
|
111
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. |
|
|
Binary file
|
|
@@ -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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmustier/pi-nes",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "NES emulator extension for pi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -13,9 +13,19 @@
|
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "https://github.com/tmustier/pi-nes.git"
|
|
15
15
|
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --import tsx --test tests/paths.test.ts tests/roms.test.ts tests/saves.test.ts tests/config.test.ts tests/input-map.test.ts",
|
|
18
|
+
"test:unit": "node --import tsx --test tests/paths.test.ts tests/roms.test.ts tests/saves.test.ts tests/config.test.ts tests/input-map.test.ts",
|
|
19
|
+
"test:core": "node --import tsx --test tests/core-smoke.test.ts",
|
|
20
|
+
"test:regression": "node --import tsx --test tests/regression.test.ts"
|
|
21
|
+
},
|
|
16
22
|
"dependencies": {
|
|
17
23
|
"pngjs": "^7.0.0"
|
|
18
24
|
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsx": "^4.19.0",
|
|
27
|
+
"typescript": "^5.5.0"
|
|
28
|
+
},
|
|
19
29
|
"peerDependencies": {
|
|
20
30
|
"@mariozechner/pi-coding-agent": "*",
|
|
21
31
|
"@mariozechner/pi-tui": "*"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
FORK_URL="https://github.com/tmustier/nes-rust.git"
|
|
5
|
+
VENDOR_DIR="extensions/nes/native/nes-core/vendor/nes_rust"
|
|
6
|
+
|
|
7
|
+
if [[ -n "$(git status --porcelain)" ]]; then
|
|
8
|
+
echo "Working tree not clean. Commit or stash changes before updating vendor." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
TMP_DIR="$(mktemp -d -t nes-rust-vendor-XXXX)"
|
|
13
|
+
cleanup() {
|
|
14
|
+
rm -rf "$TMP_DIR"
|
|
15
|
+
}
|
|
16
|
+
trap cleanup EXIT
|
|
17
|
+
|
|
18
|
+
git clone --depth 1 "$FORK_URL" "$TMP_DIR"
|
|
19
|
+
|
|
20
|
+
echo "This will replace contents of $VENDOR_DIR with $FORK_URL"
|
|
21
|
+
read -r -p "Continue? [y/N] " confirm
|
|
22
|
+
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
23
|
+
echo "Aborted."
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
rsync -a --delete --exclude ".git" "$TMP_DIR/" "$VENDOR_DIR/"
|
|
28
|
+
|
|
29
|
+
echo "Vendored from commit: $(git -C "$TMP_DIR" rev-parse HEAD)"
|
|
30
|
+
echo "Update $VENDOR_DIR/VENDOR.md with commit/tag + date + patch summary."
|
package/tests/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Tests
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# Run unit tests (no ROMs needed)
|
|
7
|
+
npm test
|
|
8
|
+
|
|
9
|
+
# Run with a specific ROM
|
|
10
|
+
NES_TEST_ROM=~/roms/nes/Zelda.nes npm run test:core
|
|
11
|
+
|
|
12
|
+
# Run regression against all ROMs in a directory
|
|
13
|
+
NES_ROM_DIR=~/roms/nes npm run test:regression
|
|
14
|
+
|
|
15
|
+
# Debug a specific game (visual ASCII output)
|
|
16
|
+
npx tsx tests/debug-game.ts ~/roms/nes/Mario.nes
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Test Structure
|
|
20
|
+
|
|
21
|
+
### Unit Tests (always run)
|
|
22
|
+
|
|
23
|
+
| File | What it tests |
|
|
24
|
+
|------|---------------|
|
|
25
|
+
| `paths.test.ts` | Path utilities (displayPath, expandHomePath, etc.) |
|
|
26
|
+
| `roms.test.ts` | ROM name parsing |
|
|
27
|
+
| `saves.test.ts` | Save path generation |
|
|
28
|
+
| `config.test.ts` | Config normalization and validation |
|
|
29
|
+
| `input-map.test.ts` | Keyboard-to-button mapping |
|
|
30
|
+
|
|
31
|
+
### Core Smoke Tests (require ROM)
|
|
32
|
+
|
|
33
|
+
Set `NES_TEST_ROM=/path/to/rom.nes` to enable.
|
|
34
|
+
|
|
35
|
+
| File | What it tests |
|
|
36
|
+
|------|---------------|
|
|
37
|
+
| `core-smoke.test.ts` | ROM loading, frame execution, freeze detection, SRAM round-trip |
|
|
38
|
+
|
|
39
|
+
### Regression Tests (require ROM directory)
|
|
40
|
+
|
|
41
|
+
Set `NES_ROM_DIR=/path/to/roms` to enable.
|
|
42
|
+
|
|
43
|
+
| File | What it tests |
|
|
44
|
+
|------|---------------|
|
|
45
|
+
| `regression.test.ts` | Scripted game tests with input sequences |
|
|
46
|
+
| `game-scripts.ts` | Game-specific input sequences (Start, move, etc.) |
|
|
47
|
+
|
|
48
|
+
### Debug Tool
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx tsx tests/debug-game.ts <rom-path>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Shows ASCII rendering of the game after running the script, useful for diagnosing rendering issues.
|
|
55
|
+
|
|
56
|
+
## CI Usage
|
|
57
|
+
|
|
58
|
+
For CI without commercial ROMs:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm run test:unit # Only pure function tests
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For local development with ROMs:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
NES_ROM_DIR=~/roms/nes npm run test:regression
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Interpreting Results
|
|
71
|
+
|
|
72
|
+
### Regression Output
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
✅ OK Dragon Quest III [scripted] (425 frames) # Loaded, ran script, frames animated
|
|
76
|
+
⚠️ FROZE Super Mario Bros [scripted] (475 frames) # Ran script but frames frozen
|
|
77
|
+
❌ ERROR Broken.nes (0 frames) # Failed to load or crashed
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### What "FROZE" Means
|
|
81
|
+
|
|
82
|
+
For **scripted** tests, "FROZE" indicates a likely bug:
|
|
83
|
+
- Background renders but sprites missing
|
|
84
|
+
- Game stuck after input sequence
|
|
85
|
+
- No animation after reaching gameplay
|
|
86
|
+
|
|
87
|
+
The scripted tests simulate actual gameplay (press Start, move character) and verify the screen animates. A frozen screen after pressing right in Mario means the sprite isn't rendering.
|
|
88
|
+
|
|
89
|
+
### Debug Output Example
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Frame Analysis (after script):
|
|
93
|
+
Non-zero pixels: 59,440 / 61,440 (96.7%) ← Background renders
|
|
94
|
+
Unique colors: 9 ← Limited palette (no sprites)
|
|
95
|
+
|
|
96
|
+
ASCII Preview:
|
|
97
|
+
[Shows level but no Mario sprite visible]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Adding Game Scripts
|
|
101
|
+
|
|
102
|
+
Edit `tests/game-scripts.ts` to add scripts for new ROMs:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
"my game": {
|
|
106
|
+
description: "Start game, verify gameplay",
|
|
107
|
+
sequence: [
|
|
108
|
+
{ type: "wait", frames: 180 }, // 3 seconds
|
|
109
|
+
{ type: "press", button: "start" }, // tap Start
|
|
110
|
+
{ type: "wait", frames: 120 }, // 2 seconds
|
|
111
|
+
{ type: "hold", button: "right", frames: 30 }, // move
|
|
112
|
+
],
|
|
113
|
+
postSequenceFrames: 60, // frames to check for animation
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Environment Variables
|
|
118
|
+
|
|
119
|
+
| Variable | Description |
|
|
120
|
+
|----------|-------------|
|
|
121
|
+
| `NES_TEST_ROM` | Path to a single ROM for core smoke tests |
|
|
122
|
+
| `NES_ROM_DIR` | Directory containing .nes files for regression tests |
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { normalizeConfig, DEFAULT_CONFIG, formatConfig } from "../extensions/nes/config.js";
|
|
4
|
+
|
|
5
|
+
describe("config", () => {
|
|
6
|
+
describe("normalizeConfig", () => {
|
|
7
|
+
test("returns defaults for empty object", () => {
|
|
8
|
+
const config = normalizeConfig({});
|
|
9
|
+
assert.strictEqual(config.enableAudio, DEFAULT_CONFIG.enableAudio);
|
|
10
|
+
assert.strictEqual(config.renderer, DEFAULT_CONFIG.renderer);
|
|
11
|
+
assert.strictEqual(config.imageQuality, DEFAULT_CONFIG.imageQuality);
|
|
12
|
+
assert.strictEqual(config.pixelScale, DEFAULT_CONFIG.pixelScale);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns defaults for null", () => {
|
|
16
|
+
const config = normalizeConfig(null);
|
|
17
|
+
assert.strictEqual(config.renderer, "image");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("returns defaults for non-object", () => {
|
|
21
|
+
const config = normalizeConfig("invalid");
|
|
22
|
+
assert.strictEqual(config.renderer, "image");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("accepts valid renderer value", () => {
|
|
26
|
+
const config = normalizeConfig({ renderer: "text" });
|
|
27
|
+
assert.strictEqual(config.renderer, "text");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("defaults invalid renderer to image", () => {
|
|
31
|
+
const config = normalizeConfig({ renderer: "invalid" });
|
|
32
|
+
assert.strictEqual(config.renderer, "image");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("accepts valid imageQuality", () => {
|
|
36
|
+
const config = normalizeConfig({ imageQuality: "high" });
|
|
37
|
+
assert.strictEqual(config.imageQuality, "high");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("defaults invalid imageQuality to balanced", () => {
|
|
41
|
+
const config = normalizeConfig({ imageQuality: "ultra" });
|
|
42
|
+
assert.strictEqual(config.imageQuality, "balanced");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("clamps pixelScale to valid range", () => {
|
|
46
|
+
assert.strictEqual(normalizeConfig({ pixelScale: 0.1 }).pixelScale, 0.5);
|
|
47
|
+
assert.strictEqual(normalizeConfig({ pixelScale: 10 }).pixelScale, 4);
|
|
48
|
+
assert.strictEqual(normalizeConfig({ pixelScale: 2 }).pixelScale, 2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("handles NaN pixelScale", () => {
|
|
52
|
+
const config = normalizeConfig({ pixelScale: NaN });
|
|
53
|
+
assert.strictEqual(config.pixelScale, DEFAULT_CONFIG.pixelScale);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("preserves valid keybindings", () => {
|
|
57
|
+
const config = normalizeConfig({
|
|
58
|
+
keybindings: { a: ["k", "l"] }
|
|
59
|
+
});
|
|
60
|
+
assert.deepStrictEqual(config.keybindings.a, ["k", "l"]);
|
|
61
|
+
// Other keys should have defaults
|
|
62
|
+
assert.ok(config.keybindings.up.length > 0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("ignores invalid keybinding values", () => {
|
|
66
|
+
const config = normalizeConfig({
|
|
67
|
+
keybindings: { a: "not-an-array" }
|
|
68
|
+
});
|
|
69
|
+
// Should fall back to default
|
|
70
|
+
assert.deepStrictEqual(config.keybindings.a, DEFAULT_CONFIG.keybindings.a);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("accepts boolean enableAudio", () => {
|
|
74
|
+
assert.strictEqual(normalizeConfig({ enableAudio: true }).enableAudio, true);
|
|
75
|
+
assert.strictEqual(normalizeConfig({ enableAudio: false }).enableAudio, false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("defaults non-boolean enableAudio", () => {
|
|
79
|
+
const config = normalizeConfig({ enableAudio: "yes" });
|
|
80
|
+
assert.strictEqual(config.enableAudio, DEFAULT_CONFIG.enableAudio);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("formatConfig", () => {
|
|
85
|
+
test("produces valid JSON", () => {
|
|
86
|
+
const json = formatConfig(DEFAULT_CONFIG);
|
|
87
|
+
const parsed = JSON.parse(json);
|
|
88
|
+
assert.strictEqual(parsed.renderer, DEFAULT_CONFIG.renderer);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("is pretty-printed", () => {
|
|
92
|
+
const json = formatConfig(DEFAULT_CONFIG);
|
|
93
|
+
assert.ok(json.includes("\n"), "Should be multi-line");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|