jsnes
CLAUDE.md6.4k starsChanged 8 months agoApache-2.0View on GitHub
JSNES is a JavaScript NES emulator with component-based architecture mirroring actual NES hardware: Main Orchestrator: nes.js - Central class that coordinates all emulation components. Accepts callback functions for frame rendering, audio output, and status updates. CPU: cpu.js - Implements 6502 processor with 64KB address space, instruction execution, and interrupt handling (NMI, IRQ, reset). PPU: ppu.js - Picture Processing Unit handles 256x240 graphics rendering, VRAM management, background/sprite rendering, and scrolling. PAPU: papu.js - Audio Processing Unit implements NES's 5 audio channels…
We split this CLAUDE.md into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 90
All code must be formatted with Prettier (enforced by test suite)
- Distinctive: line 110
$4015 bit 5: Not driven by APU; comes from the CPU data bus
- Very specific: line 172
Keep README.md up to date when changing the public API (adding/removing/renaming methods, options, or exports).
≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Style
- This codebase should be readable by somebody who's not familiar with the NES or emulation, so add more comments and documentation than you might otherwise do, and explain context.
Essential Commands
npm test- Run code formatting check and tests (required before commits)npm run build- Build distribution files (dist/jsnes.jsanddist/jsnes.min.js)npm run format- Auto-format all code (core and web) with Prettiernpm run test:watch- Run tests in watch mode for developmentnpm run typecheck- TypeScript type checking (verifies.d.tsfiles)node bench.js- Run performance benchmark (~1800 fps baseline). Run this on major changes or when you suspect a performance regression/improvement.
Code Architecture
JSNES is a JavaScript NES emulator with component-based architecture mirroring actual NES hardware:
Core Components (all in src/)
Main Orchestrator: nes.js - Central class that coordinates all emulation components. Accepts callback functions for frame rendering, audio output, and status updates.
CPU: cpu.js - Implements 6502 processor with 64KB address space, instruction execution, and interrupt handling (NMI, IRQ, reset).
PPU: ppu.js - Picture Processing Unit handles 256x240 graphics rendering, VRAM management, background/sprite rendering, and scrolling.
PAPU: papu.js - Audio Processing Unit implements NES's 5 audio channels (2 square waves, triangle, noise, DMC) with 44.1kHz/48kHz sample generation.
Memory Mappers: mappers.js - Implements cartridge memory mappers (0-180) using inheritance hierarchy. All mappers inherit from Mapper 0 and override specific banking/memory mapping behavior.
ROM Loader: rom.js - Parses iNES format ROM files, extracts PRG-ROM/CHR-ROM, and determines appropriate mapper.
Tile Renderer: tile.js - Handles 8x8 pixel tile rendering with sprite flipping (horizontal/vertical), alpha-blending priority, and scanline-based rendering.
Controller: controller.js - Button state management for 2 controllers with 8 buttons each (A, B, SELECT, START, UP, DOWN, LEFT, RIGHT) and serial strobe protocol.
Utilities: utils.js - Helper functions for state serialization (toJSON()/fromJSON()) used across CPU, PPU, PAPU, and mappers for save state support.
Key Architectural Patterns
- Event-driven design: Main NES class uses callbacks (
onFrame,onAudioSample,onStatusUpdate,onBatteryRamWrite) - Component separation: Each NES subsystem is a separate class with clear interfaces
- Inheritance for mappers: Code reuse while supporting cartridge-specific behavior
- Frame-based execution: 60 FPS timing with proper CPU cycle counting
Usage Pattern
var nes = new jsnes.NES({
onFrame: function(frameBuffer) { /* render 256x240 pixels */ },
onAudioSample: function(left, right) { /* play audio */ }
});
nes.loadROM(romData);
nes.frame(); // Execute one frame
nes.buttonDown(1, jsnes.Controller.BUTTON_A); // Handle inputTesting
Tests use node:test + node:assert/strict (Node's built-in test runner):
test/nes.spec.js- Basic initialization, ROM loading, frame generation with regression testing usingcroom.nestest ROM, frame buffer validation, error handlingtest/cpu.spec.js- Comprehensive CPU instruction testing (based on upstream wedNESday and nintengo test suites)test/mappers.spec.js- Mapper functionality tests (ROM protection, SRAM writes, RAM mirroring)test/nestest.spec.js- Full nestest.nes ROM harness with detailed error code mappings for all CPU instructions and addressing modestest/accuracycoin.spec.js- AccuracyCoin test ROM harness (134 hardware accuracy tests)
Test ROMs:
roms/croom/- Simple test ROM for automated testingroms/AccuracyCoin/- Comprehensive accuracy test ROM (134 tests covering CPU, PPU, APU behavior)roms/nestest/- CPU instruction test ROM (official + unofficial opcodes)local-roms/- Collection of ROMs for manual testing
Known AccuracyCoin failures are listed in the KNOWN_FAILURES object in test/accuracycoin.spec.js.
Remember that AccuracyCoin and nestest are DEFINITELY correct. They pass on a real NES. Don't blame the ROM for being wrong.
Build Process
Webpack configuration creates UMD modules compatible with browsers and Node.js:
- Entry point:
src/index.js(exports NES and Controller classes) - Output:
dist/jsnes.js(regular) anddist/jsnes.min.js(minified) - Includes ESLint checking and source map generation
- Library name:
jsnes(global variable in browsers) - TypeScript type definitions:
src/nes.d.tsandsrc/controller.d.tsprovide public API types - CI: GitHub Actions (
.github/workflows/ci.yaml) runs build and tests on push/PR with Node.js 22.x
Code Quality Requirements
- All code must be formatted with Prettier (enforced by test suite)
- ESLint rules are enforced during build
- Tests must pass before commits
- Frame buffer regression tests prevent rendering regressions
Documentation and reference for the NES
The nesdev wiki has tons of information about the NES. You should browse this before implementing something to understand how it works: https://www.nesdev.org/wiki/NES_reference_guide
NES Hardware Accuracy
The emulator implements several hardware-accurate behaviors verified by the AccuracyCoin test ROM.
Open Bus
The NES data bus retains the last value from any read/write. Reading from unmapped or write-only addresses returns this "open bus" value. The CPU tracks this in cpu.dataBus, updated on every load, write, push, pull, opcode fetch, and interrupt vector fetch.
- CPU open bus regions: $4018-$5FFF (unmapped expansion), $4000-$4014 (write-only APU registers)
- PPU open bus: The PPU has its own internal I/O latch (
ppu.openBusLatch), updated on every PPU register write. Write-only PPU registers ($2000, $2001, $2003, $2005, $2006) return this latch. $2002 returns status in bits 7-5 with the latch in bits 4-0. - Controller open bus: $4016/$4017 only drive bits 0-4; bits 5-7 come from the CPU data bus
- $4015 bit 5: Not driven by APU; comes from the CPU data bus
Key reference: https://www.nesdev.org/wiki/Open_bus_behavior
Dummy Reads and Writes
The 6502 performs "dummy reads" during addressing — extra bus cycles that read from intermediate/incorrect addresses while the CPU computes the final effective address. These are real bus operations visible on the bus: they update the data bus and trigger I/O side effects (e.g., reading $4015 clears interrupt flags). See https://www.nesdev.org/wiki/CPU_addressing_modes
Dummy reads occur in:
- Absolute indexed (cases 8, 9): On page crossing, reads from address with uncorrected high byte
- Zero page indexed (cases 6, 7): Reads from unindexed zero-page address while adding X/Y
- Pre-indexed indirect (case 10): Reads from pointer address before adding X
- Post-indexed indirect (case 11): Same page-crossing behavior as absolute indexed
- Stores (STA/STX/STY) and RMW instructions: Always perform the indexed dummy read, even without page crossing
RMW instructions (ASL, LSR, ROL, ROR, INC, DEC, and unofficial SLO, SRE, RLA, RRA, DCP, ISC) also perform a "dummy write" — they write the original value back to the address before writing the modified value. This is documented in the ASL implementation (case 2) in cpu.js.
PPU Catch-up
On real hardware, the CPU and PPU advance in lockstep (3 PPU dots per CPU cycle). The emulator runs CPU instructions atomically for performance, then advances the PPU afterward. This means PPU register reads mid-instruction (e.g., reading VBlank status from $2002) would see stale PPU state.
To fix this, cpu.load() and cpu.write() call _ppuCatchUp() before any PPU register access ($2000-$3FFF). This method advances the PPU by instrBusCycles * 3 dots — the number of PPU dots that should have elapsed based on how many bus operations the instruction has completed so far. The frame loop in nes.js then subtracts the already-advanced dots from the total.
See https://www.nesdev.org/wiki/Catch-up
JSR Cycle Order
The real 6502 reads JSR's target high byte after pushing the return address, making it the last bus operation before entering the target. This matters when JSR targets unmapped addresses — the data bus value on entry is the high byte of the target.
APU Frame IRQ
The frame interrupt flag (frameIrqActive) is set in step 4 of the 4-step frame counter sequence, but ONLY when the IRQ inhibit flag is clear (frameIrqEnabled is true). Writing $4017 with bit 6 set prevents the flag from being set entirely — it doesn't just suppress the IRQ, it prevents the flag from appearing in $4015 bit 6. See https://www.nesdev.org/wiki/APU_Frame_Counter
DMC DMA Bus Hijacking
The DMC channel's DMA transfer can hijack CPU bus cycles, which interacts with certain unofficial opcodes (SHx family). During DMA, the value on the data bus affects these instructions' output since they AND the high byte of the target address with the accumulator/register. The implementation handles this interaction to match real hardware behavior.
Unofficial Opcodes
The CPU implements the full set of unofficial 6502 opcodes needed to pass nestest: ALR, ANC, ARR, AXS, LAX, SAX, DCP, ISC, RLA, RRA, SLO, SRE, SKB, IGN, SHA, SHX, SHY, SHS, LAE, ANE, LXA, and DUMMY (cycle halt). The SHx/SHA family have complex bus interactions documented in the code.
State Serialization
Full save state support via toJSON()/fromJSON() on the NES class, which serializes/deserializes CPU, PPU, PAPU, and mapper state. Battery-backed SRAM is surfaced via the onBatteryRamWrite() callback.
Zapper (Light Gun) Support
The NES class exposes zapperMove(x, y), zapperFireDown(), and zapperFireUp() for light gun input. The PPU's isPixelWhite() method handles light detection at the zapper's position.
Error Recovery
The NES has a crashed flag that prevents further frame execution after exceptions. Users must call reset() or loadROM() to recover.
Development Notes
- The
example/directory contains a basic web implementation but is noted as flawed compared to jsnes-web - For web integration, prefer jsnes-web repository over the local example
- ROMs should be loaded as binary strings or byte arrays
- Timing management is the responsibility of the integrating application (60 FPS)
- Controller input uses simple button state management with 8 buttons per controller
- Liberally document things with comments, particularly if it's not obvious why something is like it is. Link to documentation and references (e.g. nesdev wiki pages) where it makes sense.
- Update CLAUDE.md if you make mistakes and you want to remind yourself in the future not to do something. Only put it in CLAUDE.md if it's high-level architecture and things that apply to several parts of the codebase. If it's about a particular part of the code or tests, just add it as comments.
- Keep
README.mdup to date when changing the public API (adding/removing/renaming methods, options, or exports). The README contains usage examples and an API reference for both theNESandBrowserclasses.
Trending
More talk lately.about the whole repository
Who's talking about bfirsh/jsnes
- A Hacker News story“A JavaScript NES Emulator”Hacker News · Sep 27, 2011 · 32 points
- A Hacker News story“JSNES: A JavaScript NES emulator”Hacker News · Feb 19, 2018 · 4 points
- A Hacker News story“A JavaScript NES emulator”Hacker News · Jan 29, 2018 · 2 points
- bfirsh/jsnes-web“Support saving game state”GitHub · Sep 11, 2017 · 1 reactions
- gamesexplained/gamesexplained“kit: Add the NES as a platform, with jsnes as its emulator”GitHub · Oct 26, 2026
The biggest 5 of 40 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 40
Talked about on GitHub and Hacker News (40 times).
On GitHub · 37
- Sep 11, 2017Support saving game state — bfirsh/jsnes-web1 reaction · 6 comments
- Oct 26, 2026kit: Add the NES as a platform, with jsnes as its emulator — gamesexplained/gamesexplained0 reactions · 2 comments
- Mar 16, 2026Fix NES ROM loading: jsnes directory, canvas resize, script-loader retry — micahkamei23-star/retro-emulator-game0 reactions · 1 comment
- Mar 3, 2026Bump jsnes from 1.2.1 to 2.0.0 — Ferine/css-nes0 reactions · 1 comment
- Feb 14, 2026Bump jsnes from `dcd2f8a` to `a8d4fbd` — bfirsh/jsnes-web0 reactions · 1 comment
32 more on GitHub
- Jan 10, 2026build(deps): bump jsnes from `dab5180` to `dcd2f8a` — caiwantai/NES-GAME0 reactions
- Jan 3, 2026Bump jsnes from `dab5180` to `795eec3` — caiwantai/NES-GAME0 reactions · 1 comment
- Aug 18, 2025Bump jsnes from `dab5180` to `70e2c5c` — bfirsh/jsnes-web0 reactions · 1 comment
- Aug 11, 2025Bump jsnes from `15924fd` to `dab5180` — bfirsh/jsnes-web0 reactions · 1 comment
- Apr 13, 2022NES javascript into blcoklet — ArcBlock/dappideas0 reactions · 2 comments
- Dec 13, 2021Bump jsnes from `667d169` to `d8021d0` — TheLandolorien/jsnes-web0 reactions
- Sep 27, 2021Bump jsnes from `667d169` to `c6cceb2` — TheLandolorien/jsnes-web0 reactions · 1 comment
- Aug 30, 2021Bump jsnes from `667d169` to `9c40bd9` — TheLandolorien/jsnes-web0 reactions · 1 comment
- Aug 24, 2021Bump jsnes from `ee248db` to `9c40bd9` — bfirsh/jsnes-web0 reactions
- Dec 29, 2020Bump jsnes from `ee248db` to `667d169` — bfirsh/jsnes-web0 reactions · 1 comment
- Aug 24, 2020Bump jsnes from `1aac0c2` to `ee248db` — bfirsh/jsnes-web0 reactions
- Aug 10, 2020Bump jsnes from `1aac0c2` to `9e16d6e` — bfirsh/jsnes-web0 reactions · 1 comment
- Aug 3, 2020Bump jsnes from `1cff0db` to `1aac0c2` — bfirsh/jsnes-web0 reactions
- May 4, 2020Bump jsnes from `96d028e` to `1cff0db` — bfirsh/jsnes-web0 reactions
- Apr 6, 2020Bump jsnes from `0ae34dd` to `96d028e` — bfirsh/jsnes-web0 reactions
- Jul 1, 2019ANN that plays NES games — lander0s/machine-learning-in-javascript0 reactions
- Jun 25, 2019Dragging ROM only works on particular parts of the page — bfirsh/jsnes-web0 reactions · 1 comment
- May 7, 2019Emulator speed — bfirsh/jsnes-web0 reactions · 2 comments
- Dec 8, 2018Refactor react components so JSNES can run a single ROM standalone — bfirsh/jsnes-web0 reactions · 1 comment
- Feb 6, 2018[New] Add jsnes w/ npm auto-update via single package.json — cdnjs/cdnjs0 reactions · 2 comments
- Feb 6, 2018Add jsnes w/ npm auto-update via single package.json — cdnjs/cdnjs0 reactions
- Feb 4, 2018Add jsnes w/ npm auto-update via single package.json — cdnjs/cdnjs0 reactions · 1 comment
- Feb 4, 2018Add jsnes w/ npm auto-update via single package.json — cdnjs/cdnjs0 reactions
- Jan 31, 2018Document build process and where the files to upload end up — bfirsh/jsnes-web0 reactions
- Jan 26, 2018[Request] Add jsnes — cdnjs/cdnjs0 reactions · 4 comments
- Jan 26, 2018bfirsh/jsnes — zhoukekestar/stars0 reactions
- Nov 3, 2017Sound is distorted. — workhorsy/SaltyNES0 reactions · 2 comments
- Sep 25, 2017Audio does not work in Mobile Safari, iOS WKWebView, and Android Browser — bfirsh/jsnes-web0 reactions · 2 comments
- Sep 11, 2017On-screen control for touch screens — bfirsh/jsnes-web0 reactions · 3 comments
- Sep 11, 2017Support game pads — bfirsh/jsnes-web0 reactions · 1 comment
- Sep 11, 2017Support loading ROMs from file — bfirsh/jsnes-web0 reactions · 1 comment
- Feb 23, 2015List of projects missing a bower.json file — bower/registry0 reactions · 4 comments
On Hacker News · 3
- Sep 27, 2011A JavaScript NES Emulator32 points · 8 comments
- Feb 19, 2018JSNES: A JavaScript NES emulator4 points
- Jan 29, 2018A JavaScript NES emulator2 points · 1 comment
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

