├── .gitattributes ├── .gitignore ├── README.md ├── ect-0.8.3.exe ├── index.html ├── roms ├── croom │ ├── README.html │ └── croom.nes ├── lj65 │ ├── README.txt │ └── lj65.nes ├── mapper30 │ ├── 1.nes │ ├── H.nes │ └── V.nes ├── nestest │ ├── nestest.log.txt │ └── nestest.nes └── others │ └── hb1.nes └── src ├── 6502.js ├── controller.js ├── cpu.js ├── mappers.js ├── memory.js ├── nes.js ├── papu.js ├── ppu.js └── rom.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | roms/commercial 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSNES lite 2 | === 3 | 4 | Tiny NES emulator in JS inspired by JSNES (https://github.com/bfirsh/jsnes + https://github.com/bfirsh/jsnes-web) 5 | 6 | Backstory (Twitter thread): https://twitter.com/MaximeEuziere/status/1316455403274858501 7 | 8 | DEMO: https://xem.github.io/jsnes-lite -------------------------------------------------------------------------------- /ect-0.8.3.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xem/jsnes-lite/923d45e04483b7655dbdd91773f74de46c38c661/ect-0.8.3.exe -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |  2 | JSNES lite 3 |

JSNES-lite

4 |

Micro NES emulator. Supported mappers: 0, 2, 30. 5 |

6 | 7 |

Gamepad 1: arrow keys + X + C + Start + Esc 8 | 9 |

10 | ROM: 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

Controls: 21 |

22 |

23 | VRAM visualizer 24 | 25 |
26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /roms/croom/README.html: -------------------------------------------------------------------------------- 1 | 2 | Concentration Room 3 | 4 | 5 |
6 |

Concentration Room

7 | 17 | 18 |
19 | 20 |

Overview

21 | 22 |

23 | An accident at the biochemical lab has released a neurotoxin, 24 | and you've been quarantined after exposure. Maintain your 25 | sanity by playing a card-matching game. 26 |

27 | The table is littered with 10, 20, 36, 52, or 72 face-down cards. 28 | Flip two cards, and if they show the same emblem, you keep them. 29 | If they don't, flip them back. 30 |

31 | 32 |

System Requirements

33 |

34 | Concentration Room is designed for your Nintendo Entertainment System. This version is an NROM-128 (16 KiB PRG, 8 KiB CHR), and it has been tested on a PowerPak. It also works in PC-based emulators such as Nestopia and FCE Ultra. 35 |

36 | 37 |

Modes

38 |
39 |
1 Player Story
40 | Play solitaire to start to work the toxin out of your system. Then defeat other contaminated technicians and children one on one. 41 |
1 Player Solitaire
42 | Select a difficulty level, then try to clear the table without having to turn back more than 99 non-matching pairs. 43 |
2 Players
44 | Two players take turns turning over cards. They can pass one controller back and forth or use one controller each. If a pair doesn't match, the other player presses the A and B Buttons and takes a turn. The first player to take half the pairs wins. 45 |
Vs. CPU
46 | Like 2 Players, except the second player is controlled by the NES. 47 |
48 | 49 |

FAQ (Fully Anticipated Questions)

50 |
51 |
How long have you been working on this?
52 | This is actually my third try. The logo and the earliest background sketch date back to 2000. It got held up because I lacked artistic skill on the 16x16 pixel canvas. The second try in 2007 finalized the appearance of the game, and I did some work on the "emblem designer" that will show up in a future release. In late November 2009, I discovered Dian Shi Mali, a gambling simulator for the Famicom (Asian version of the NES) that also uses 16x16 pixel emblems. After a few hours of pushing Start to rich, I was inspired to create a set of 36 emblems. By then, I was ready to code most of the game in spare time during December 2009. 53 |
Why are you still making games that don't scroll? You're better than that, as I saw in the President video.
54 | I saw it as something simple that I could finish fairly quickly in order to push falling block games off the front page of my web site. 55 |
GameTek already made two other Concentration games on the NES. Why did you make this one?
56 | The controls in I Can Remember nor Classic Concentration are clunky. Neither of them features a full 72-card deck. And of course, they're not free software. 57 |
In vs. modes, why end the game at half the cards matched instead of one more than half?
58 | Pairs early in a game require more skill to clear, and the last pair requires absolutely no skill. For example, a 20-card game tied at 4-4 will always end up 6-4. And at 5-3, the player in the lead likely got more early matches. So if we award no points for the last pair, the first player to reach half always wins. 59 |
What's that font?
60 | The font in the game's logo is called Wasted Collection. The font in Multiboot Menu was based on it. The monospace font for menu text originally appeared in the "Who's Cuter" demo and is based on Apple Chicago by Susan Kare. (Another fun font is on this page.) 61 |
Are you a Nazi?
62 | No, and that's why this game is called Concentration Room, not Concentration Camp. 63 |
64 |

Legal

65 |

66 | Copyright © 2010 Damian Yerrick <croom@pineight.com> 67 |

68 | Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. 69 |

70 | The accompanying program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License, version 3 or later. As a special exception, you may copy and distribute exact copies of the program, as published by Damian Yerrick, in iNES or UNIF executable form without source code. 71 |

72 | This product is not sponsored or endorsed by Nintendo, Ravensburger, Hasbro, Mattel, Quaker Oats, NBC Universal, GameTek, or Apple. 73 |

74 |
75 | 76 | -------------------------------------------------------------------------------- /roms/croom/croom.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xem/jsnes-lite/923d45e04483b7655dbdd91773f74de46c38c661/roms/croom/croom.nes -------------------------------------------------------------------------------- /roms/lj65/README.txt: -------------------------------------------------------------------------------- 1 | _ _ __ ___ 2 | | | (_) / / / __| 3 | | | _ / /_ | /__ 4 | | | | | | _ \ |___ \ 5 | | |_ | | | (_) | .___) | 6 | \__|_| | \___/ \___/ 7 | |__/ 8 | 9 | LJ65 10 | an NES game 11 | by Damian Yerrick 12 | 13 | See the legal section below. 14 | 15 | _____________________________________________________________________ 16 | Introduction 17 | 18 | LJ65 is an action puzzle game for NES comparable to the popular 19 | game Tetris(R), except distributed as free software and with more 20 | responsive movement controls. 21 | 22 | _____________________________________________________________________ 23 | Installing 24 | 25 | LJ65 is designed to run on Nintendo Entertainment System (called 26 | Family Computer in Japan) and accurate NES emulators. It is 27 | distributed as source code and an iNES format binary, using mapper 28 | 0 (NROM). Separate binaries for NTSC and PAL systems are provided. 29 | 30 | This program has been tested on NES using a PowerPak. It also works 31 | on the current versions of Nintendulator, Nestopia, and FCE Ultra. 32 | (Do not use the outdated Nesticle emulator anymore.) 33 | 34 | To run LJ65 on an NES without buying a PowerPak, you'll need to 35 | solder together an NES cartridge with at least 16 KB of PRG space 36 | and 4 KB of CHR space. A modded NROM-128 or CNROM board should be 37 | fine. Chris Covell has put together instructions on how to replace 38 | NES Game Paks' mask ROM chips with writable EEPROMs. 39 | http://www.zyx.com/chrisc/solarwarscart.html 40 | 41 | To build LJ65 from source code, you will need 42 | * CC65 (from http://www.cc65.org/ but you don't need the 43 | non-free C compiler) 44 | * GNU Make and Coreutils (included with most Linux distributions; 45 | Windows users can use MSYS from http://www.devkitpro.org/) 46 | 47 | Modify the makefile to point to where you have CC65 installed. 48 | Then run make. (Windows users can run mk.bat instead, which runs 49 | make in the correct folder.) On a desktop PC from late 2000 with 50 | a Pentium III 866 MHz, recompiling the whole thing takes about one 51 | second. To build some data conversion tools, you'll need a GNU C 52 | compiler such as MinGW; I have included Windows binaries of the 53 | conversion tools for those who want to quickly get into hacking 54 | on LJ65. 55 | 56 | _____________________________________________________________________ 57 | Game controls 58 | 59 | Title screen: 60 | Start: Show playfields. 61 | Game over: 62 | A+B: Join game. 63 | Menu: 64 | Control Pad up, down: Move cursor. 65 | Control Pad left, right: Change option at cursor. 66 | A: Start game. 67 | Game: 68 | Control Pad left, right, down: Move piece. 69 | Control Pad up: Move piece to floor. 70 | Control Pad up, down once landed: Lock piece into place. 71 | A: Rotate piece clockwise. 72 | B: Rotate piece anticlockwise. 73 | Start: Pause game. 74 | 75 | _____________________________________________________________________ 76 | Play 77 | 78 | At first, press Start to skip past each of the informational screens. 79 | Then press Start at the title screen to display the playfields. 80 | At this point, either player can press the A and B buttons at the 81 | same time to begin playing. 82 | 83 | The pieces in LJ65 are called tetrominoes. (The word comes from 84 | tetra-, a Greek prefix meaning four, and -omino, as in domino or 85 | pentomino.) Each of the seven tetrominoes is made of four square 86 | blocks and named after a letter of the Latin alphabet that it 87 | resembles: 88 | _ _ ___ ___ _ ___ 89 | _______ | |___ ___| | | | _| _| _| |_ |_ |_ 90 | |_______| |_____| |_____| |___| |___| |_____| |___| 91 | I J L O S T Z 92 | 93 | When you start the game, a tetromino will begin to fall slowly into 94 | the bin. You can move it with the Control Pad and rotate it with 95 | the A or B button. 96 | 97 | The goal of LJ65 is to make complete horizontal lines by 98 | packing the pieces into the bin with no holes. If you complete 99 | a line, everything above it will move down a row. If you complete 100 | more than one line with a piece, you get more points. 101 | 102 | As you play, the pieces will gradually fall faster, making the game 103 | more difficult. At some point, the pieces will fall so fast that 104 | they appear immediately at the bottom row of the playfield. If you 105 | fill the bin to the top, to the point where more pieces cannot enter, 106 | you "top out" and the game ends. 107 | 108 | If you have an overhang in the blocks, you can slide another 109 | piece under it by holding Left or Right as the new piece passes 110 | by the overhang: 111 | _ 112 | | | 113 | _| | 114 | |___| 115 | _ _ _ _ _ 116 | _| | => _| | | | => _| | | 117 | | _| | _|_| | | _| | 118 | |_| |_| |___| |_|___| 119 | 120 | Or in some cases, you can rotate pieces into very tight spaces: 121 | _ 122 | _| | 123 | |_ | 124 | |_| 125 | _ ___ _ _ ___ _ ___ 126 | | | |_ | => | |_| |_ | => | |___|_ | 127 | | |_ _| | | |_ |_| | | |_ _| | 128 | |___| |___| |___|_|___| |___|_|___| 129 | 130 | _____________________________________________________________________ 131 | Rotation systems 132 | 133 | LJ65 supports two rotation systems, which it calls "Center" and 134 | "Bottom". Center implements rules more familiar to Western players, 135 | while Bottom pleases fans of the Japanese arcade tradition. 136 | 137 | In Center, pieces start out with their flat side down, and they 138 | rotate around the center of an imaginary 3x3 or 4x4 cell bounding 139 | box. If this is blocked, try one square to the right, one square to 140 | the left, and finally one square up. 141 | Up locks a piece into place immediately, and down waits for another 142 | press of up or down before locking the piece. 143 | After a piece locks, the next one comes out immediately, but after 144 | the pieces have sped up enough, the next piece waits a bit. 145 | Colors match the so-called Guideline: I is turquoise. 146 | 147 | . []. . []. . . . . []. . [][] . []. . . . []. . 148 | [][][] . [][] [][][] [][]. [][]. . [][] . [][] [][]. 149 | . . . . []. . []. . []. . . . . . [] [][]. . []. 150 | Figure: T and S rotation in Center 151 | 152 | In Bottom, the J, L, S, T, and Z pieces start out with their flat 153 | side up, and they rotate to stay in contact with the bottom of an 154 | imaginary 3x3 cell box. S and Z pieces also keep a block in the 155 | bottom center of this box. If this is blocked by a wall or a block 156 | outside the piece's central column, then try one square to the right, 157 | one square to the left, and finally (in the case of T) one square up. 158 | Down locks on contact, and up waits for another press of up or down 159 | to lock. After a piece locks, the next one waits a bit to come out. 160 | Colors match those from a game with a monkey: I is red. 161 | 162 | . . . . []. . . . . []. . . . []. . . . . []. . 163 | [][][] [][]. . []. . [][] . [][] [][]. . [][] [][]. 164 | . []. . []. [][][] . []. [][]. . []. [][]. . []. 165 | Figure: T and S rotation in Bottom 166 | 167 | _____________________________________________________________________ 168 | Scoring 169 | 170 | Use up or down on the Control Pad to drop pieces, and you'll get 171 | one point per row that the piece moves down. 172 | 173 | You also get points for clearing lines. Clearing more lines 174 | with a single piece is worth more points: 175 | 176 | SINGLE (1 line with any piece) 1 * 1 * 100 = 100 points 177 | DOUBLE (2 lines with any piece) 2 * 2 * 100 = 400 points 178 | TRIPLE (3 lines with I, J, or L) 3 * 3 * 100 = 900 points 179 | HOME RUN (4 lines with I only) 4 * 4 * 100 = 1600 points 180 | 181 | Making lines with consecutive pieces is called a combo and is 182 | worth even more points. In general, the score for a line clear 183 | is the number of lines cleared with this piece, times the number 184 | of lines cleared so far in this combo, times 100. For example, 185 | a double-triple-single combo is worth a total of 2300 points: 186 | 187 | 2 lines 2 * 2 * 100 = 200 points 188 | 3 lines 3 * 5 * 100 = 1500 points 189 | 1 line 1 * 6 * 100 = 600 points 190 | 191 | When you start clearing lines, the game shows how many lines you 192 | made in this combo. If you leave a 2-block-wide hole at the side 193 | of the bin, you might manage to make a combo of 12 lines or more. 194 | But then you have to weigh this against keeping your stack low 195 | and earning more drop bonus. 196 | 197 | There are some grandmasters who can get millions of points in 198 | some puzzle games. There exists a known corner case in this 199 | game's score computation, and scoring is expected to fail beyond 200 | 6,553,000 points. 201 | 202 | If two players are playing, and you have GARBAGE turned on in the 203 | menu, and you complete more than one line with a piece, the other 204 | player's field rises by one or more rows: 205 | 206 | DOUBLE: 1 line 207 | TRIPLE: 2 lines 208 | HOME RUN: 4 lines 209 | 210 | This is not affected by combos. 211 | 212 | _____________________________________________________________________ 213 | Keypress codes 214 | 215 | Some of the lesser-used features of the game are hidden so that 216 | players interested in the most common features don't become confused. 217 | 218 | At title screen: 219 | * B + Left hides the ghost piece. 220 | 221 | _____________________________________________________________________ 222 | Questions 223 | 224 | Q: Isn't this a copy of Tetris? 225 | 226 | Yes, in part, but we don't believe it infringes Tetris Holding's 227 | copyright. It was developed by people who had not read the source 228 | code of Tetris. We disagree with Tetris Holding's claim of broad 229 | patent-like rights over the game. Any similarity between LJ65 and 230 | Tetris is a consequence of common methods of operation, which are 231 | excluded from U.S. copyright (17 USC 102(b)). 232 | 233 | Q: Where's (feature that has appeared in another game)? 234 | 235 | If it's mentioned in the "future" list at the bottom of CHANGES.txt, 236 | I know about it, and you may see some of those issues resolved in 237 | the next version. Otherwise, I'd be glad to take suggestions, 238 | provided that they aren't "network play with no lag" or "make the 239 | game just like that Japanese game I saw on YouTube". 240 | 241 | Q: Why aren't the blocks square on my TV? 242 | 243 | In NTSC, a square pixel is 7/24 of a color subcarrier period wide 244 | in 480i mode or 7/12 of a period in the so-called "240p" mode. 245 | But like the video chipsets in most 8-bit and 16-bit computing 246 | platforms, the NES PPU generates pixels that are not square: 247 | 8/12 of a period instead of 7/12. Games for PC, Apple II, or any 248 | other platform with frame buffer video could correct for this by 249 | drawing differently sized tiles, but games for NES are limited to 250 | an 8x8 pixel tile grid. PAL video and widescreen televisions make 251 | the problem even more pronounced. 252 | 253 | Q: Why do some pieces change color subtly when they land? 254 | 255 | The NES's tile size is 8x8 pixels, but the "attribute table" 256 | assigns palettes to 16x16 pixel areas, or clusters of 2x2 tiles. 257 | Only three colors plus the backdrop color can appear in each 258 | color area. So the game approximates the color of each piece as a 259 | combination of blue, orange, and green throughout the screen. 260 | 261 | The MMC5 mapper has ExGrafix, which allows 8x8 pixel color areas. 262 | But the only source of MMC5 hardware is used copies of Castlevania 263 | III: Dracula's Curse and Koei's war sims, unlike the discrete mapper 264 | boards that retrousb.com sells. 265 | 266 | Q: Who is the fellow on How to Play, and where are his legs? 267 | 268 | Who are you, and where is your tail? ;-) 269 | 270 | _____________________________________________________________________ 271 | Credits 272 | 273 | Program and graphics by Damian Yerrick 274 | Original game design by Alexey Pajitnov 275 | NES assembler toolchain by Ullrich von Bassewitz 276 | NES emulators by Xodnizel, Martin Freij, and Quietust 277 | NES documentation by contributors to http://nesdevwiki.org/ 278 | 279 | Music: 280 | TEMP is "Tetris New Melody (OCRemoved)" by Am.Fm.GM 281 | K.231 is "Leck mich im Arsch" by Wolfgang A. Mozart 282 | 283 | _____________________________________________________________________ 284 | Legal 285 | 286 | Copyright (c) 2009 Damian Yerrick 287 | 288 | This manual is under the following license: 289 | 290 | This work is provided 'as-is', without any express or implied 291 | warranty. In no event will the authors be held liable for any 292 | damages arising from the use of this work. 293 | 294 | Permission is granted to anyone to use this work for any 295 | purpose, including commercial applications, and to alter it and 296 | redistribute it freely, subject to the following restrictions: 297 | 298 | 1. The origin of this work must not be misrepresented; you 299 | must not claim that you wrote the original work. If you use 300 | this work in a product, an acknowledgment in the product 301 | documentation would be appreciated but is not required. 302 | 2. Altered source versions must be plainly marked as such, 303 | and must not be misrepresented as being the original work. 304 | 3. This notice may not be removed or altered from any 305 | source distribution. 306 | 307 | The term "source" refers to the preferred form of a work for making 308 | changes to it. 309 | 310 | The LJ65 software described by this manual is distributed under 311 | the GNU General Public License, version 2 or later, with ABSOLUTELY 312 | NO WARRANTY. See GPL.txt for details. 313 | 314 | LJ65 is not a Tetris product and is not endorsed by Tetris Holding. 315 | -------------------------------------------------------------------------------- /roms/lj65/lj65.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xem/jsnes-lite/923d45e04483b7655dbdd91773f74de46c38c661/roms/lj65/lj65.nes -------------------------------------------------------------------------------- /roms/mapper30/1.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xem/jsnes-lite/923d45e04483b7655dbdd91773f74de46c38c661/roms/mapper30/1.nes -------------------------------------------------------------------------------- /roms/mapper30/H.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xem/jsnes-lite/923d45e04483b7655dbdd91773f74de46c38c661/roms/mapper30/H.nes -------------------------------------------------------------------------------- /roms/mapper30/V.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xem/jsnes-lite/923d45e04483b7655dbdd91773f74de46c38c661/roms/mapper30/V.nes -------------------------------------------------------------------------------- /roms/nestest/nestest.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xem/jsnes-lite/923d45e04483b7655dbdd91773f74de46c38c661/roms/nestest/nestest.nes -------------------------------------------------------------------------------- /roms/others/hb1.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xem/jsnes-lite/923d45e04483b7655dbdd91773f74de46c38c661/roms/others/hb1.nes -------------------------------------------------------------------------------- /src/6502.js: -------------------------------------------------------------------------------- 1 | // mini 6502 CPU simulator 2 | // ======================= 3 | 4 | // Globals 5 | // ------- 6 | 7 | // Registers 8 | A = // accumulator 9 | X = // X 10 | Y = // Y 11 | S = 0, // stack pointer (also called SP) 12 | PC = 0x8000, // program counter (address of next instruction) 13 | P = 0x24, // status register (flags on bytes 0-7: C=0, Z=0, I=1, D=0, B=0, 1, V=0, N=0) 14 | 15 | // Other globals 16 | t = // temp var 17 | o = // opcode value 18 | a = // operand address 19 | p = // operand value 20 | c = 0, // cycle counter 21 | 22 | // Helpers 23 | // ------- 24 | 25 | // Read/write a byte in memory. Costs 1 cycle 26 | // These functions handle mirrors, persistent save slots, and Mapper-specific features like bank switches 27 | r = v => (c++, memory_read(v)), 28 | w = (v, w) => (c++, memory_write(v, w)), 29 | 30 | // Update N and Z status flags: 31 | // - The value v is clamped on 8 bits and returned 32 | // - The Zero flag (bit 1 of P) is set if v is zero, otherwise it's cleared 33 | // - The Negative flag (bit 7 of P) is set if byte 7 of v is 1, otherwise it's cleared 34 | F = v => ( 35 | Z = (v &= 255) < 1, 36 | N = v >> 7, 37 | v 38 | ), 39 | 40 | // Update the flags values according to the status register P 41 | f = v => ( 42 | C = v & 1, 43 | Z = (v>>1) & 1, 44 | I = (v>>2) & 1, 45 | D = (v>>3) & 1, 46 | B = (v>>4) & 1, 47 | V = (v>>6) & 1, 48 | N = v>>7 49 | ), 50 | 51 | // Set all flags on load 52 | f(P = 0x24), 53 | 54 | // Push on Stack 55 | // Write at address $100 + S, decrement S, wrap it between $00 and $FF 56 | h = v => ( 57 | w(256 + S--, v), 58 | S &= 255 59 | ), 60 | 61 | // Pull from stack 62 | // Increment S, wrap it between $00 and $FF, read at address $100 + S 63 | g = v => r(256 + (S = (255 & (S+1)))), 64 | 65 | // Instructions 66 | // ============ 67 | 68 | // The code below creates a function for each valid opcode supported by the CPU. 69 | // When a function is called: 70 | // - PC represents the current opcode's address 71 | // - o is the opcode's value 72 | // - a equals PC+1 73 | // - p is the value stored at the address PC+1 74 | // - c (the cycle counter) equals 2 because two memory reads have already been done (o and p) 75 | O = [...Array(255)].map((t,o) => 76 | Function( 77 | 78 | ( 79 | 80 | // Addressing modes 81 | // ---------------- 82 | 83 | // Some opcodes require an address in memory 84 | // This address can be computed in 11 different ways 85 | // The 10 main ones are implemented here, the 11th is included in the last instruction (JMP ind) 86 | // The order and implementations below are optimized for a better gzip compression 87 | 88 | // "0": Immediate: 89 | // The target address is PC+1, already stored in a 90 | // Opcode size: 2 bytes 91 | // Cycles total: 2 92 | // Cycles addr.: -1 (1 cycle is removed because the first p fetch is redundant, the instruction has to read it again) 93 | // Cycles opc. : 1 94 | "c--,PC++;" 95 | 96 | // "1": Relative: 97 | // (only used for branching) 98 | // The target address (between PC-128 and PC+127) = PC + signed offset stored in p 99 | // Opcode size: 2 bytes 100 | // Cycles total: 2 (no branch) / 3 (branch on same page) / 4 (branch on another page) 101 | // Cycles addr.: 0 102 | // Cycles opc. : 0-2 103 | + "a=a+p-256*(p>>7),PC++;" 104 | 105 | // "2": Indexed indirect X 106 | // The target address is absolute and stored at a zero page address which is stored at PC + 1 + X 107 | // Opcode size: 2 bytes 108 | // Cycles total: 6 (read or write) 109 | // Cycles addr.: 3 110 | // Cycles opc. : 1 111 | + "a=r(p+X&255)+256*r(p+X+1&255),PC++,c++;" 112 | 113 | // "3": Indirect indexed Y 114 | // The target address is absolute and stored at a zero page address which is stored at PC+1, then Y is added to it 115 | // Opcode size: 2 bytes 116 | // Cycles total: 5* (read) / 6 (write) 117 | // Cycles addr.: 2-3 118 | // Cycles opc. : 0-1 119 | // * Cross-page read (if address and address + Y are on different pages) costs 1 extra cycle 120 | + "a=r(p)+256*r(p+1&255)+Y,c+=a-Y>>8>8||o>>4==9,PC++;" 121 | 122 | // "4": Zero page X 123 | // The target address is equal to zero page address (stored at PC+1) + X, wrapping between $00 and $FF 124 | // Opcode size: 2 bytes 125 | // Cycles total: 3 (BIT) / 4 (read or write) / 6 (read + write) 126 | // Cycles addr.: 1 127 | // Cycles opc. : 0-2 128 | + "a=r(a)+X&255,PC++;" 129 | 130 | // "5": Zero page Y 131 | // The target address is equal to zero page address (stored at PC+1) + Y, wrapping between $00 and $FF 132 | // Opcode size: 2 bytes 133 | // Cycles total: 4 (read or write) 134 | // Cycles addr.: 1 135 | // Cycles opc. : 1 136 | + "a=r(a)+Y&255,PC++;" 137 | 138 | // "6": Zero page 139 | // The target address (between $00 and $FF) is stored in p 140 | // Opcode size: 2 bytes 141 | // Cycles total: 3 (read or write) / 5 (read + write) 142 | // Cycles addr.: 0 143 | // Cycles opc. : 1-3 144 | + "a=p,PC++;" 145 | 146 | // "7": Absolute 147 | // The target address is stored at PC+1 (low byte) and PC+2 (high byte) 148 | // Opcode size: 3 bytes 149 | // Cycles total: 3 (JMP) / 4 (read or write) / 6 (read + write or JSR) 150 | // Cycles addr.: 1 151 | // Cycles opc. : 0-3 152 | + "a=p+256*r(PC+=2);" 153 | 154 | // "8": Absolute Y 155 | // The target address is equal to absolute address (stored at PC+1 and PC+2) + Y 156 | // Opcode size: 3 bytes 157 | // Cycles total: 4* (read) / 5 (write) 158 | // Cycles addr.: 1-2 159 | // Cycles opc. : 0-2 160 | // * Cross-page read (if address and address + Y are on different pages) costs 1 extra cycle 161 | + "t=p+256*r(PC+=2),c+=t>>8>8||o>>4==9,a=t+Y;" 162 | 163 | // "9": Absolute X 164 | // The target address is equal to absolute address (stored at PC+1 and PC+2) + X 165 | // Opcode size: 3 bytes 166 | // Cycles total: 4* (read) / 5 (write) / 7 (read + write) 167 | // Cycles addr.: 1-2 168 | // Cycles opc. : 0-4 169 | // * Cross-page read (if address and address + X are on different pages) costs 1 extra cycle 170 | + "t=p+256*r(PC+=2),c+=t>>8>8||o>>4==9||(15&o)>13,a=t+X" 171 | 172 | // "Z": implicit or Accumulator 173 | // The target is either a flag or a CPU register (no need to compute an address) 174 | // (When a "Z" is read, the generated JavaScript code will just contain "undefined;") 175 | // Opcode size: 1 byte (no need to increment PC) 176 | // Cycles total: 2-7 177 | // Cycles addr.: 0 178 | // Cycles opc. : 0-5 179 | + "" 180 | 181 | // Make an array from this string 182 | ).split(";") 183 | 184 | // Fetch the right addressing mode for the current opcode (ignore illegal opcode where o % 4 == 3): 185 | // (The string below is optomized for compression: the illegal opcodes are assigned characters that create extra repetitions) 186 | [ 187 | ( 188 | "020666Z0Z77713Z444Z8Z999" 189 | +"720666Z0Z77713Z444Z8Z999" 190 | +"Z20666Z0Z77713Z444Z8Z999" 191 | +"Z20666Z0Z77713Z444Z8Z999" 192 | +"020666Z0Z77713Z445Z8Z998" 193 | +"020666Z0Z77713Z445Z8Z998" 194 | +"020666Z0Z77713Z444Z8Z999" 195 | +"020666Z0Z77713Z444Z8Z999" 196 | )[o-(o>>2)] 197 | ] 198 | 199 | // Separator 200 | + ";" 201 | 202 | // Instructions 203 | // ------------ 204 | 205 | // There are 56 official instructions, performing operations in memory and/or in the registers 206 | // Some instructions use extra cycles: 207 | // * : cross-page when fetching the address costs 1 extra cycle 208 | // ** : Same-page branch (PC+2>>8 == a>>8) costs 1 extra cycle. Cross-page branch costs 2 extra cycles 209 | // ***: Instructions that read, modify and write a value in memory (+ JSR/RTI/RTS/PLA/PLP) cost 1 to 2 extra cycles 210 | // The order and implementations below are also optimized for a better gzip compression 211 | // Also, some instructions were splitted in two if they target either the memory or the Accumulator register (ROR, ROL, LSR, ASL) 212 | 213 | + ( 214 | 215 | // " ": TXS (transfer X to stack pointer) 216 | // Stack pointer = X 217 | // Addressing: imp 218 | // Opcode: 9A 219 | // Cycles total: 2 220 | // Cycles addr.: 0 221 | // Cycles opc. : 0 222 | "S=X;" 223 | 224 | // "!": CPX (compare memory and X) 225 | // N, Z and C are set with the result of X minus a byte in memory 226 | // Flag C is set if there's no borrow 227 | // Addressings: imm, zpg, abs 228 | // Opcodes: E0, E4, EC 229 | // Cycles total: 2, 3, 4 230 | // Cycles addr.: -1, 0, 1 231 | // Cycles opc. : 1, 1, 1 232 | + "p=r(a),C=X-p>=0,F(X-p);" 233 | 234 | // '"': CPY (compare memory and Y) 235 | // N, Z and C are set with the result of Y minus a byte in memory 236 | // Flag C is set if there's no borrow 237 | // Addressings: imm, zpg, abs 238 | // Opcodes: C0, C4, CC 239 | // Cycles total: 2, 3, 4 240 | // Cycles addr.: -1, 0, 1 241 | // Cycles opc. : 1, 1, 1 242 | + "p=r(a),C=Y-p>=0,F(Y-p);" 243 | 244 | // "#": ASL (shift left) 245 | // A byte in memory is left shifted. Flags: N, Z, C 246 | // The shifted-out bit 7 is saved in C 247 | // Addressings: zpg, zpgX, abs, absX 248 | // Opcodes: 06, 16, 0E, 1E 249 | // Cycles total: 5, 6, 6, 7 250 | // Cycles addr.: 0, 1, 1, 2 251 | // Cycles opc. : 3, 3, 3, 3 (***) 252 | + "p=r(a),C=p>>7,w(a,F(2*p)),c++;" 253 | 254 | // "$": ROL A (rotate left accumulator) 255 | // Rotate left A. Same as left shift but C flag is put into bit 0. Flags: N, Z, C 256 | // The shifted-out bit 7 is saved in C 257 | // Addressing: A 258 | // Opcode: 2A 259 | // Cycles total: 2 260 | // Cycles addr.: 0 261 | // Cycles opc. : 0 262 | + "C=A>>7,A=F(2*A+(1&P));" 263 | 264 | // "%": ROL (rotate left) 265 | // Rotate left a byte in memory. Same as left shift but C flag is put into bit 0. Flags: N, Z, C 266 | // The shifted-out bit 7 is saved in C 267 | // Addressings: zpg, zpgX, abs, absX 268 | // Opcodes: 26, 36, 2E, 3E 269 | // Cycles total: 5, 6, 6, 7 270 | // Cycles addr.: 0, 1, 1, 2 271 | // Cycles opc. : 3, 3, 3, 3 (***) 272 | + "p=r(a),C=p>>7,w(a,F(2*p+(1&P))),c++;" 273 | 274 | // "&": LSR A (shift right accumulator) 275 | // A is shifted right. Flags: N, Z, C 276 | // The shifted-out bit 0 is saved in C 277 | // Addressing: A 278 | // Opcode: 4A 279 | // Cycles total: 2 280 | // Cycles addr.: 0 281 | // Cycles opc. : 0 282 | + "C=1&A,A=F(A>>1);" 283 | 284 | // "'": LSR (shift right) 285 | // A byte in memory is shifted right. Flags: N, Z, C 286 | // The shifted-out bit 0 is saved in C 287 | // Addressings: zpg, zpgX, abs, absX 288 | // Opcodes: 46, 56, 4E, 5E 289 | // Cycles total: 5, 6, 6, 7 290 | // Cycles addr.: 0, 1, 1, 2 291 | // Cycles opc. : 3, 3, 3, 3 (***) 292 | + "p=r(a),C=1&p,w(a,F(p>>1)),c++;" 293 | 294 | // "(": DEX (decrement X) 295 | // X is decremented. Flags: N, Z 296 | // Addressing: imp 297 | // Opcode: CA 298 | // Cycles total: 2 299 | // Cycles addr.: 0 300 | // Cycles opc. : 0 301 | + "X=F(X-1);" 302 | 303 | // ")": BIT (test bits in memory) 304 | // N and V = bits 7 and 6 of operand. Z is set if operand AND A is not zero. Flags: N, Z, V 305 | // Addressings: zpg, abs 306 | // Opcodes: 24, 2C 307 | // Cycles total: 3, 4 308 | // Cycles addr.: 0, 1 309 | // Cycles opc. : 1, 1 310 | + "p=r(a),F(p&A),N=p>>7&1,V=p>>6&1;" 311 | 312 | // "*": ROR A (rotate right accumulator) 313 | // Rotate right A. Same as right shift but C flag is put into bit 7. Flags: N, Z, C 314 | // The shifted-out bit 0 is saved in C 315 | // Addressing: A 316 | // Opcode: 6A 317 | // Cycles total: 2 318 | // Cycles addr.: 0 319 | // Cycles opc. : 0 320 | + "C=1&A,A=F((A>>1)+128*(1&P));" 321 | 322 | // "+": INC (increment memory) 323 | // A byte in memory is incremented. Flags: N, Z 324 | // Addressings: zpg, zpgX, abs, absX 325 | // Opcodes: E6, F6, EE, FE 326 | // Cycles total: 5, 6, 6, 7 327 | // Cycles addr.: 0, 1, 1, 2 328 | // Cycles opc. : 3, 3, 3, 3 (***) 329 | + "w(a,F(r(a)+1)),c++;" 330 | 331 | // ",": INX (increment X) 332 | // X is incremented. Flags: N, Z 333 | // Addressing: imp 334 | // Opcode: E8 335 | // Cycles total: 2 336 | // Cycles addr.: 0 337 | // Cycles opc. : 0 338 | + "X=F(X+1);" 339 | 340 | // "-": DEY (decrement Y) 341 | // Y is decremented. Flags: N, Z 342 | // Addressing: imp 343 | // Opcode: 88 344 | // Cycles total: 2 345 | // Cycles addr.: 0 346 | // Cycles opc. : 0 347 | + "Y=F(Y-1);" 348 | 349 | // ".": INY (increment Y) 350 | // Y is incremented. Flags: N, Z 351 | // Addressing: imp 352 | // Opcode: C8 353 | // Cycles total: 2 354 | // Cycles addr.: 0 355 | // Cycles opc. : 0 356 | + "Y=F(Y+1);" 357 | 358 | // "/": LDY (load Y with memory) 359 | // Y = a byte from memory. Flags: N, Z 360 | // Addressings: imm, zpg, zpgX, abs, absX 361 | // Opcodes: A0, A4, B4, AC, BC 362 | // Cycles total: 2, 3, 4, 4, 4* 363 | // Cycles addr.: -1, 0, 1, 1, 1* 364 | // Cycles opc. : 1, 1, 1, 1, 1 365 | + "Y=F(r(a));" 366 | 367 | // 0: ROR (rotate right) 368 | // Rotate right a byte in memory. Same as left shift but C flag is put into bit 7. Flags: N, Z, C 369 | // The shifted-out bit 0 is saved in C 370 | // Addressings: zpg, zpgX, abs, absX 371 | // Opcodes: 66, 76, 6E, 7E 372 | // Cycles total: 5, 6, 6, 7 373 | // Cycles addr.: 0, 1, 1, 2 374 | // Cycles opc. : 3, 3, 3, 3 (***) 375 | + "p=r(a),C=1&p,w(a,F((p>>1)+128*(1&P))),c++;" 376 | 377 | // "1": CLC (clear carry flag) 378 | // C is set to 0 379 | // Addressing: imp 380 | // Opcode: 18 381 | // Cycles total: 2 382 | // Cycles addr.: 0 383 | // Cycles opc. : 0 384 | + "C=0;" 385 | 386 | // "2": SEI (set interrupt disable flag) 387 | // I is set to 1 388 | // Addressing: imp 389 | // Opcode: 78 390 | // Cycles total: 2 391 | // Cycles addr.: 0 392 | // Cycles opc. : 0 393 | + "I=1;" 394 | 395 | // "3": CLD (clear decimal flag) 396 | // D is set to 0 397 | // Addressing: imp 398 | // Opcode: D8 399 | // Cycles total: 2 400 | // Cycles addr.: 0 401 | // Cycles opc. : 0 402 | + "D=0;" 403 | 404 | // "4": CLI (clear interrupt disable flag) 405 | // I is set to 0 406 | // Addressing: imp 407 | // Opcode: 58 408 | // Cycles total: 2 409 | // Cycles addr.: 0 410 | // Cycles opc. : 0 411 | + "I=0;" 412 | 413 | // "5": LDA (load accumulator with memory) 414 | // A = a byte from memory. Flags: N, Z 415 | // Addressings: imm, zpg, zpgX, abs, absX, absY, indX, indY 416 | // Opcodes: A9, A5, B5, AD, BD, B9, A1, B1 417 | // Cycles total: 2, 3, 4, 4, 4*, 4*, 6, 5* 418 | // Cycles addr.: -1, 0, 1, 1, 1*, 1* 3, 3* 419 | // Cycles opc. : 1, 1, 1, 1, 1, 1, 1, 1 420 | + "A=F(r(a));" 421 | 422 | // "6": AND: (AND memory and accumulator) 423 | // A = A AND a byte in memory. Flags: N, Z 424 | // Addressings: imm, zpg, zpgX, abs, absX, absY, indX, indY 425 | // Opcodes: 29, 25, 35, 2D, 3D, 39, 21, 31 426 | // Cycles total: 2, 3, 4, 4, 4*, 4*, 6, 5* 427 | // Cycles addr.: -1, 0, 1, 1, 1*, 1* 3, 3* 428 | // Cycles opc. : 1, 1, 1, 1, 1, 1, 1, 1 429 | + "A=F(r(a)&A);" 430 | 431 | // "7": CMP (compare memory and accumulator) 432 | // N, Z and C are set with the result of A - a byte in memory 433 | // Flag C is set if there's no borrow 434 | // Addressings: imm, zpg, zpgX, abs, absX, absY, indX, indY 435 | // Opcodes: C9, C5, D5, CD, DD, D9, C1, D1 436 | // Cycles total: 2, 3, 4, 4, 4*, 4*, 6, 5* 437 | // Cycles addr.: -1, 0, 1, 1, 1*, 1* 3, 3* 438 | // Cycles opc. : 1, 1, 1, 1, 1, 1, 1, 1 439 | + "p=r(a),C=A-p>=0,F(A-p);" 440 | 441 | // "8": SBC (subtract from accumulator with carry) 442 | // A = A - a byte from memory - (1 - Carry). Flags: N, Z, C, V 443 | // Flag C is set if there's no borrow 444 | // Flag V is set if the subtraction is incorrectly considered positive 445 | // Addressings: imm, zpg, zpgX, abs, absX, absY, indX, indY 446 | // Opcodes: E9, E5, F5, ED, FD, F9, E1, F1 447 | // Cycles total: 2, 3, 4, 4, 4*, 4*, 6, 5* 448 | // Cycles addr.: -1, 0, 1, 1, 1*, 1* 3, 3* 449 | // Cycles opc. : 1, 1, 1, 1, 1, 1, 1, 1 450 | + "p=r(a),t=A+C-1-p,V=!!(128&(A^p))&&!!(128&(A^t)),C=t>=0,A=F(t);" 451 | 452 | // "9": ADC (add to accumulator with carry) 453 | // A = A + a byte in memory + Carry. Flags: N, Z, C, V 454 | // Flag C is set if there's a carry 455 | // Flag V is set if the sum of two positive numbers is incorrectly considered negative 456 | // Addressings: imm, zpg, zpgX, abs, absX, absY, indX, indY 457 | // Opcodes: 69, 65, 75, 6D, 7D, 79, 61, 71 458 | // Cycles total: 2, 3, 4, 4, 4*, 4*, 6, 5* 459 | // Cycles addr.: -1, 0, 1, 1, 1*, 1* 3, 3* 460 | // Cycles opc. : 1, 1, 1, 1, 1, 1, 1, 1 461 | + "p=r(a),t=A+C+p,V=!(128&(A^p))&&!!(128&(A^t)),C=t>255,A=F(t);" 462 | 463 | // ":": BCS (branch on carry set) 464 | // PC = address if C is 1 465 | // Addressing: rel 466 | // Opcode: B0 467 | // Cycles total: 2** 468 | // Cycles addr.: 0 469 | // Cycles opc. : 0** 470 | + "C&&(c+=1+(a>>8!=PC+1>>8),PC=a);" 471 | 472 | // ";": BMI (branch on minus) 473 | // PC = address if N is 1 474 | // Addressing: rel 475 | // Opcode: 30 476 | // Cycles total: 2** 477 | // Cycles addr.: 0 478 | // Cycles opc. : 0** 479 | + "N&&(c+=1+(a>>8!=PC+1>>8),PC=a);" 480 | 481 | // "<": BEQ (branch if equal) 482 | // PC = address if Z is 0 483 | // Addressing: rel 484 | // Opcode: F0 485 | // Cycles total: 2** 486 | // Cycles addr.: 0 487 | // Cycles opc. : 0** 488 | + "Z&&(c+=1+(a>>8!=PC+1>>8),PC=a);" 489 | 490 | // "=": BPL (branch on plus) 491 | // PC = address if N is 0 492 | // Addressing: rel 493 | // Opcode: 10 494 | // Cycles total: 2** 495 | // Cycles addr.: 0 496 | // Cycles opc. : 0** 497 | + "N||(c+=1+(a>>8!=PC+1>>8),PC=a);" 498 | 499 | // ">": BVS (branch on overflow set) 500 | // PC = address if V is 1 501 | // Addressing: rel 502 | // Opcode: 70 503 | // Cycles total: 2** 504 | // Cycles addr.: 0 505 | // Cycles opc. : 0** 506 | + "V&&(c+=1+(a>>8!=PC+1>>8),PC=a);" 507 | 508 | // "?": BNE (branch if not equal) 509 | // PC = address if Z is 1 510 | // Addressing: rel 511 | // Opcode: D0 512 | // Cycles total: 2** 513 | // Cycles addr.: 0 514 | // Cycles opc. : 0** 515 | + "Z||(c+=1+(a>>8!=PC+1>>8),PC=a);" 516 | 517 | // "@": BVC (branch on overflow clear) 518 | // PC = address if V is 0 519 | // Addressing: rel 520 | // Opcode: 50 521 | // Cycles total: 2** 522 | // Cycles addr.: 0 523 | // Cycles opc. : 0** 524 | + "V||(c+=1+(a>>8!=PC+1>>8),PC=a);" 525 | 526 | // "A": LDX (load X with memory) 527 | // X = a byte from memory. Flags: N, Z 528 | // Addressings: imm, zpg, zpgY, abs, absY 529 | // Opcodes: A2, A6, B6, AE, BE 530 | // Cycles total: 2, 3, 4, 4, 4* 531 | // Cycles addr.: -1, 0, 1, 1, 1* 532 | // Cycles opc. : 1, 1, 1, 1, 1 533 | + "X=F(r(a));" 534 | 535 | // "B": EOR (exclusive-or memory and accumulator) 536 | // A = A XOR a byte in memory. Flags: N, Z 537 | // Addressings: imm, zpg, zpgX, abs, absX, absY, indX, indY 538 | // Opcodes: 49, 45, 55, 4D, 5D, 59, 41, 51 539 | // Cycles total: 2, 3, 4, 4, 4*, 4*, 6, 5* 540 | // Cycles addr.: -1, 0, 1, 1, 1*, 1* 3, 3* 541 | // Cycles opc. : 1, 1, 1, 1, 1, 1, 1, 1 542 | + "A=F(r(a)^A);" 543 | 544 | // "C": DEC (decrement memory) 545 | // A byte in memory is decremented. Flags: N, Z 546 | // Addressings: zpg, zpgX, abs, absX 547 | // Opcodes: C6, D6, CE, DE 548 | // Cycles total: 5, 6, 6, 7 549 | // Cycles addr.: 0, 1, 1, 2 550 | // Cycles opc. : 3, 3, 3, 3 (***) 551 | + "w(a,F((r(a)-1)&255)),c++;" 552 | 553 | // "D": ASL A (shift left accumulator) 554 | // A is left shifted. Flags: N, Z, C 555 | // The shifted-out bit 7 is saved in C 556 | // Addressing: A 557 | // Opcode: 0A 558 | // Cycles total: 2 559 | // Cycles addr.: 0 560 | // Cycles opc. : 0 561 | + "C=A>>7,A=F(2*A);" 562 | 563 | // "E": JSR (jump to subroutine) 564 | // Push PC + 2, PC = absolute address 565 | // Addressing: abs 566 | // Opcode: 20 567 | // Cycles total: 6 568 | // Cycles addr.: 1 569 | // Cycles opc. : 3 (***) 570 | + "h(PC>>8),h(255&PC),PC=a-1,c++;" 571 | 572 | // "F": SEC (set carry flag) 573 | // C is set to 1 574 | // Addressing: imp 575 | // Opcode: 38 576 | // Cycles total: 2 577 | // Cycles addr.: 0 578 | // Cycles opc. : 0 579 | + "C=1;" 580 | 581 | // "G": SED (set decomal flag) 582 | // D is set to 1 583 | // Addressing: imp 584 | // Opcode: F8 585 | // Cycles total: 2 586 | // Cycles addr.: 0 587 | // Cycles opc. : 0 588 | + "D=1;" 589 | 590 | // "H": CLV (clear overflow flag) 591 | // V is set to 0 592 | // Addressing: imp 593 | // Opcode: B8 594 | // Cycles total: 2 595 | // Cycles addr.: 0 596 | // Cycles opc. : 0 597 | + "V=0;" 598 | 599 | // "I": ORA (OR memory and accumulator) 600 | // A = A OR a byte in memory. Flags: N, Z. 601 | // Addressings: imm, zpg, zpgX, abs, absX, absY, indX, indY 602 | // Opcodes: 09, 05, 15, 0D, 1D, 19, 01, 11 603 | // Cycles total: 2, 3, 4, 4, 4*, 4*, 6, 5* 604 | // Cycles addr.: -1, 0, 1, 1, 1*, 1* 3, 3* 605 | // Cycles opc. : 1, 1, 1, 1, 1, 1, 1, 1 606 | + "A=F(r(a)|A);" 607 | 608 | // "J": PHA (push accumulator) 609 | // Push A 610 | // Addressing: imp 611 | // Opcode: 48 612 | // Cycles total: 3 613 | // Cycles addr.: 0 614 | // Cycles opc. : 1 615 | + "h(A);" 616 | 617 | // "K": PHP (push processor status) 618 | // Push P with B flag set to 1 619 | // Addressing: imp 620 | // Opcode: 08 621 | // Cycles total: 3 622 | // Cycles addr.: 0 623 | // Cycles opc. : 1 624 | + "h(P|16);" 625 | 626 | // "L": PLA (pull accumulator) 627 | // Pull A. Flags: N, Z. 628 | // Addressing: imp 629 | // Opcode: 68 630 | // Cycles total: 4 (*** 1 extra cycle according to nestest) 631 | // Cycles addr.: 0 632 | // Cycles opc. : 1 633 | + "A=F(g()),c++;" 634 | 635 | // "M": PLP (pull processor status) 636 | // Pull P and set all flags 637 | // (According to nestest, the B flag stays at 0) 638 | // Addressing: imp 639 | // Opcode: 28 640 | // Cycles total: 4 (*** 1 extra cycle according to nestest) 641 | // Cycles addr.: 0 642 | // Cycles opc. : 1 643 | + "f(g()&239),c++;" 644 | 645 | // "N": RTI (return from interrupt) 646 | // Pull P, set all flags, pull PC 647 | // Addressing: imp 648 | // Opcode: 40 649 | // Cycles total: 6 650 | // Cycles addr.: 0 651 | // Cycles opc. : 4 (***) 652 | + "f(g()),PC=g()+256*g()-1,c++;" 653 | 654 | // "O": BCC (branch on carry clear) 655 | // PC = address if C is 0 656 | // Addressing: rel 657 | // Opcode: 90 658 | // Cycles total: 2** 659 | // Cycles addr.: 0 660 | // Cycles opc. : 0** 661 | + "C||(c+=1+(a>>8!=PC+1>>8),PC=a);" 662 | 663 | // "P": BRK (force break) 664 | // Interrupt, push PC+2 (PC+1 is a padding byte), push P with B flag set to 1, set I to 1 665 | // This is equivalent to an IRQ interrupt with another value of P pushed on the stack: 666 | // "h(PC>>8),h(255&PC),h(P|16),I=1,PC=r(65534)+256*r(65535)-1;" 667 | // Addressing: imp 668 | // Opcode: 00 669 | // Cycles total: 7 670 | // Cycles addr.: 0 671 | // Cycles opc. : 5 672 | //+ 673 | + "op(3,1);" 674 | 675 | // "Q": TAY (transfer accumulator to Y) 676 | // Y = A. Flags: N, Z 677 | // Addressing: imp 678 | // Opcode: A8 679 | // Cycles total: 2 680 | // Cycles addr.: 0 681 | // Cycles opc. : 0 682 | + "Y=F(A);" 683 | 684 | // "R": STA (store accumulator) 685 | // A is copied in memory 686 | // Addressings: zpg, zpgX, abs, absX, absY, indX, indY 687 | // Opcodes: 85, 95, 8D, 9D, 99, 81, 91 688 | // Cycles total: 3, 4, 4, 5, 5, 6, 6 689 | // Cycles addr.: 0, 1, 1, 2, 2 3, 2 690 | // Cycles opc. : 1, 1, 1, 1, 1, 1, 1 691 | + "w(a,A);" 692 | 693 | // "S": STX (store X) 694 | // X is copied in memory 695 | // Addressings: zpg, zpgY, abs 696 | // Opcodes: 86, 96, 8E 697 | // Cycles total: 3, 4, 4 698 | // Cycles addr.: 0, 1, 1 699 | // Cycles opc. : 1, 1, 1 700 | + "w(a,X);" 701 | 702 | // "T": TSX (transfer stack pointer to X) 703 | // X = S. Flags: N, Z 704 | // Addressing: imp 705 | // Opcode: BA 706 | // Cycles total: 2 707 | // Cycles addr.: 0 708 | // Cycles opc. : 0 709 | + "X=F(S);" 710 | 711 | // "U": TAX (transfer accumulator to X) 712 | // X = A. Flags: N, Z 713 | // Addressing: imp 714 | // Opcode: AA 715 | // Cycles total: 2 716 | // Cycles addr.: 0 717 | // Cycles opc. : 0 718 | + "X=F(A);" 719 | 720 | // "V": STY (store Y) 721 | // Y is copied in memory 722 | // Addressings: zpg, zpgX, abs 723 | // Opcodes: 84, 94, 8C 724 | // Cycles total: 3, 4, 4 725 | // Cycles addr.: 0, 1, 1 726 | // Cycles opc. : 1, 1, 1 727 | + "w(a,Y);" 728 | 729 | // "W": TYA (transfer Y to accumulator) 730 | // A = Y. Flags: N, Z 731 | // Addressing: imp 732 | // Opcode: 98 733 | // Cycles total: 2 734 | // Cycles addr.: 0 735 | // Cycles opc. : 0 736 | + "A=F(Y);" 737 | 738 | // "X": TXA (transfer X to accumulator) 739 | // A = X. Flags: N, Z 740 | // Addressing: imp 741 | // Opcode: 8A 742 | // Cycles total: 2 743 | // Cycles addr.: 0 744 | // Cycles opc. : 0 745 | + "A=F(X);" 746 | 747 | // "Y": RTS (return from subroutine) 748 | // Pull and increment PC 749 | // Addressing: imp 750 | // Opcode: 60 751 | // Cycles total: 6 752 | // Cycles addr.: 0 753 | // Cycles opc. : 0 (***) 754 | + "PC=g()+256*g(c+=2);" 755 | 756 | // "Z": JMP (jump to new location) 757 | // Set a new value to PC 758 | // Addressings: abs 759 | // Opcodes: 4C 760 | // Cycles total: 3 761 | // Cycles addr.: 1 762 | // Cycles opc. : 0 763 | + "PC=a-1;" 764 | 765 | // "[" JMP indirect 766 | // Jump to an address stored anywhere in memory. The address of this address is stored after the opcode 767 | // Hardware bug: if the indirect address falls on a page boundary ($xxFF), it will wrap and fetch the low byte in the same page ($xx00) 768 | // Addressing: ind 769 | // Opcodes: 6C 770 | // Cycles total: 5 771 | // Cycles addr.: 3 772 | // Cycles opc. : 2 773 | + "PC=r(a)+256*r(a+1-256*((a&255)==255))-1" 774 | 775 | // "z": NOP (no operation) 776 | // (When a "z" is read, the generated JavaScript code will just contain "undefined;") 777 | // Addressing: imp 778 | // Opcode: EA 779 | // Cycles total: 2 780 | // Cycles addr.: 0 781 | // Cycles opc. : 0 782 | + "" 783 | 784 | // Make an array from this string 785 | ).split(";") 786 | 787 | // Fetch the right instruction for the current opcode (ignore every illegal opcode where o % 4 == 3): 788 | // (The string below is optomized for compression: all the illegal opcodes are assigned characters that allow extra repetition) 789 | [ 790 | ( 791 | `PI#PI#KIDPI#=I#PI#1IDPI#` 792 | +`E6%)6%M6$)6%;6%E6%F6%E6%` 793 | +`NB'NB'JB&ZB'@B'NB'4B'NB'` 794 | +`Y90Y90L9*[90>90Y90290Y90` 795 | +`VRSVRS-zXVRSORSVRSWR VRS` 796 | +`/5A/5AQ5U/5A:5A/5AH5T/5A` 797 | +`"7C"7C.7("7C?7C"7C37C"7C` 798 | +`!8+!8+,8z!8+<8+!8+G8+!8+` 799 | )[o - (o >> 2)].charCodeAt() - 32 800 | ] 801 | ) 802 | ); 803 | 804 | // Emulation 805 | // --------- 806 | 807 | // If an interrupt (v = 1/2/3) is specified, it's executed 808 | // Otherwise, execute the next opcode at the address pointed by the PC register 809 | // z means BRK opcode (similar to IRQ, but with a slightly different P pushed on the stack) 810 | op = (v, z) => ( 811 | 812 | // - Reset cycle counter 813 | // - Fetch opcode at address PC (costs 1 cycle), save it in o 814 | // - Increment PC, save it in a 815 | // - Fetch the byte at address a (costs 1 cycle), save it in p 816 | c = 0, 817 | o = r(PC), 818 | p = r(a = PC+1), 819 | 820 | // Execute an interrupt if v is set 821 | v ? ( 822 | 823 | // 1: NMI: 824 | // Push PC and P with B flag set to 0, then set I to 1, 825 | // then jump to address stored at $FFFA-$FFFB 826 | // This costs 7 cycles 827 | // On NES, it is triggered at the beginning of VBlank if bit 7 of PPU register $2000 is set, otherwise it's skipped and only costs 2 cycles 828 | 829 | // 2: Reset: 830 | // Push PC and P with B flag set to 0, then set I to 1, 831 | // then jump to address stored at $FFFC-$FFFD 832 | // This resets c and costs 8 cycles 833 | // On NES, this also resets the PPU 834 | 835 | // 3: IRQ/BRK: 836 | // Push PC and P with B flag set to 0 (IRQ) or 1 (BRK), then set I to 1, 837 | // then jump to address stored at $FFFE-$FFFF 838 | // This costs 7 cycles 839 | 840 | // Only execute NMI if VBlank (bit 7 of $2000) is set 841 | (v > 1 || r(0x2000) >> 7) 842 | 843 | // Only execute IRQ if I is not set 844 | && (v < 3 || !I) 845 | 846 | && ( 847 | ( 848 | (v - 2) 849 | ? (h(PC >> 8), h(255 & PC), h(z ? (P|16) : (239 & P))) // NMI/IRQ/BRK 850 | : (S = (S-3) & 255) // Reset 851 | ), 852 | 853 | I = 1, 854 | PC = r(65528 + v * 2) + 256 * r(65528 + v * 2 + 1) 855 | ) 856 | ) 857 | 858 | // Or execute the next instruction: 859 | : ( 860 | o && O[o](), 861 | PC++, 862 | PC &= 0xFFFF 863 | ), 864 | 865 | // Update status register P according to the new flags values 866 | P = C + Z*2 + I*4 + D*8 + B*16 + 32 + V*64 + N*128 867 | ) -------------------------------------------------------------------------------- /src/controller.js: -------------------------------------------------------------------------------- 1 | var Controller = function(){ 2 | this.state = new Array(8); 3 | for(var i = 0; i < this.state.length; i++){ 4 | this.state[i] = 0x40; 5 | } 6 | }; 7 | 8 | joy1StrobeState = 0 9 | joy2StrobeState = 0 10 | 11 | Controller.BUTTON_A = 0; 12 | Controller.BUTTON_B = 1; 13 | Controller.BUTTON_SELECT = 2; 14 | Controller.BUTTON_START = 3; 15 | Controller.BUTTON_UP = 4; 16 | Controller.BUTTON_DOWN = 5; 17 | Controller.BUTTON_LEFT = 6; 18 | Controller.BUTTON_RIGHT = 7; 19 | 20 | Controller.prototype = { 21 | keydown: function(key){ 22 | this.state[key] = 0x41; 23 | }, 24 | 25 | keyup: function(key){ 26 | this.state[key] = 0x40; 27 | } 28 | }; 29 | 30 | 31 | joy1Read = function(){ 32 | var ret; 33 | 34 | switch (joy1StrobeState){ 35 | case 0: 36 | case 1: 37 | case 2: 38 | case 3: 39 | case 4: 40 | case 5: 41 | case 6: 42 | case 7: 43 | ret = NES.controllers[1].state[joy1StrobeState]; 44 | break; 45 | case 8: 46 | case 9: 47 | case 10: 48 | case 11: 49 | case 12: 50 | case 13: 51 | case 14: 52 | case 15: 53 | case 16: 54 | case 17: 55 | case 18: 56 | ret = 0; 57 | break; 58 | case 19: 59 | ret = 1; 60 | break; 61 | default: 62 | ret = 0; 63 | } 64 | 65 | joy1StrobeState++; 66 | if(joy1StrobeState === 24){ 67 | joy1StrobeState = 0; 68 | } 69 | 70 | return ret; 71 | }, 72 | 73 | joy2Read = function(){ 74 | var ret; 75 | 76 | switch (joy2StrobeState){ 77 | case 0: 78 | case 1: 79 | case 2: 80 | case 3: 81 | case 4: 82 | case 5: 83 | case 6: 84 | case 7: 85 | ret = NES.controllers[2].state[joy2StrobeState]; 86 | break; 87 | case 8: 88 | case 9: 89 | case 10: 90 | case 11: 91 | case 12: 92 | case 13: 93 | case 14: 94 | case 15: 95 | case 16: 96 | case 17: 97 | case 18: 98 | ret = 0; 99 | break; 100 | case 19: 101 | ret = 1; 102 | break; 103 | default: 104 | ret = 0; 105 | } 106 | 107 | joy2StrobeState++; 108 | if(joy2StrobeState === 24){ 109 | joy2StrobeState = 0; 110 | } 111 | 112 | return ret; 113 | } -------------------------------------------------------------------------------- /src/cpu.js: -------------------------------------------------------------------------------- 1 | // CPU 2 | // === 3 | 4 | // Resources: 5 | // - https://wiki.nesdev.com/w/index.php/CPU_unofficial_opcodes 6 | // - https://wiki.nesdev.com/w/index.php/Status_flags 7 | // - http://wiki.nesdev.com/w/index.php/CPU_interrupts 8 | // - https://wiki.nesdev.com/w/index.php/Stack 9 | // - https://www.masswerk.at/6502/6502_instruction_set.html 10 | // - https://problemkaputt.de/everynes.htm#cpu65xxmicroprocessor 11 | // - https://www.npmjs.com/package/dict-tempering 12 | // - http://www.6502.org/tutorials/vflag.html 13 | // - https://retrocomputing.stackexchange.com/questions/145 14 | // - http://forum.6502.org/viewtopic.php?f=8&t=6370 15 | 16 | var 17 | cpu_mem, 18 | interrupt_requested, 19 | clk = 0, 20 | 21 | // Reset the CPU 22 | cpu_reset = () => { 23 | 24 | // CPU internal memory (64KB) 25 | cpu_mem = []; 26 | }, 27 | 28 | // Clock 1 CPU cycle 29 | // During this time, 3 PPU cycles and 1 APU cycle take place 30 | cpu_tick = () => { 31 | //console.log(clk++); 32 | ppu_tick(); 33 | ppu_tick(); 34 | ppu_tick(); 35 | //apu_tick(); 36 | }, 37 | 38 | // Emulates a single CPU instruction or interrupt, returns the number of cycles 39 | emulate = () => { 40 | 41 | // Execute the requested interrupt, if any 42 | if(interrupt_requested){ 43 | 44 | // 1: NMI 45 | // 2: Reset 46 | // 3: IRQ 47 | op(interrupt_requested); 48 | 49 | // Reset interrupt requested flag 50 | interrupt_requested = 0; 51 | } 52 | 53 | // Or execute next instruction 54 | else { 55 | op(); 56 | } 57 | 58 | // Return the number of cycles spent 59 | return c; 60 | } -------------------------------------------------------------------------------- /src/mappers.js: -------------------------------------------------------------------------------- 1 | // Mappers 2 | // ======= 3 | 4 | // Resources: 5 | // - https://problemkaputt.de/everynes.htm#cartridgesandmappers 6 | // - https://wiki.nesdev.com/w/index.php/Mapper 7 | // - https://wiki.nesdev.com/w/index.php/Cartridge_and_mappers%27_history 8 | 9 | // -------- 10 | 11 | 12 | // Load ROM's content in memory 13 | NES.load = () => { 14 | 15 | // Mapper 0 16 | // -------- 17 | // https://wiki.nesdev.com/w/index.php/NROM 18 | // - 2 x 16KB PRG-ROM banks (mirrored if less than 32KB) 19 | // - 1 x 8KB CHR-ROM bank (mirrored if less than 8KB) or 1 x 8KB CHR-RAM bank 20 | // - Horizontal or vertical nametable mirroring (fixed) 21 | if(!NES.mapper){ 22 | 23 | // Set PRG-ROM banks: 0 and the last one (fixed): 24 | NES.prg_0_bank = 0; 25 | NES.prg_1_bank = NES.prg.length - 1; 26 | 27 | // Set CHR-ROM/RAM bank: 0 (fixed) 28 | NES.chr_bank = 0; 29 | } 30 | 31 | // Mapper 2 32 | // -------- 33 | // https://wiki.nesdev.com/w/index.php/NROM 34 | // - 8 or 16 x 16KB PRG-ROM banks 35 | // - 1 x 8KB CHR-ROM bank 36 | // - Horizontal or vertical nametable mirroring (fixed) 37 | else if(NES.mapper == 2){ 38 | 39 | // Set PRG-ROM banks: 0 (swappable) and the last one (fixed) 40 | NES.prg_0_bank = 0; 41 | NES.prg_1_bank = NES.prg.length - 1; 42 | 43 | // Set CHR-RAM bank: 0 (fixed) 44 | NES.chr_bank = 0; 45 | } 46 | 47 | // Mapper 30 (incomplete) 48 | // ---------------------- 49 | // https://www.nesdev.org/wiki/UNROM_512 50 | // - 16 or 32 x 16KB PRG-ROM banks 51 | // - 4 x 8KB CHR-RAM banks 52 | // - Horizontal / vertical mirroring (fixed) or 1-screen mirroring (programmable) 53 | else if(NES.mapper == 30){ 54 | 55 | // Initialize the 4 x 8KB CHR-RAM banks (empty on boot) 56 | NES.chr = [[], [], [], []]; 57 | 58 | // Set PRG-ROM banks: 0 (swappable) and the last one (fixed) 59 | NES.prg_0_bank = 0; 60 | NES.prg_1_bank = NES.prg.length - 1; 61 | 62 | // Set CHR-RAM bank (fixed): 63 | NES.chr_bank = 0; 64 | 65 | // Backup mirroring 66 | NES.mirroring_backup = NES.mirroring; 67 | } 68 | else { 69 | alert("unknown mapper: " + NES.mapper); 70 | } 71 | }, 72 | 73 | // Some mappers do special things when some addresses are written in CPU memory. This is handled here: 74 | mapper_write = (address, value) => { 75 | 76 | // Mapper 0: 77 | // --------- 78 | // nothing 79 | 80 | // Mapper 2: 81 | // --------- 82 | // - Write [MCCP PPPP] in any address between $C000 and $FFFF to set M (1-screen mirroring), CC (CHR-RAM bank swap), PPPPP (low PRG-ROM bank swap: $8000-$BFFF). 83 | // - TODO: Erase 4KB of PRG-ROM (code must be in wram): $C000:$01, $9555:$AA, $C000:$00, $AAAA:$55, $C000:$01, $9555:$80, $C000:$01, $9555:$AA, $C000:$00, $AAAA:$55, $C000:BANK (BANK = $00-$1F), ADDR:$30 (ADDR = $8000/9000/$A000/$B000). Read ADDR twice until DATA is correct twice. 84 | // - TODO: Write 1B of PRG-ROM (code must be in wram): $C000:$01, $9555:$AA, $C000:$00, $AAAA:$55, $C000:$01, $9555:$A0, $C000:BANK, ADDR:DATA (DATA = $00-$FF). Read ADDR twice until DATA is correct twice. 85 | // - Other routines (ignored here): chip-erase, Software ID entry and Software ID exit. 86 | if(NES.mapper == 30){ 87 | if(address >= 0xC000){ 88 | NES.prg_0_bank = value & 0x11111; 89 | NES.chr_bank = (value >> 5) & 0b11; 90 | NES.mirroring = (value >> 7) ? 3 : NES.mirroring_backup; 91 | } 92 | } 93 | 94 | // Mapper 30: 95 | // ---------- 96 | // - Write [xxxx PPPP] in any address between $8000 and $FFFF to set PPPP (low PRG-ROM bank swap: $8000-$BFFF). 97 | if(NES.mapper == 2){ 98 | if(address >= 0x8000){ 99 | NES.prg_0_bank = value & 0x1111; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/memory.js: -------------------------------------------------------------------------------- 1 | // Memory manager 2 | // ============== 3 | 4 | // This file handles the CPU's memory accesses 5 | // It assumes that the memory has been initialized (see CPU_reset()) 6 | // Reading and writing at specific addresses makes the CPU able to communicate with other parts of the emulator (PPU, APU, controllers, mapper) 7 | 8 | // Resources: 9 | // - https://problemkaputt.de/everynes.htm#memorymaps 10 | // - https://problemkaputt.de/everynes.htm#iomap 11 | // - https://wiki.nesdev.com/w/index.php/cpu_memory_map 12 | 13 | // CPU memory map (64KB): 14 | 15 | // +-------------+-------+-------------------------------------------------------+ 16 | // | Address | Size | Use | 17 | // +-------------+-------+-------------------------------------------------------+ 18 | // | $0000-$07FF | 2KB | 2KB internal RAM: | 19 | // | $0000-$00FF | 256B | - Zero page | 20 | // | $0100-$01FF | 256B | - Stack | 21 | // | $0200-$07FF | 1.5KB | - General purpose | 22 | // +- - - - - - -+- - - -+- - - - - - - - - - - - - - - - - - - - - - - - - - - -+ 23 | // | $0800-$0FFF | 2KB | Mirror of $0000-$07FF | 24 | // | $1000-$17FF | 2KB | Mirror of $0000-$07FF | 25 | // | $1800-$1FFF | 2KB | Mirror of $0000-$07FF | 26 | // +-------------+-------+-------------------------------------------------------+ 27 | // | $2000-$2007 | 8B | PPU I/O registers: | 28 | // | 2000 | 1B | PPU Control Register 1 (Write-only) | 29 | // | 2001 | 1B | PPU Control Register 2 (Write-only) | 30 | // | 2002 | 1B | PPU Status Register (Read-only) | 31 | // | 2003 | 1B | SPR-RAM Address Register (Write-only) | 32 | // | 2004 | 1B | SPR-RAM Data Register (Read/write) | 33 | // | 2005 | 1B | PPU Background Scrolling Offset (Write-only, twice) | 34 | // | 2006 | 1B | VRAM Address Register (Write-only, twice) | 35 | // | 2007 | 1B | VRAM Read/Write Data Register (Read/write) | 36 | // +- - - - - - -+- - - -+- - - - - - - - - - - - - - - - - - - - - - - - - - - -+ 37 | // | $2008-$3FFF | 8184B | Mirrors of $2000-$2007 (every 8 bytes) | 38 | // +-------------+-------+-------------------------------------------------------+ 39 | // | $4000-$4017 | 24B | APU I/O registers: | 40 | // | 4000 | 1B | APU Channel 1 (Rectangle) Volume/Decay (Write) | 41 | // | 4001 | 1B | APU Channel 1 (Rectangle) Sweep (Write) | 42 | // | 4002 | 1B | APU Channel 1 (Rectangle) Frequency (Write) | 43 | // | 4003 | 1B | APU Channel 1 (Rectangle) Length (Write) | 44 | // | 4004 | 1B | APU Channel 2 (Rectangle) Volume/Decay (Write) | 45 | // | 4005 | 1B | APU Channel 2 (Rectangle) Sweep (Write) | 46 | // | 4006 | 1B | APU Channel 2 (Rectangle) Frequency (Write) | 47 | // | 4007 | 1B | APU Channel 2 (Rectangle) Length (Write) | 48 | // | 4008 | 1B | APU Channel 3 (Triangle) Linear Counter (Write) | 49 | // | 4009 | 1B | N/A | 50 | // | 400A | 1B | APU Channel 3 (Triangle) Frequency (Write) | 51 | // | 400B | 1B | APU Channel 3 (Triangle) Length (Write) | 52 | // | 400C | 1B | APU Channel 4 (Noise) Volume/Decay (Write) | 53 | // | 400D | 1B | N/A | 54 | // | 400E | 1B | APU Channel 4 (Noise) Frequency (Write) | 55 | // | 400F | 1B | APU Channel 4 (Noise) Length (Write) | 56 | // | 4010 | 1B | APU Channel 5 (DMC) Play mode & DMA frequency (Write) | 57 | // | 4011 | 1B | APU Channel 5 (DMC) Delta counter load (Write) | 58 | // | 4012 | 1B | APU Channel 5 (DMC) Address load (Write) | 59 | // | 4013 | 1B | APU Channel 5 (DMC) Length (Write) | 60 | // +- - - - - - -+- - - -+- - - - - - - - - - - - - - - - - - - - - - - - - - - -+ 61 | // | 4014 | 1B | SPR-RAM DMA Register (Write) | 62 | // +- - - - - - -+- - - -+- - - - - - - - - - - - - - - - - - - - - - - - - - - -+ 63 | // | 4015 | 1B | DMC/IRQ/length status/channel enable (Read/write) | 64 | // +- - - - - - -+- - - -+- - - - - - - - - - - - - - - - - - - - - - - - - - - -+ 65 | // | 4016 | 1B | Joypad #1 (Read/write) | 66 | // | 4017 | 1B | Joypad #2 / APU SOFTCLK (Read/write) | 67 | // +- - - - - - -+- - - -+- - - - - - - - - - - - - - - - - - - - - - - - - - - -+ 68 | // | $4018-$401F | 8B | APU I/O test registers (disabled) | 69 | // +-------------+-------+-------------------------------------------------------+ 70 | // | $4020-$FFFF | 48KB | Cartridge space: | 71 | // | $4020-$5FFF | 8160B | - Expansion ROM (some mappers add extra ROM/RAM here) | 72 | // | $6000-$7FFF | 8KB | - PRG-RAM (if any) | 73 | // | $7000-$71FF | 512B | - trainer (if any) | 74 | // | $8000-$BFFF | 16KB | - PRG-ROM low page | 75 | // | $C000-$FFFF | 16KB | - PRG-ROM high page, including: | 76 | // | $FFFA-$FFFB | 2B | * NMI vector | 77 | // | $FFFC-$FFFD | 2B | * Reset vector | 78 | // | $FFFE-$FFFF | 2B | * IRQ/BRK vector | 79 | // +-------------+-------+-------------------------------------------------------+ 80 | 81 | 82 | // Read a 8-byte value in memory 83 | memory_read = address => { 84 | 85 | // Wrap around ($0000-$FFFF) 86 | address &= 0xFFFF; 87 | 88 | // Handle RAM mirrors ($0000-$07FF + $0800-$1FFF) 89 | if(address < 0x2000) address &= 0x7FF; 90 | 91 | // PPU registers ($2000-$2007) + mirrors ($2008-$3FFF) 92 | else if(address < 0x4000){ 93 | 94 | // Mirroring 95 | address &= 0x2007; 96 | 97 | // $2002: PPU Status Register 98 | if(address == 0x2002) return get_PPUSTATUS(); 99 | 100 | // $2004: Sprite Memory read 101 | else if(address == 0x2004) return get_OAMDATA(); 102 | 103 | // $2007: VRAM read 104 | else if(address == 0x2007) return get_PPUDATA(); 105 | } 106 | 107 | // Sound and I/O registers ($4000-$401F) 108 | else if(address < 0x4020){ 109 | 110 | // $4015: Sound channel enable, DMC Status 111 | if(address == 0x4015){ 112 | return get_4015(); 113 | } 114 | 115 | // $4016: Joystick 1 + Strobe 116 | else if(address == 0x4016) return joy1Read(); 117 | 118 | // $4017: Joystick 2 + Strobe 119 | else if(address == 0x4017) return joy2Read(); 120 | } 121 | 122 | // PRG-ROM 123 | else if(address >= 0x8000 && address < 0xC000){ 124 | return NES.prg[NES.prg_0_bank][address - 0x8000]; 125 | } 126 | else if(address >= 0xC000){ 127 | return NES.prg[NES.prg_1_bank][address - 0xC000]; 128 | } 129 | 130 | // Simply read in memory 131 | return cpu_mem[address] || 0; 132 | }, 133 | 134 | // Write a 8-bit value in memory 135 | memory_write = (address, value) => { 136 | 137 | // Wrap around ($0000-$FFFF) 138 | address &= 0xFFFF; 139 | 140 | // Handle RAM mirrors ($0000-$07FF + $0800-$1FFF) 141 | if(address < 0x2000) address &= 0x7FF; 142 | 143 | // PPU registers ($2000-$2007) + mirrors ($2008-$3FFF) 144 | else if(address < 0x4000){ 145 | 146 | address &= 0x2007; 147 | 148 | // $2000: PPU Control register 1 (write-only) 149 | if(address == 0x2000) set_PPUCTRL(value); 150 | 151 | // $2001: PPU Control register 2 (write-only) 152 | else if(address == 0x2001) set_PPUMASK(value); 153 | 154 | // $2003: Set Sprite RAM address (write-only) 155 | else if(address == 0x2003) set_OAMADDR(value); 156 | 157 | // $2004: Write to Sprite RAM 158 | else if(address == 0x2004) set_OAMDATA(value); 159 | 160 | // $2005: Screen Scroll offsets (write-only) 161 | else if(address == 0x2005) set_PPUSCROLL(value); 162 | 163 | // $2006: Set VRAM address (write-only) 164 | else if(address == 0x2006) set_PPUADDR(value); 165 | 166 | // $2007: Write to VRAM 167 | else if(address == 0x2007) set_PPUDATA(value); 168 | } 169 | 170 | // APU registers ($4000-$4013) 171 | else if(address < 0x4014) { 172 | //APU.storeRegister(address, value); 173 | //APU.writeReg(address, value); 174 | 175 | if(address == 0x4000) set_4000(value); 176 | if(address == 0x4001) set_4001(value); 177 | if(address == 0x4002) set_4002(value); 178 | if(address == 0x4003) set_4003(value); 179 | 180 | if(address == 0x4004) set_4004(value); 181 | if(address == 0x4005) set_4005(value); 182 | if(address == 0x4006) set_4006(value); 183 | if(address == 0x4007) set_4007(value); 184 | 185 | if(address == 0x4008) set_4008(value); 186 | if(address == 0x400a) set_400a(value); 187 | if(address == 0x400b) set_400b(value); 188 | 189 | if(address == 0x400c) set_400c(value); 190 | if(address == 0x400e) set_400e(value); 191 | if(address == 0x400f) set_400f(value); 192 | 193 | /* 194 | if(address == 0x4010) set_4010(value); 195 | if(address == 0x4011) set_4011(value); 196 | if(address == 0x4012) set_4012(value); 197 | if(address == 0x4013) set_4013(value);*/ 198 | } 199 | 200 | // I/O registers ($4014-$401F) 201 | else if(address < 0x4020){ 202 | 203 | // $4014: Sprite Memory DMA Access 204 | if(address == 0x4014) set_OAMDMA(value); 205 | 206 | // $4015: Sound Channel Switch, DMC Status 207 | else if(address == 0x4015) { 208 | set_4015(value); 209 | } 210 | 211 | // $4016: Joystick 1 + Strobe 212 | else if(address == 0x4016){ 213 | if((value & 1) === 0 && (joypadLastWrite & 1) === 1){ 214 | joy1StrobeState = 0; 215 | joy2StrobeState = 0; 216 | } 217 | joypadLastWrite = value; 218 | } 219 | 220 | // $4017: Sound channel frame sequencer: 221 | else if(address == 0x4017){ 222 | set_4017(value); 223 | } 224 | } 225 | 226 | // Write to persistent RAM (save slot) 227 | else if(address >= 0x6000 && address < 0x8000) NES.onBatteryRamWrite(address, value); 228 | 229 | // Simply write in memory, if not in PRG-ROM (necessary?) 230 | if(address < 0x8000){ 231 | cpu_mem[address] = value; 232 | } 233 | 234 | // Inform the Mapper that a write has been made in memory 235 | mapper_write(address, value); 236 | } -------------------------------------------------------------------------------- /src/nes.js: -------------------------------------------------------------------------------- 1 | // Jsnes-lite API 2 | // ============== 3 | 4 | // This file exposes six functions that help handle the inputs and outputs of the emulator: 5 | // - NES.start({rom, save, frame, vram, audio}) 6 | // - NES.reset() 7 | // - NES.frame() 8 | // - NES.buttonDown(controller, button) 9 | // - NES.buttonUp(controller, button) 10 | var NES = { 11 | 12 | // Emulator globals 13 | loop: null, 14 | fps: 60, 15 | frameTime: 1000 / 60, 16 | cpu_cycles: 0, 17 | 18 | // ROM globals 19 | mapper: 0, 20 | mirroring: 0, 21 | prg: [], 22 | chr: [], 23 | 24 | // Graphics globals 25 | frameCtx: null, 26 | vramCtx: null, 27 | frameData: null, 28 | vramData: null, 29 | frameBuffer: null, 30 | vramBuffer: null, 31 | frameBuffer8: null, 32 | vramBuffer8: null, 33 | frameBuffer32: null, 34 | vramBuffer32: null, 35 | 36 | // Audio globals 37 | leftSamples: [], 38 | rightSamples: [], 39 | currentSample: 0, 40 | sampleRate: 44100, 41 | cyclesToHalt: 0, 42 | 43 | // Controllers globals 44 | controllers: null, 45 | 46 | // Mapper globals 47 | prg_0_bank: 0, 48 | prg_1_bank: 1, 49 | chr_bank: 0, 50 | mirroring_backup: 0, 51 | 52 | // Init the emulator with the rom (binary string), optional save file, and the 2 canvases 53 | init: ({rom, save, frame, vram}) => { 54 | 55 | // Reset emulator 56 | NES.reset(); 57 | 58 | // Parse ROM 59 | NES.parse(rom); 60 | 61 | // Load ROM's content in memory according to current mapper 62 | NES.load(); 63 | 64 | // Init display 65 | NES.frameCtx = frame.getContext("2d"); 66 | NES.frameData = NES.frameCtx.getImageData(0, 0, 256, 240); 67 | NES.frameBuffer = new ArrayBuffer(256 * 240 * 4); 68 | NES.vramBuffer = new ArrayBuffer(512 * 480 * 4); 69 | NES.frameBuffer8 = new Uint8Array(NES.frameBuffer); 70 | NES.frameBuffer32 = new Uint32Array(NES.frameBuffer); 71 | if(vram){ 72 | NES.vramCtx = vram.getContext("2d"); 73 | NES.vramData = NES.vramCtx.getImageData(0, 0, 512, 480); 74 | NES.vramBuffer32 = new Uint32Array(NES.vramBuffer); 75 | NES.vramBuffer8 = new Uint8Array(NES.vramBuffer); 76 | } 77 | 78 | // Init controllers 79 | NES.controllers = { 80 | 1: new Controller(), 81 | 2: new Controller() 82 | }; 83 | 84 | // Controller #1 keys listeners 85 | onkeydown = onkeyup = e => { 86 | NES[e.type]( 87 | 1, 88 | { 89 | 37: Controller.BUTTON_LEFT, 90 | 38: Controller.BUTTON_UP, 91 | 39: Controller.BUTTON_RIGHT, 92 | 40: Controller.BUTTON_DOWN, 93 | 88: Controller.BUTTON_A, // X = A 94 | 67: Controller.BUTTON_B, // C = B 95 | 27: Controller.BUTTON_SELECT, // Esc = Select 96 | 13: Controller.BUTTON_START // Enter = Start 97 | }[e.keyCode] 98 | ) 99 | } 100 | }, 101 | 102 | // Play 103 | play: () => { 104 | NES.loop = setInterval(NES.frame, NES.frameTime); 105 | }, 106 | 107 | // Pause 108 | pause: () => { 109 | clearInterval(NES.loop); 110 | }, 111 | 112 | // Reset 113 | reset: () => { 114 | 115 | // Stop 60 fps loop (if any) 116 | NES.pause(); 117 | 118 | // Reset CPU, PPU, APU 119 | cpu_reset(); 120 | ppu_reset(); 121 | apu_reset(); 122 | 123 | // Send reset interrupt to the CPU 124 | interrupt_requested = 2; 125 | 126 | NES.cyclesToHalt = 0; 127 | 128 | //NES.cpu_cycles = 0; 129 | }, 130 | 131 | // Render a new frame 132 | frame: () => { 133 | 134 | vramCanvas.width ^= 0; 135 | 136 | var cycles; 137 | totalCycles = 0; 138 | endFrame = 0; 139 | 140 | // Repeatedly execute CPU instructions until the frame is fully rendered 141 | while(!endFrame){ 142 | //if (NES.cyclesToHalt === 0) { 143 | cycles = emulate(); 144 | // APU.clockFrameCounter(cycles); 145 | //} else { 146 | // APU.clockFrameCounter(Math.min(NES.cyclesToHalt, 8)); 147 | // NES.cyclesToHalt -= Math.min(NES.cyclesToHalt, 8); 148 | //} 149 | 150 | for (i = cycles; i--;) { 151 | cpu_tick(); 152 | } 153 | } 154 | }, 155 | 156 | keydown: (controller, button) => { 157 | NES.controllers[controller].keydown(button); 158 | }, 159 | 160 | keyup: (controller, button) => { 161 | NES.controllers[controller].keyup(button); 162 | }, 163 | 164 | //haltCycles: (cycles) => { 165 | // NES.cyclesToHalt += cycles; 166 | //} 167 | } -------------------------------------------------------------------------------- /src/papu.js: -------------------------------------------------------------------------------- 1 | // APU 2 | // === 3 | 4 | // Resources: 5 | // - https://www.nesdev.org/apu_ref.txt 6 | // - https://www.emulationonline.com/systems/nes/apu-audio/ 7 | // - https://www.nesdev.org/wiki/APU 8 | // - https://www.nesdev.org/wiki/APU_Pulse 9 | // - https://www.nesdev.org/wiki/APU_Triangle 10 | // - https://www.nesdev.org/wiki/APU_Noise 11 | // - https://www.nesdev.org/wiki/APU_DMC 12 | // - https://www.nesdev.org/wiki/APU_basics 13 | // - https://www.nesdev.org/wiki/APU_Envelope 14 | // - https://www.nesdev.org/wiki/APU_Length_Counter 15 | // - https://www.nesdev.org/wiki/APU_Sweep 16 | // - https://www.nesdev.org/2A03%20technical%20reference.txt 17 | 18 | // Audio contexts, oscillators, gains 19 | a1 = a2 = a3 = a4 = a5 = 0; 20 | o1 = o2 = o3 = o4 = o5 = 0; 21 | g1 = g2 = g3 = g4 = g5 = 0; 22 | 23 | // Status: DMC/frame interrupt + enabled DMC/noise/triangle/pulse 2/pulse 1 channels 24 | apu_status = {I: 0, F:0, P1: 0, P2: 0, T:0, N:0, D:0} 25 | 26 | apu_reset = () => { 27 | if(!a2) a2 = new AudioContext(); 28 | if(!a3) a3 = new AudioContext(); 29 | if(!a4) a4 = new AudioContext(); 30 | if(!a5) a5 = new AudioContext(); 31 | 32 | if(o1) o1.stop(); 33 | if(o2) o2.stop(); 34 | if(o3) o3.stop(); 35 | if(o4) o4.stop(); 36 | if(o5) o5.stop(); 37 | 38 | // Pulse 1 39 | pulse1timer = 0; 40 | pulse1volume = 0; 41 | if(!a1) a1 = new AudioContext(); 42 | o1 = new OscillatorNode(a1, {type: "square", frequency: 0}); 43 | g1 = new GainNode(a1, {gain: 0.1}); 44 | o1.connect(g1).connect(a1.destination); 45 | o1.start(); 46 | 47 | // Pulse 2 48 | pulse2timer = 0; 49 | pulse2volume = 0; 50 | if(!a2) a2 = new AudioContext(); 51 | o2 = new OscillatorNode(a2, {type: "square", frequency: 0}); 52 | g2 = new GainNode(a2, {gain: 0.1}); 53 | o2.connect(g2).connect(a2.destination); 54 | o2.start(); 55 | 56 | // Triangle 57 | triangletimer = 0; 58 | if(!a3) a3 = new AudioContext(); 59 | o3 = new OscillatorNode(a3, {type: "triangle", frequency: 0}); 60 | g3 = new GainNode(a3, {gain: 0.1}); 61 | o3.connect(g3).connect(a3.destination); 62 | o3.start(); 63 | 64 | // Noise 65 | noisetimer = 0; 66 | noisevolume = 0; 67 | if(!a4) a4 = new AudioContext(); 68 | o4 = new OscillatorNode(a4, {frequency: 0}); 69 | real = new Float32Array(128); 70 | imag = new Float32Array(128); 71 | for(var i = 128; i--;){ 72 | real[i] = 3; 73 | imag[i] = -Math.random()/3; 74 | } 75 | wave = a4.createPeriodicWave(real, imag, { disableNormalization: true }); 76 | o4.setPeriodicWave(wave) 77 | g4 = new GainNode(a4, {gain: 0.003 * 15}); 78 | o4.connect(g4).connect(a4.destination); 79 | o4.start(); 80 | } 81 | 82 | // 4015: Status register (read/write) 83 | get_4015 = () => { 84 | //console.log("get 4015"); 85 | return apu_status.P1 + apu_status.P2 << 1 + apu_status.T << 2 + apu_status.N << 3 + apu_status.D << 4 + apu_status.F <<6 + apu_status.I << 7; 86 | } 87 | 88 | set_4015 = (value) => { 89 | //console.log("set 4015"); 90 | apu_status.P1 = value & 1; 91 | apu_status.P2 = (value >> 1) & 0b1; 92 | apu_status.T = (value >> 2) & 0b1; 93 | apu_status.N = (value >> 3) & 0b1; 94 | apu_status.D1 = (value >> 4) & 0b1; 95 | } 96 | 97 | // 4017: Frame counter 98 | set_4017 = (value) => { 99 | // Bit 6: IRQ inhibit flag 100 | // Bit 7: Mode 101 | } 102 | 103 | // 4000-4003: Pulse 1 104 | // ================== 105 | 106 | set_4000 = (value) => { 107 | //console.log("set 4000"); 108 | //pulse1duty = value >> 6; 109 | //pulse1loop = (value >> 5) & 0b1; 110 | //pulse1constant = (value >> 4) & 0b1; 111 | pulse1volume = value & 0b1111; // Volume 112 | g1.gain.value = 0.1 * (pulse1volume/15); 113 | } 114 | 115 | set_4001 = (value) => { 116 | //console.log("set 4001"); 117 | //pulse1enabled = value >> 7; 118 | //pulse1period = (value >> 4) & 0b111; 119 | //pulse1negate = (value >> 3) & 0b1; 120 | //pulse1shift = value & 0b111; 121 | } 122 | 123 | set_4002 = (value) => { 124 | //console.log("set 4002"); 125 | pulse1timer = (pulse1timer & 0b00000000) + value; // Timer low 126 | o1.frequency.setValueAtTime(pulse1timer < 8 ? 0 : (111860.8/(pulse1timer+1)),1); 127 | } 128 | 129 | set_4003 = (value) => { 130 | //console.log("set 4003"); 131 | //pulse1length = value >> 3; 132 | pulse1timer = (pulse1timer & 0b00011111111) + ((value & 0b111) << 8); // Timer high 133 | o1.frequency.setValueAtTime(pulse1timer < 8 ? 0 : (111860.8/(pulse1timer+1)),1); 134 | } 135 | 136 | // 4004-4007: Pulse 2 137 | // ================== 138 | 139 | set_4004 = (value) => { 140 | //console.log("set 4004"); 141 | //pulse2duty = value >> 6; 142 | //pulse2loop = (value >> 5) & 0b1; 143 | //pulse2constant = (value >> 4) & 0b1; 144 | pulse2volume = value & 0b1111; // Volume 145 | g2.gain.value = 0.1 * (pulse2volume/15); 146 | } 147 | 148 | set_4005 = (value) => { 149 | //console.log("set 4005"); 150 | //pulse2enabled = value >> 7; 151 | //pulse2period = (value >> 4) & 0b111; 152 | //pulse2negate = (value >> 3) & 0b1; 153 | //pulse2shift = value & 0b111; 154 | } 155 | 156 | set_4006 = (value) => { 157 | //console.log("set 4006"); 158 | pulse2timer = (pulse2timer & 0b00000000) + value; // Timer low 159 | o2.frequency.setValueAtTime(pulse2timer < 8 ? 0 : (111860.8/(pulse2timer+1)),1); 160 | } 161 | 162 | set_4007 = (value) => { 163 | //console.log("set 4007"); 164 | //pulse2lengthload = value >> 3; 165 | pulse2timer = (pulse2timer & 0b00011111111) + ((value & 0b111) << 8); // Timer high 166 | o2.frequency.setValueAtTime(pulse2timer < 8 ? 0 : (111860.8/(pulse2timer+1)),1); 167 | } 168 | 169 | // 4008-400B: Triangle 170 | // =================== 171 | 172 | set_4008 = (value) => { 173 | //console.log("set 4008"); 174 | //trianglecontrol = (value >> 7); // 1: play, 0: mute 175 | //trianglecounter = value & 0b1111111; 176 | } 177 | 178 | set_400a = (value) => { 179 | //console.log("set 400a"); 180 | triangletimer = (triangletimer & 0b00000000) + value; // Timer low 181 | o3.frequency.setValueAtTime(55930.4/(triangletimer+1),1); 182 | } 183 | 184 | set_400b = (value) => { 185 | //console.log("set 400b"); 186 | triangletimer = (triangletimer & 0b00011111111) + ((value & 0b111) << 8); // Timer high 187 | o3.frequency.setValueAtTime(55930.4/(triangletimer+1),1); 188 | } 189 | 190 | // 400C-400F: Noise 191 | // ================ 192 | 193 | set_400c = (value) => { 194 | console.log("set 400c"); 195 | // ... 196 | noisevolume = value & 0b1111; // Volume 197 | g4.gain.value = 0.003 * (noisevolume/15); 198 | } 199 | 200 | set_400e = (value) => { 201 | console.log("set 400e"); 202 | // ... 203 | noiseperiod = value & 0b1111; 204 | o4.frequency.setValueAtTime(111860.8/[4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068][noiseperiod],1); 205 | } 206 | 207 | set_400f = (value) => { 208 | console.log("set 400f"); 209 | // ... 210 | } 211 | 212 | 213 | // DMC 214 | 215 | set_4010 = (value) => { 216 | console.log("set 4010"); 217 | } 218 | 219 | set_4011 = (value) => { 220 | console.log("set 4011"); 221 | } 222 | 223 | set_4012 = (value) => { 224 | console.log("set 4012"); 225 | } 226 | 227 | set_4013 = (value) => { 228 | console.log("set 4013"); 229 | } -------------------------------------------------------------------------------- /src/ppu.js: -------------------------------------------------------------------------------- 1 | // PPU 2 | // === 3 | 4 | // Resources: 5 | // - https://wiki.nesdev.com/w/index.php/PPU (+ subpages) 6 | // - https://problemkaputt.de/everynes.htm#pictureprocessingunitppu 7 | // - https://austinmorlan.com/posts/nes_rendering_overview/ 8 | // - https://www.gridbugs.org/zelda-screen-transitions-are-undefined-behaviour/ 9 | // - https://www.youtube.com/watch?v=wfrNnwJrujw 10 | // - https://gist.githubusercontent.com/adamveld12/d0398717145a2c8dedab/raw/750246a2b4ee4bb722c2b5bb5e6ba997cdf74661/spec.md 11 | // - https://emulation.gametechwiki.com/index.php/Famicom_Color_Palette 12 | // - http://forums.nesdev.com/viewtopic.php?t=19259 13 | // - http://forums.nesdev.com/viewtopic.php?t=8214 14 | // - https://www.youtube.com/watch?v=wt73KPS_23w 15 | 16 | // PPU memory map (64KB): 17 | 18 | // +-------------+-------+-----------------------------------------------------------+ 19 | // | Address | Size | Use | 20 | // +-------------+-------+-----------------------------------------------------------+ 21 | // | $0000-$1FFF | 8KB | Cartridge space (CHR-ROM or CHR-RAM): | 22 | // | $0000-$0FFF | 4KB | Pattern Table 0 (256 tiles) "left page" | 23 | // | $1000-$1FFF | 4KB | Pattern Table 1 (256 tiles) "right page" | 24 | // +-------------+-------+-----------------------------------------------------------+ 25 | // | $2000-$2FFF | 4KB | VRAM (2KB in the NES, 2KB in the cartridge or mirrored): | 26 | // | $2000-$23BF | 960B | Name Table 0 | 27 | // | $23C0-$23FF | 24B | Attribute Table 0 | 28 | // | $2400-$27BF | 960B | Name Table 1 | 29 | // | $27C0-$27FF | 24B | Attribute Table 1 | 30 | // | $2800-$2BBF | 960B | Name Table 2 | 31 | // | $2BC0-$2BFF | 24B | Attribute Table 2 | 32 | // | $2C00-$2FBF | 960B | Name Table 3 | 33 | // | $2FC0-$2FFF | 24B | Attribute Table 3 | 34 | // +- - - - - - -+- - - -+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ 35 | // | $3000-$3EFF | 3840B | Mirror of $2000-$2EFF (never used by the PPU) | 36 | // +-------------+-------+-----------------------------------------------------------+ 37 | // | $3F00-$3F1F | 32B | Palettes: | 38 | // | $3F00 | 1B | Universal background color | 39 | // | $3F01-$3F03 | 3B | Background palette 0 | 40 | // | $3F04 | 1B | Universal bg color replaces it except in forced blanking | 41 | // | $3F05-$3F07 | 3B | Background palette 1 | 42 | // | $3F08 | 1B | Universal bg color replaces it except in forced blanking | 43 | // | $3F09-$3F0B | 3B | Background palette 2 | 44 | // | $3F0C | 1B | Universal bg color replaces it except in forced blanking | 45 | // | $3F0D-$3F0F | 3B | Background palette 3 | 46 | // | $3F10 | 1B | Mirror of $3F00 | 47 | // | $3F11-$3F13 | 3B | Sprite palette 0 | 48 | // | $3F14 | 1B | N/A (mirror of $3F04) | 49 | // | $3F15-$3F17 | 3B | Sprite palette 1 | 50 | // | $3F18 | 1B | N/A (mirror of $3F08) | 51 | // | $3F19-$3F1B | 3B | Sprite palette 2 | 52 | // | $3F1C | 1B | N/A (mirror of $3F0C) | 53 | // | $3F1D-$3F1F | 3B | Sprite palette 3 | 54 | // +- - - - - - -+- - - -+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ 55 | // | $3F20-$3FFF | 224B | Mirrors of $3F00-$3F1F | 56 | // +-------------+-------+-----------------------------------------------------------+ 57 | // | $4000-$FFFF | 48KB | Mirrors of $0000-$3FFF | 58 | // +-------------+-------+-----------------------------------------------------------+ 59 | 60 | // OAM Memory (256 bytes): 61 | 62 | // +-------------+-------+-----------------------------------------------------------+ 63 | // | Address | Size | Use | 64 | // +-------------+-------+-----------------------------------------------------------+ 65 | // | $00-$FF | 256B | Sprites properties (4 bytes for each) | 66 | // +-------------+-------+-----------------------------------------------------------+ 67 | 68 | // Rendering beam for each frame (one pixel for each PPU tick): 69 | 70 | // x=0 x=256 x=340 71 | // ---+-------------------+-------------------+ 72 | // y=0 | visible area | Horizontal blank | 73 | // | (this is rendered | (prepare sprites | 74 | // y=239 | on the screen) | for the next | 75 | // y=239 | | scanline) | 76 | // ---+-------------------+-------------------+ 77 | // y=240 | idle scanline | 78 | // ---+---------------------------------------+ 79 | // y=241 | vertical blanking (idle) | 80 | // | - 20 scanlines long on NTSC consoles | 81 | // y=260 | - 70 scanlines on PAL consoles | 82 | // ---+-------------------------------------+-+ 83 | // y=261 | pre-render scanline |*| 84 | // or -1 -+-------------------------------------+-+ 85 | 86 | // Globals 87 | // ------- 88 | 89 | var 90 | t, 91 | o, 92 | scanline, 93 | dot, 94 | PPU_mem, 95 | OAM, 96 | vramPixelBuffer, 97 | V_yyy, 98 | V_NN, 99 | V_YYYYY, 100 | V_XXXXX, 101 | T_yyy, 102 | T_NN, 103 | T_YYYYY, 104 | T_XXXXX, 105 | xxx, 106 | scroll_x, 107 | scroll_y, 108 | PPUDATA_read_buffer, 109 | PPUSTATUS_O, 110 | PPUSTATUS_S, 111 | PPUSTATUS_V, 112 | latch, 113 | PPUCTRL_V, 114 | PPUCTRL_H, 115 | PPUCTRL_B, 116 | PPUCTRL_S, 117 | PPUCTRL_I, 118 | PPUMASK_s, 119 | PPUMASK_b, 120 | OAMADDR, 121 | PPUADDR, 122 | endFrame, 123 | 124 | 125 | // System palette 126 | // An array of 64 RGB colors, inspired by the 3DS VC palette, stored in AABBGGRR format. 127 | systemPalette = ( 128 | "777812a0090470810a00a007024040050130531000000000"+ 129 | "bbbe70e32f08b0b50e02d04c0780900a0390880111000000"+ 130 | "ffffb3f95f8af7fb7f67f39f3bf1d84d49f5de0333000000"+ 131 | "ffffeafdcfcdfcfdcfbbfadfaefafebfacfbff9888000000" 132 | ) 133 | .match(/.../g) 134 | .map(c => +("0xff" + c[0] + c[0] + c[1] + c[1] + c[2] + c[2])), 135 | 136 | // Reset the PPU 137 | ppu_reset = () => { 138 | 139 | // Reset PPU memory (64KB) and OAM memory (256b) 140 | PPU_mem = []; 141 | OAM = []; 142 | 143 | // Coordinates of the current pixel 144 | scanline = // Y between 0 and 261 on NTSC (or 311 on PAL) 145 | dot = // X between 0 and 340 (on scanline 261, alternate between 239 and 240 if background rendering is enabled) 146 | 147 | // PPU scrolling is handled via two 15-bit registers called V and T 148 | // In these registers: 149 | // - bits 0-4 (XXXXX) represent the coarse X scrolling 150 | // - bits 5-9 (YYYYY) represent the coarse Y scrolling 151 | // - bits 10-11 (NN) represent the nametable where the scrolling starts 152 | // - bits 12-14 (yyy) represent the fine Y scrolling 153 | // The fine X scrolling (xxx) is stored separately 154 | // The effective X scrolling is equal to: (NN & 0b1) * 256 + XXXXX * 8 + xxx 155 | // The effective Y scrolling is equal to: (NN >> 1) * 240 + YYYYY * 8 + yyy 156 | 157 | // V: value of scroll on the next scanline (V = 0yyyNNYYYYYXXXXX) 158 | V_yyy = 159 | V_NN = 160 | V_YYYYY = 161 | V_XXXXX = 162 | 163 | // T: value of scroll on current scanline (T = 0yyyNNYYYYYXXXXX) 164 | T_yyy = 165 | T_NN = 166 | T_YYYYY = 167 | T_XXXXX = 168 | 169 | // Fine X scroll (xxx, also called w) 170 | xxx = 171 | 172 | // Effective PPU scroll 173 | scroll_x = 174 | scroll_y = 175 | 176 | // PPU Status register 177 | PPUSTATUS_O = 178 | PPUSTATUS_S = 179 | PPUSTATUS_V = 180 | 181 | // PPU latch (also called w) 182 | // It's toggled between 0 and 1 every time PPUSCROLL or PPUADDR get written, and reset to 0 when PPUSTATUS is read 183 | latch = 0; 184 | 185 | // Reset PPUCTRL and PPUMASK registers 186 | set_PPUCTRL(0); 187 | set_PPUMASK(0); 188 | }, 189 | 190 | // Memory access 191 | // ------------- 192 | 193 | // Handle address mirrorings 194 | mirrorAddress = address => { 195 | 196 | // $4000-$FFFF: mirrors of $0000-$3FFF 197 | address &= 0x3FFF; 198 | 199 | // $3F00-$3FFF: palettes and mirrors 200 | if(address > 0x3EFF){ 201 | 202 | address &= 0x3F1F; 203 | 204 | // $3F10: mirror of $3F00 205 | if(address == 0x3F10) address = 0x3F00; 206 | } 207 | 208 | // $3000-$3EFF: mirror of $2000-$2EFF (RAM) 209 | else if(address > 0x2FFF){ 210 | address -= 0x1000; 211 | } 212 | 213 | // $2000-$2FFF: RAM (name tables + attributes tables) 214 | else if(address > 0x1FFF){ 215 | 216 | if(NES.mirroring < 3){ 217 | 218 | // 0: vertical mirroring: 219 | // - $2800-$2BFF is a mirror of $2000-$23FF 220 | // - $2C00-$2FFF is a mirror of $2400-$27FF 221 | 222 | // 1: horizontal mirroring: 223 | // - $2400-$27FF is a mirror of $2000-$23FF 224 | // - $2C00-$2FFF is a mirror of $2800-$2BFF 225 | 226 | // 2: four-screen nametable 227 | // There's no mirroring in this case, the address is not modified 228 | address &= (0x37ff + 0x400 * NES.mirroring); 229 | } 230 | 231 | else if(NES.mirroring == 3){ 232 | // 3: 1-screen mirroring 233 | // $2400-$27FF, $2800-$2BFF and $2C00-$2FFF are mirrors of $2000-$23FF 234 | address &= 0x23FF; 235 | } 236 | } 237 | 238 | return address; 239 | }, 240 | 241 | // Read a byte in memory 242 | ppu_read = address => { 243 | 244 | // $0000-$1FFF: CHR-ROM/RAM 245 | if(address < 0x2000){ 246 | return NES.chr[NES.chr_bank][address]; 247 | } 248 | 249 | // $3F04, $3F08, $3F0C: mirrors of $3F00 (universal background color) except during forced blanking (TODO) 250 | if((address & 0xFFF3) == 0x3F00) address = 0x3F00; 251 | 252 | return PPU_mem[mirrorAddress(address)] || 0; 253 | }, 254 | 255 | // PPU I/O registers 256 | // ----------------- 257 | 258 | // TODO: move all this in memory.js 259 | 260 | // The CPU can read/write at the following addresses in memory to interact with the PPU 261 | 262 | // $2000 (write): set PPU Control Register 1 (PPUCTRL) 263 | set_PPUCTRL = value => { 264 | PPUCTRL_V = (value >> 7) & 1; // bit 7: trigger a NMI on VBlank 265 | //PPUCTRL_P = (value >> 6) & 1 // bit 6: external pin controlling backdrop color (ignored here) 266 | PPUCTRL_H = (value >> 5) & 1; // bit 5: sprite size (0: 8x8, 1: 8x16) 267 | PPUCTRL_B = (value >> 4) & 1; // bit 4: background pattern table (0: $0000, 1: $1000) 268 | PPUCTRL_S = (value >> 3) & 1; // bit 3: sprite pattern table (0: $0000, 1: $1000, ignored in 8x16 mode) 269 | PPUCTRL_I = (value >> 2) & 1; // bit 2: VRAM address increment after reading/writing in PPUDATA (0: 1, 1: 32) 270 | T_NN = value & 0b11; // bits 0-1: update nametable bits in scroll register T 271 | }, 272 | 273 | // $2001 (write): set PPU Control Register 2 (PPUMASK) 274 | set_PPUMASK = value => { 275 | //PPUMASK_RGB = (value >> 5) & 7; // Bits 5-7: red/green/blue emphasis on NTSC, red/blue/green on PAL (ignored here) 276 | PPUMASK_s = (value >> 4) & 1; // Bit 4: show sprites 277 | PPUMASK_b = (value >> 3) & 1; // Bit 3: show background 278 | //PPUMASK_M = (value >> 2) & 1; // Bit 2: show sprites on leftmost 8px-wide column (ignored here) 279 | //PPUMASK_m = (value >> 1) & 1; // Bit 1: show background on leftmost 8px-wide column (ignored here) 280 | //PPUMASK_G = value & 1; // Bit 0: greyscale (all colors are ANDed with $30; ignored here) 281 | }, 282 | 283 | // $2002 (read): get PPU Status Register (PPUSTATUS) 284 | get_PPUSTATUS = () => { 285 | 286 | // Update status 287 | t = cpu_mem[0x2002] = 288 | // 0 + // Bits 0-4: copy of last 5 bits written to a PPU register (can be ignored) 289 | (PPUSTATUS_O << 5) // Bit 5 (O): Sprite overflow (set during sprite evaluation if more than 8 sprites in next scanline, cleared at pre-render line, buggy on the NES) 290 | + (PPUSTATUS_S << 6) // Bit 6 (S): Sprite 0 hit (set when an opaque pixel from sprite 0 overlaps an opaque pixel of the background if both displays are enabled, cleared at pre-render line) 291 | + (PPUSTATUS_V << 7); // Bit 7 (V): VBlank (set at line 241, cleared after reading PPUSTATUS and at pre-render line) 292 | 293 | // Reset PPUSCROLL/PPUADDR latch 294 | latch = 0; 295 | 296 | // Reset VBlank 297 | PPUSTATUS_V = 0; 298 | 299 | // Return status (without the resets) 300 | return t; 301 | }, 302 | 303 | // $2003 (write): set SPR-RAM Address Register (OAMADDR) 304 | set_OAMADDR = address => { 305 | OAMADDR = address; 306 | }, 307 | 308 | // $2004h (read/write): SPR-RAM Data Register (OAMDATA, address must be set first). Not readable on Famicom 309 | get_OAMDATA = () => { 310 | return OAM[OAMADDR]; 311 | }, 312 | 313 | set_OAMDATA = value => { 314 | OAM[OAMADDR] = value; 315 | OAMADDR++; 316 | OAMADDR %= 0x100; 317 | }, 318 | 319 | // $2005 (write twice: vertical, then horizontal): PPU Background Scrolling Offset (PPUSCROLL) 320 | set_PPUSCROLL = value => { 321 | 322 | // Latch equals 1: second write, update vertical scroll 323 | // If value is between 240 and 255, it becomes negative (-16 to -1) and the rendering gets glitchy (ignored here) 324 | if(latch){ 325 | 326 | // Update Y bits of scroll register T (YYYYYyyy = value) 327 | T_YYYYY = value >> 3; 328 | T_yyy = value & 0b111; 329 | } 330 | 331 | // Latch equals 0: first write, update horizontal scroll 332 | else { 333 | 334 | // Update X bits of scroll register T (XXXXXxxx = value) 335 | T_XXXXX = value >> 3; 336 | xxx = value & 0b111; 337 | } 338 | 339 | // Toggle latch 340 | latch ^= 1; 341 | }, 342 | 343 | // $2006 (write twice): VRAM Address Register (PPUADDR) 344 | set_PPUADDR = value => { 345 | 346 | // Latch equals 1: second write, set low byte of address and update X and Y scrolling 347 | if(latch) { 348 | 349 | PPUADDR += value; 350 | 351 | // Update X and Y bits of scroll register T (YYYXXXXX) 352 | T_YYYYY += (value >> 5); // read the three low bits of YYYYY 353 | T_XXXXX = value & 0b11111; 354 | 355 | // Copy T in V, containing the scroll values to be used in the next scanline 356 | V_yyy = T_yyy; 357 | V_YYYYY = T_YYYYY; 358 | V_XXXXX = T_XXXXX; 359 | V_NN = T_NN; 360 | } 361 | // Latch equals 0: first write, set high byte of address and update Y scrolling 362 | else { 363 | 364 | PPUADDR = value << 8; 365 | 366 | // Update Y bits of scroll register T (00yyNNYY) 367 | T_yyy = (value >> 4) & 0b11; // only bits 0 and 1 of yyy are set. Bit 2 is corrupted to 0 368 | T_NN = (value >> 2) & 0b11; 369 | T_YYYYY = (value & 0b11) << 3; // read the two high bits of YYYYY 370 | } 371 | 372 | // Toggle latch 373 | latch ^= 1; 374 | }, 375 | 376 | // $2007h (read/write): VRAM Data Register (PPUDATA, an address must be set using PPUADDR before accessing this) 377 | 378 | // Write 379 | set_PPUDATA = value => { 380 | 381 | // $0000-$1FFF: CHR-ROM/RAM 382 | if(PPUADDR < 0x2000){ 383 | NES.chr[NES.chr_bank][PPUADDR] = value; 384 | } 385 | 386 | PPU_mem[mirrorAddress(PPUADDR)] = value; 387 | 388 | // increment address (1 or 32 depending on bit 2 of PPUCTRL) 389 | PPUADDR += PPUCTRL_I ? 32 : 1; 390 | }, 391 | 392 | // Read 393 | get_PPUDATA = () => { 394 | 395 | // PPUADDR between $0000 and $3EFF: buffered read 396 | // Each read fills a 1-byte buffer and returns the value previously stored in that buffer 397 | if(PPUADDR <= 0x3F00){ 398 | t = PPUDATA_read_buffer; 399 | PPUDATA_read_buffer = ppu_read(PPUADDR); 400 | } 401 | 402 | // PPUADDR higher than $3EFF: direct read 403 | else { 404 | t = ppu_read(PPUADDR); 405 | } 406 | 407 | // increment address (1 or 32 depending on bit 2 of PPUCTRL) 408 | PPUADDR += PPUCTRL_I === 1 ? 32 : 1; 409 | 410 | return t; 411 | }, 412 | 413 | // $4014: (write): copy a 256-byte page of CPU memory into the OAM memory (OAMDMA) 414 | set_OAMDMA = value => { 415 | for(var i = OAMADDR; i < 256; i++){ 416 | OAM[i] = cpu_mem[value * 0x100 + i]; 417 | } 418 | 419 | // Consume 513 CPU cycles 420 | // (or 514 cycles when the current CPU cycle is odd. Ignored here) 421 | for(i = 0; i < 513; i++){ 422 | cpu_tick(); 423 | } 424 | //NES.haltCycles(513) 425 | }, 426 | 427 | // Rendering 428 | // --------- 429 | 430 | // Background: 431 | // The data stored in VRAM represents a 512*480px background, separated in four 256*240px screens 432 | // Each screen can contain 32*30 tiles, and each tile measures 8*8px and can use a 4-color palette 433 | // For each screen, a nametable in VRAM tells which tiles to draw, and an attribute table tells which palettes to use 434 | // Attributes hold four 2-bit values, each of these values indicates which background subpalette to use for a given tile 435 | // 436 | // Attribute table for a given nametable (8*8 attributes): 437 | // 438 | // 0 1 2 3 4 5 6 7 439 | // +--+--+--+--+--+--+--+--+ 440 | // 2xC0 | | | | | | | | | 441 | // +--+--+--+--+--+--+--+--+ 442 | // 2xC8 | | | | | | | | | 443 | // +--+--+--+--+--+--+--+--+ 444 | // 2xD0 | | | | | | | | | 445 | // +--+--+--+--+--+--+--+--+ 446 | // 2xD8 | | | | | | | | | 447 | // +--+--+--+--+--+--+--+--+ 448 | // 2xE0 | | | | | | | | | 449 | // +--+--+--+--+--+--+--+--+ 450 | // 2xE8 | | | | | | | | | 451 | // +--+--+--+--+--+--+--+--+ 452 | // 2xF0 | | | | | | | | | 453 | // +--+--+--+--+--+--+--+--+ 454 | // 2xF8 | | | | | | | | | 455 | // +--+--+--+--+--+--+--+--+ 456 | // 457 | // One attribute: / \ 458 | // / \ 459 | // / \ 460 | // / \ 461 | // / \ 462 | // 463 | // bits 0-1 bits 2-3 464 | // +-------------------+ 465 | // | Tile | Tile | 466 | // | | | 467 | // | X, Y | X+1, Y | 468 | // |---------+---------| 469 | // | Tile | Tile | 470 | // | | | 471 | // | X, Y+1 | X+1,Y+1 | 472 | // +-------------------+ 473 | // bits 4-5 bits 6-7 474 | // 475 | // 476 | // Render one line of the VRAM visualizer and fill a buffer with all the non-transparent pixels 477 | // The line number (y) is a value vetween 0 and 480 478 | drawVramScanline = y => { 479 | 480 | 481 | var i, j, X, Y, nametable, bits, pixel; 482 | 483 | // Reset pixel buffer 484 | vramPixelBuffer = []; 485 | 486 | // Y tile coordinate (0-60) 487 | Y = ~~(y/8); 488 | 489 | // For each tile of the scanline (X tile coordinate between 0 and 64): 490 | for(X = 64; X--;){ 491 | 492 | // Get the nametable address in PPU memory: 493 | // $2000-$23BF: top left screen 494 | // $2400-$27BF: top right screen 495 | // $2800-$2BBF: bottom left screen 496 | // $2C00-$2FBF: bottom right screen 497 | nametable = 0x2000 + (0x800 * (Y > 29)) + (0x400 * (X > 31)); 498 | 499 | // Get the attribute table address in PPU memory: 500 | // $23C0-$23FF: top left screen 501 | // $27C0-$27FF: top right screen 502 | // $2BC0-$2BFF: bottom left screen 503 | // $2FC0-$2FFF: bottom right screen 504 | // attributetable = nametable + 0x3C0; 505 | 506 | // Get the attribute byte for the group including the current tile: 507 | // attribute_X = (X % 32) >> 2; // 0-7 508 | // attribute_Y = (Y % 30) >> 2; // 0-7 509 | // attribute = ppu_read(attributetable + attribute_Y * 8 + attribute_X); 510 | 511 | // Get the attribute's 2-bit value for the current title: 512 | // bits_X = ((X % 32) >> 1) & 1; // 0-1 513 | // bits_Y = ((Y % 30) >> 1) & 1; // 0-1 514 | // bits = ((attribute >> (4 * bits_Y + 2 * bits_X)) & 0b11); // 0-3 515 | 516 | // Golfed here: 517 | bits = ( 518 | ppu_read(nametable + 0x3C0 + ((Y % 30) >> 2) * 8 + ((X % 32) >> 2)) 519 | >> 520 | ( 521 | 4 * (((Y % 30) >> 1) & 1) 522 | + 523 | 2 * (((X % 32) >> 1) & 1) 524 | ) 525 | ) & 0b11; 526 | 527 | // Get the subpalette represented by these bits: 528 | // Background palette is stored at $3F00-$3F0F (16 colors, 4 subpalette of 4 colors) 529 | // The values stored in the palettes are indexes of the 64 system colors (systemPalette) 530 | // The first color of each subpalette is ignored (*), the value at $3F00 (universal background color) is used instead 531 | // (*) during forced blanking (background & sprites disabled), if PPUADDR points to a subpalette's color 0, this color is used as universal background color (TODO) 532 | /*colors = [ 533 | systemPalette[ppu_read(0x3F00 + bits * 4)], // universal background color 534 | systemPalette[ppu_read(0x3F00 + bits * 4 + 1)], // color 1 of current subpalette 535 | systemPalette[ppu_read(0x3F00 + bits * 4 + 2)], // color 2 of current subpalette 536 | systemPalette[ppu_read(0x3F00 + bits * 4 + 3)], // color 3 of current subpalette 537 | ];*/ 538 | 539 | // Get the tile's address: 540 | // The bit B of PPUCTRL tells if the tile's graphics are stored in the first or second CHR-ROM bank ($0000-$0FFF or $1000-$1FFF) 541 | // The tile's index within the current CHR-ROM bank is stored in the nametable 542 | // tile = PPUCTRL_B * 0x1000 + ppu_read(nametable + (Y % 30) * 32 + (X % 32)) 543 | 544 | // Get the pixels values: 545 | // The pixels of each tile are encoded on 2 bits 546 | // The value of a pixel (0-3) corresponds to a color from the current subpalette 547 | // Each tile is stored on 16 bytes, the first 8 bytes represent the "high bit" of each pixel, and the last 8 bytes the "low bit" 548 | //if(X == 0 && Y > 54 && Y < 57 && debug) console.log(y, Y, X, nametable.toString(16), bits, PPUCTRL_B); 549 | 550 | // Let x and y be the coordinates of the pixels to draw inside the current tile (x = 0-7, y = 0-7) 551 | y %= 8; 552 | for(x = 8; x--;){ 553 | 554 | // The current line of 8 pixels is encoded on 2 bytes: 555 | // byte1 = ppu_read(tile * 16 + y); 556 | // byte2 = ppu_read(tile * 16 + y + 8); 557 | 558 | // And the current pixel's value is encoded as: 559 | // pixel = ((byte2 >> (7 - x)) & 1) * 2 + ((byte1 >> (7 - x)) & 1); 560 | 561 | // If the pixel's value is 0, the pixel is considered transparent and the universal background color is rendered 562 | // But if it's opaque (non-zero), its color is stored in the current line's pixel buffer 563 | // This buffer will be useful to render the sprites either in the front or behind the background tiles 564 | 565 | // Golfed here: 566 | t = PPUCTRL_B * 0x1000 + ppu_read(nametable + (Y % 30) * 32 + (X % 32)) * 16 + y; 567 | 568 | if(pixel = ((ppu_read(t + 8) >> (7 - x)) & 1) * 2 + ((ppu_read(t) >> (7 - x)) & 1)){ 569 | vramPixelBuffer[X * 8 + x] = systemPalette[ppu_read(0x3F00 + bits * 4 + pixel)]; 570 | } 571 | 572 | // Debug: Render the pixel on the VRAM visualizer 573 | NES.vramBuffer32[(Y * 8 + y) * 512 + (X * 8 + x)] = systemPalette[ppu_read(0x3F00 + bits * 4 + pixel)]; 574 | } 575 | } 576 | }, 577 | 578 | // Screen: 579 | // Backround rendering and sprite rendering can be enabled or disabled using bits b and s of PPUMASK 580 | // Background pixels are stored in a buffer using drawVramScanline() 581 | // Up to 64 sprites can be drawn on screen, either on the foreground or behind the background tiles 582 | // Sprites are drawn from front to back (sprite 0 to sprite 63) and can overlap 583 | // A background sprite can overlap a foreground sprite, in that case it behaves like a clipping mask (ex: SMB's mushroom coming out of a question block) 584 | // Sprites can measure 8*8px or 8*16px (if bit H of PPUCTRL is set) 585 | // If 8*16px sprites are enabled, two consecutive tiles from the CHR-ROM are drawn on screen (the first one on top, the second one on bottom) with the same palette 586 | // Contrary to background tiles: 587 | // - sprites tiles use a dedicated sprite color palette ($3F10-$3F1F), divided in 4 subpalettes 588 | // - the first color of each subpalette is always "transparent" 589 | // - sprites can be flipped horizontally and/or vertically 590 | // As soon as an opaque pixel of the sprite 0 overlaps an opaque pixel of the background, a "sprite 0 hit" is detected (bit S of PPUSTATUS is set) 591 | 592 | // Each sprite is encoded on 4 bytes in the OAM memory: 593 | // Byte 0: Y coordinate 594 | // Byte 1: 595 | // * 8x8 mode: tile index in current tile bank 596 | // * 8x16 mode: bit 0 = tile bank / bits 1-7 = top tile index 597 | // Byte 2: 598 | // * bits 0-1: palette (4-7) 599 | // * bits 2-4: always 0 600 | // * bit 5: priority (0: in front of the background, 1: behind the background) 601 | // * bit 6: X flip 602 | // * bit 7: Y flip 603 | // Byte 3: X coordinate 604 | 605 | // Render one final scanline on screen (background + sprites): 606 | // The scanline number (y) is a value vetween 0 and 240 607 | drawScanline = y => { 608 | 609 | var i, x, scanlineSprites, spriteScanlineAddress, bits, pixel; 610 | 611 | // Find which sprites are present in the current scanline: 612 | // Reset the list 613 | scanlineSprites = []; 614 | 615 | // Loop on all the sprites 616 | for(i = 0; i < 64; i++){ 617 | 618 | // If the current scanline is between the top of the sprite and its bottom (8px or 16px lower, depending on bit H of PPUCTRL) 619 | if(y >= OAM[i * 4] && y < OAM[i * 4] + (PPUCTRL_H ? 16 : 8)){ 620 | 621 | // If more than 8 sprites are found, set overflow flag (bit 0 of PPUSTATUS) and stop checking 622 | if(scanlineSprites.length == 8) { 623 | PPUSTATUS_O = 1; 624 | break; 625 | } 626 | 627 | // Else, the sprite is visible in the current scanline. Add it to the list 628 | scanlineSprites.push(i); 629 | } 630 | } 631 | 632 | // Draw the scanline's pixels: 633 | for(x = 0; x < 256; x++){ 634 | 635 | // Draw background tiles if background rendering is enabled 636 | // Use universal background color if no background tile is present 637 | // X and Y scrolling are applied when fetching the pixels values inside vramBuffer32 638 | if(PPUMASK_b){ 639 | NES.frameBuffer32[y * 256 + x] = vramPixelBuffer[(x + scroll_x) % 512] || systemPalette[ppu_read(mirrorAddress(0x3F00))]; 640 | } 641 | 642 | // Then, for each sprite from back to front: 643 | for(i = scanlineSprites.length - 1; i >= 0; i--){ 644 | 645 | // If this sprite is present at this pixel (if x is between the left column and right column of the sprite) 646 | if(x >= OAM[scanlineSprites[i] * 4 + 3] && x < OAM[scanlineSprites[i] * 4 + 3] + 8){ 647 | 648 | // Retrieve the sprite's subpalette (bits 0-1 of byte 2 of sprite i in OAM memory) 649 | bits = OAM[scanlineSprites[i] * 4 + 2] & 0b11; 650 | /*colors = [ 651 | , // transparent 652 | systemPalette[PPU_mem[0x3F10 + bits * 4 + 1]], // color 1 of current subpalette 653 | systemPalette[PPU_mem[0x3F10 + bits * 4 + 2]], // color 2 of current subpalette 654 | systemPalette[PPU_mem[0x3F10 + bits * 4 + 3]], // color 3 of current subpalette 655 | ];*/ 656 | 657 | // Retrieve the address of the current sprite's scanline in CHR-ROM: 658 | t = scanlineSprites[i] * 4; 659 | o = OAM[t]; 660 | spriteScanlineAddress = 661 | 662 | // CHR-ROM bank 663 | ( 664 | PPUCTRL_H 665 | 666 | // 8*16 667 | ? (OAM[t + 1] & 1) 668 | 669 | // 8*8 670 | : PPUCTRL_S 671 | ) * 0x1000 672 | 673 | // Tile 674 | + (OAM[t + 1] & (0xFF - PPUCTRL_H)) * 16 675 | 676 | // Scanline 677 | + (OAM[t + 2] & 0b10000000 678 | 679 | // Y flip 680 | ? (7 - y + o + 8 * ((y < o + 8 && PPUCTRL_H) + PPUCTRL_H)) 681 | 682 | // No Y flip 683 | : (8 + y - o - 8 * (y < o + 8)) 684 | ); 685 | 686 | // Get pixel position within the sprite scanline 687 | t = x - OAM[scanlineSprites[i] * 4 + 3]; 688 | 689 | // Handle horizontal flip 690 | o = (OAM[scanlineSprites[i] * 4 + 2] & 0b1000000) ? t : 7 - t; 691 | 692 | // Get current pixel value, and check if it's opaque (value: 1, 2 or 3) 693 | if(pixel = ((ppu_read(spriteScanlineAddress + 8) >> o) & 1) * 2 + ((ppu_read(spriteScanlineAddress) >> o) & 1)){ 694 | 695 | // If sprite rendering is enabled, draw it on the current frame 696 | // But if priority bit is 1 and background rendering is enabled: let the background tile's pixel displayed on top if it's opaque 697 | // TODO: fix excitebike black sprite 0 that shows up behind background 698 | if(((!((OAM[scanlineSprites[i] * 4 + 2] & 0b100000) && vramPixelBuffer[(x + scroll_x) % 512])) || !PPUMASK_b) && PPUMASK_s){ 699 | NES.frameBuffer32[y * 256 + x] = systemPalette[PPU_mem[0x3F10 + bits * 4 + pixel]]; 700 | } 701 | 702 | // Sprite 0 hit detection 703 | if(scanlineSprites[i] === 0 && !PPUSTATUS_S && vramPixelBuffer[(x + scroll_x) % 512] && PPUMASK_s && PPUMASK_b){ 704 | PPUSTATUS_S = 1; 705 | } 706 | } 707 | } 708 | } 709 | } 710 | }, 711 | 712 | // Clock 713 | // ----- 714 | 715 | // Clock one PPU cycle 716 | ppu_tick = () => { 717 | 718 | dot++; 719 | 720 | // The PPU renders one dot (one pixel) per cycle 721 | // At the end of each scanline (341 dots), a new scanline starts 722 | // The screen is complete when 240 scanlines are rendered 723 | if(dot > 340){ 724 | 725 | dot = 0; 726 | scanline++; 727 | 728 | // Update scroll 729 | V_XXXXX = T_XXXXX; 730 | V_NN = (V_NN & 0b10) + (T_NN & 0b01); 731 | 732 | // Visible scanlines (0-239) 733 | if(scanline < 240){ 734 | drawVramScanline((((scanline + scroll_y) % 480) + 240) % 480); // Debug 735 | drawVramScanline((scanline + scroll_y) % 480); 736 | drawScanline(scanline); 737 | 738 | // Update scroll 739 | scroll_x = (V_NN & 0b1) * 256 + V_XXXXX * 8 + xxx; 740 | 741 | // Debug 742 | NES.vramCtx.fillStyle = "pink"; 743 | NES.vramCtx.rect(scroll_x - 2, (scanline + scroll_y - 2) % 480, (scanline == 0 || scanline == 239) ? 256 : 4, 4); 744 | NES.vramCtx.rect((scroll_x - 2 + 256) % 512, (scanline + scroll_y - 2) % 480, 4, 4); 745 | } 746 | 747 | // VBlank starts at scanline 241 (a NMI interrupt is triggered, and the frame is displayed on the canvas) 748 | else if(scanline == 240){ 749 | 750 | // VBlank flag 751 | PPUSTATUS_V = 1; 752 | 753 | // Send NMI interrupt to the CPU 754 | interrupt_requested = 1; 755 | 756 | // Output frameBuffer on canvas 757 | NES.frameData.data.set(NES.frameBuffer8); 758 | NES.frameCtx.putImageData(NES.frameData, 0, 0); 759 | 760 | // Debug (VRAM view) 761 | NES.vramData.data.set(NES.vramBuffer8); 762 | NES.vramCtx.putImageData(NES.vramData, 0, 0); 763 | if(PPUMASK_b){ 764 | NES.vramCtx.fill(); 765 | } 766 | } 767 | 768 | // VBlank ends at the pre-render scanline, and PPUSTATUS is reset 769 | else if(scanline == 260){ 770 | PPUSTATUS_O = 771 | PPUSTATUS_S = 772 | PPUSTATUS_V = 0; 773 | } 774 | 775 | // When the pre-render scanline is completed, a new frame starts 776 | else if(scanline == 261){ 777 | scanline = -1; 778 | endFrame = 1; 779 | 780 | // Update scroll 781 | V_YYYYY = T_YYYYY; 782 | V_yyy = T_yyy; 783 | V_NN = (V_NN & 0b01) + (T_NN & 0b10); 784 | scroll_y = (V_NN >> 1) * 240 + V_YYYYY * 8 + V_yyy; 785 | } 786 | } 787 | } -------------------------------------------------------------------------------- /src/rom.js: -------------------------------------------------------------------------------- 1 | // ROM loader 2 | // ========== 3 | 4 | // This file exposes one function: parse_rom(data) 5 | // It parses a ROM file (iNES 1.0) and sets up the ROM banks and various emulator options. 6 | 7 | // iNes ROM file header (16 bytes): 8 | 9 | // +------+-----------------------------------------------------------------------------------------+ 10 | // | Byte | Use | 11 | // +------+-----------------------------------------------------------------------------------------+ 12 | // | 0-3 | $4E $45 $53 $1A (ASCII chars "NES") | 13 | // +------+-----------------------------------------------------------------------------------------+ 14 | // | 4 | Number of 16KB PRG-ROM banks | 15 | // | 5 | Number of 8KB CHR-ROM banks (if 0 => use 1 CHR-RAM bank) | 16 | // +------+-----------------------------------------------------------------------------------------+ 17 | // | 6 | - bit 0: Nametable mirroring (0 => horizontal / 1 => vertical) | 18 | // | | - bit 1: Cartridge contains battery-backed PRG-RAM (CPU $6000-$7FFF) | 19 | // | | - bit 2: Cartridge contains a 512B trainer (CPU $7000-$71FF) | 20 | // | | - bit 3: Ignore mirroring in bit 0, use 4-screen nametable instead | 21 | // | | - bits 4-7: Bits 0-3 of mapper number | 22 | // +------+-----------------------------------------------------------------------------------------+ 23 | // | 7 | - bit 0: Vs. arcade system | 24 | // | | - bit 1: extra 8KB ROM bank for arcade systems | 25 | // | | - bits 2-3: If bit 3 = 1 and bit 2 = 0: iNES 2.0. Else: iNES 1.0. (Unreliable) | 26 | // | | - bits 4-7: Bits 4-7 of mapper number | 27 | // +------+-----------------------------------------------------------------------------------------+ 28 | // | 8 | - iNES 1.0: Number of 8KB PRG-RAM banks (if 0 => add 1 bank for better compatibility) | 29 | // | | - iNES 2.0: Submapper (bits 0-4), bits 8-11 of mapper number (bits 5-8) | 30 | // +------+-----------------------------------------------------------------------------------------+ 31 | // | 9 | - iNES 1.0: TV system (0: NTSC / 1: PAL). (Unreliable) | 32 | // | | - iNES 2.0: bits 8-11 of CHR-ROM size (bits 0-3), bits 8-11 of PRG-ROM size (bits 4-7) | 33 | // +------+-----------------------------------------------------------------------------------------+ 34 | // | 10 | iNES 2.0: PRG-RAM NOT battery-backed (bits 0-3) and battery-backed (bits 4-7) | 35 | // | 11 | iNES 2.0: CHR-RAM NOT battery-backed (bits 0-3) and battery-backed (bits 4-7) | 36 | // | | (values follow a logarithmic scale. 0: 0 byte / 1-14: 128 * 2^N bytes / 15: reserved) | 37 | // +------+-----------------------------------------------------------------------------------------+ 38 | // | 12 | iNES 2.0: Bit 0: NTSC / PAL Bit 1: both. (Unreliable) | 39 | // +------+-----------------------------------------------------------------------------------------+ 40 | // | 13 | iNES 2.0: Vs. arcade system configuration: PPU mode (bits 0-4), Vs. mode (bits 4-7) | 41 | // +------+-----------------------------------------------------------------------------------------+ 42 | // | 14 | iNES 2.0: amount of extra ROM banks (bits 0-1) | 43 | // +------+-----------------------------------------------------------------------------------------+ 44 | // | 15 | $00 (Reserved) | 45 | // +------+-----------------------------------------------------------------------------------------+ 46 | 47 | // ROM file contents, after the header: 48 | // ------------------------------------ 49 | // - Trainer, if present (0 or 512 bytes) 50 | // - PRG-ROM banks (16384 * x bytes) 51 | // - CHR-ROM banks (8192 * y bytes, a bank contains two 4KB pages) 52 | // - Extra ROM banks, if present (arcade games only) 53 | 54 | // Banks not present in the ROM file: 55 | // ---------------------------------- 56 | // - PRG-RAM banks (save slot) - emulators usually use a save file to simulate this 57 | // - CHR-RAM banks (same as CHR-ROM, readable & writeable) - this is filled directly by the CPU 58 | 59 | // How to detect the iNES format version (ignored here): 60 | // ----------------------------------------------------- 61 | // - If (byte 7 AND $0C) == $08, and the size encoded in bytes 4, 5 and 9 does not exceed the file size, then iNES 2.0 62 | // - Else if (byte 7 AND $0C) == $00, and bytes 12-15 are 0, then iNES 1.0 63 | // - Else, archaic iNES format 64 | 65 | // How to detect the TV system (when it's absent or wrong in the header): 66 | // ---------------------------------------------------------------------- 67 | // - The ROM banks' checksums can be searched in a NES games database like NesCartDB (http://bootgod.dyndns.org:7777) 68 | // - The filename can contain "(E)", "(EUR)" or "(Europe)" for PAL / "(U)", "(USA)", "(J)" or "(Japan)" for NTSC / "(EU)" or "(World)" for both 69 | // - You can also fallback to NTSC if you don't know (if the game is PAL, it will still work, but it'll run 20% faster) 70 | 71 | // Parse a ROM file: 72 | NES.parse = (data, i, j, offset) => { 73 | 74 | // Ensure file starts with chars "NES\x1a" 75 | if(data[0] + data[1] + data[2] + data[3] == 256){ 76 | 77 | // Read useful information from the rom header: 78 | 79 | // Check if the game adds 2 extra KB to the PPU's VRAM to have a 4-screen nametable (byte 6, bit 3) 80 | // Otherwise, read mirroring layout (byte 6, bit 0) 81 | // 0 => vertical mirroring (bit 0 on: the game can scroll horizontally) 82 | // 1 => horizontal mirroring (bit 0 off: the game can scroll vertically) 83 | // 2 => 4-screen nametable (bit 4 on: the game can scroll horizontally and vertically) 84 | // 3 => 1-screen mirroring (can be enabled by some mappers but can't be set in the iNES header) 85 | NES.mirroring = (data[6] & 0b00001000) ? 2 : (data[6] & 0b0000001) ? 0 : 1; 86 | 87 | // Check if the game has at least one battery-backed PRG-RAM bank (byte 6, bit 1) 88 | // This is a persistent save slot that can be used to save the player's progress in a game 89 | // If present, it can be accessed by the CPU at the addresses $6000-$7FFF (ignored for now) 90 | // batteryRam = (data.charCodeAt(6) & 0b0000010); 91 | 92 | // Mapper number (byte 6, bits 4-7 >> 4 + byte 7, bits 4-7) 93 | // iNes 2.0 ROMs contain more mapper bits on byte 8 94 | NES.mapper = (data[6] >> 4) + (data[7] & 0b11110000); 95 | 96 | // Skip header 97 | offset = 16; 98 | 99 | // Skip 512b trainer, if it's present (byte 6, bit 2) 100 | // This ROM bank is only used by special hardware or rom hacks, so it can be ignored 101 | // (if present, it's usually mapped to the memory addresses $7000-$71FF) 102 | if(data[6] & 0b00000100) offset += 512; 103 | 104 | // Load the PRG-ROM banks containing the game's code 105 | // The number of 16KB PRG-ROM banks is stored on byte 4 of the header 106 | NES.prg = []; 107 | for(i = 0; i < data[4]; i++){ 108 | NES.prg[i] = []; 109 | for(j = 0; j < 16 * 1024; j++){ 110 | NES.prg[i][j] = data[offset++]; 111 | } 112 | } 113 | 114 | // Load the CHR-ROM banks 115 | // The number of 8KB CHR-ROM banks is stored on byte 5 of the header 116 | // Each bank contains 2 banks of 256 8*8px, 4-color bitmap tiles 117 | // If only one bank is present, it is mirrored (ignored here, no game seems to actually do that) 118 | NES.chr = [[]]; 119 | for(i = 0; i < data[5]; i++){ 120 | NES.chr[i] = []; 121 | for(j = 0; j < 8 * 1024; j++){ 122 | NES.chr[i][j] = data[offset++]; 123 | } 124 | } 125 | } 126 | } --------------------------------------------------------------------------------