UNPKG

broken-neees

Version:

A really broken NEEES emulator that introduces glitches and random bugs on purpose!

284 lines (256 loc) 9.54 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _byte = _interopRequireDefault(require("../byte")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const LOOPY_ADDR_COARSE_X_OFFSET = 0; const LOOPY_ADDR_COARSE_X_MASK = 0b11111; const LOOPY_ADDR_COARSE_Y_OFFSET = 5; const LOOPY_ADDR_COARSE_Y_MASK = 0b11111; const LOOPY_ADDR_BASE_NAME_TABLE_ID_OFFSET = 10; const LOOPY_ADDR_BASE_NAME_TABLE_ID_MASK = 0b11; const LOOPY_ADDR_FINE_Y_OFFSET = 12; const LOOPY_ADDR_FINE_Y_MASK = 0b111; const NAME_TABLE_OFFSETS = [1, -1, 1, -1]; const TILE_SIZE_PIXELS = 8; const SCREEN_WIDTH = 256; /** * PPU's internal register (discovered by a user called `loopy` on nesdev). * It contains important data related to Name table scrolling. * Every write to `PPUAddr`, `PPUScroll`, and `PPUCtrl` changes its state. * It's also changed multiple times by the PPU during render. */ class LoopyRegister { constructor() { this.vAddress = new LoopyAddress(); // v (current VRAM address) this.tAddress = new LoopyAddress(); // t (temporary VRAM address) this.fineX = 0; // x (fine X scroll) this.latch = false; // w (first or second write toggle) } /** * Returns the scrolled X in Name table coordinates ([0..262]). * If this value overflows (> 255), switch the horizontal Name table. */ scrolledX(x) { const { vAddress, fineX } = this; return vAddress.coarseX * TILE_SIZE_PIXELS + fineX + x % TILE_SIZE_PIXELS; } /** Returns the scrolled Y in Name table coordinates ([0..255]). */ scrolledY() { const { vAddress } = this; return vAddress.coarseY * TILE_SIZE_PIXELS + vAddress.fineY; } /** * Returns the appropriate Name table id for a `scrolledX`. * It switches the horizontal Name table if scrolledX has overflowed. */ nameTableId(scrolledX) { const baseNameTableId = this.vAddress.nameTableId; const offset = scrolledX >= SCREEN_WIDTH ? NAME_TABLE_OFFSETS[baseNameTableId] : 0; return baseNameTableId + offset; } /** Executed on `PPUCtrl` writes (updates `nameTableId` of `t`). */ onPPUCtrlWrite(value) { // $2000 write // t: ...GH.. ........ <- d: ......GH // <used elsewhere> <- d: ABCDEF.. this.tAddress.nameTableId = _byte.default.getBits(value, 0, 2); } /** Executed on `PPUStatus` reads (resets `latch`). */ onPPUStatusRead() { // $2002 read // w: <- 0 this.latch = false; } /** Executed on `PPUScroll` writes (updates X and Y scrolling on `t`). */ onPPUScrollWrite(value) { if (!this.latch) { // $2005 first write (w is 0) // t: ....... ...ABCDE <- d: ABCDE... // x: FGH <- d: .....FGH // w: <- 1 this.tAddress.coarseX = _byte.default.getBits(value, 3, 5); this.fineX = _byte.default.getBits(value, 0, 3); } else { // $2005 second write (w is 1) // t: FGH..AB CDE..... <- d: ABCDEFGH // w: <- 0 this.tAddress.coarseY = _byte.default.getBits(value, 3, 5); this.tAddress.fineY = _byte.default.getBits(value, 0, 3); } this.latch = !this.latch; } /** Executed on `PPUAddr` writes (updates everything in a weird way, copying `t` to `v`). */ onPPUAddrWrite(value) { if (!this.latch) { // $2006 first write (w is 0) // t: .CDEFGH ........ <- d: ..CDEFGH // <unused> <- d: AB...... // t: Z...... ........ <- 0 (bit Z is cleared) // w: <- 1 let number = this.tAddress.toNumber(); let high = _byte.default.highByteOf(number); high = _byte.default.setBits(high, 0, 6, _byte.default.getBits(value, 0, 6)); high = _byte.default.setBits(high, 6, 1, 0); number = _byte.default.buildU16(high, _byte.default.lowByteOf(number)); this.tAddress.setValue(number); } else { // $2006 second write (w is 1) // t: ....... ABCDEFGH <- d: ABCDEFGH // v: <...all bits...> <- t: <...all bits...> // w: <- 0 let number = this.tAddress.toNumber(); number = _byte.default.buildU16(_byte.default.highByteOf(number), value); this.tAddress.setValue(number); this.vAddress.setValue(number); } this.latch = !this.latch; } /** Executed multiple times for each pre line. */ onPreLine(cycle) { /** * During dots 280 to 304 of the pre-render scanline (end of vblank) * If rendering is enabled, at the end of vblank, shortly after the horizontal bits are copied * from t to v at dot 257, the PPU will repeatedly copy the vertical bits from t to v from * dots 280 to 304, completing the full initialization of v from t. */ if (cycle >= 280 && cycle <= 304) this._copyY(); this._onLine(cycle); } /** Executed multiple times for each visible line. */ onVisibleLine(cycle) { this._onLine(cycle); } /** Executed multiple times for each visible line (prefetch dots were ignored). */ onPlot(x) { const cycle = x + 1; /** * Between dot 328 of a scanline, and 256 of the next scanline * If rendering is enabled, the PPU increments the horizontal position in v many times * across the scanline, it begins at dots 328 and 336, and will continue through the next * scanline at 8, 16, 24... 240, 248, 256 (every 8 dots across the scanline until 256). * Across the scanline the effective coarse X scroll coordinate is incremented repeatedly, * which will also wrap to the next nametable appropriately. */ if (cycle >= 8 && cycle <= 256 && cycle % 8 === 0) this.vAddress.incrementX(); } /** Returns a snapshot of the current state. */ getSaveState() { return { v: this.vAddress.toNumber(), t: this.tAddress.toNumber(), x: this.fineX, w: this.latch }; } /** Restores state from a snapshot. */ setSaveState(saveState) { this.vAddress.setValue(saveState.v); this.tAddress.setValue(saveState.t); this.fineX = saveState.x; this.latch = saveState.w; } /** Executed multiple times for each line. */ _onLine(cycle) { /** * At dot 256 of each scanline * If rendering is enabled, the PPU increments the vertical position in v. The effective Y * scroll coordinate is incremented, which is a complex operation that will correctly skip * the attribute table memory regions, and wrap to the next nametable appropriately. */ if (cycle === 256) this.vAddress.incrementY(); /** * At dot 257 of each scanline * If rendering is enabled, the PPU copies all bits related to horizontal position from t to v. */ if (cycle === 257) this._copyX(); } _copyX() { // (copies all bits related to horizontal position from `t` to `v`) const v = this.vAddress.toNumber(); const t = this.tAddress.toNumber(); // v: ....A.. ...BCDEF <- t: ....A.. ...BCDEF this.vAddress.setValue(v & 0b111101111100000 | t & 0b000010000011111); } _copyY() { // (copies all bits related to vertical position from `t` to `v`) const v = this.vAddress.toNumber(); const t = this.tAddress.toNumber(); // v: GHIA.BC DEF..... <- t: GHIA.BC DEF..... this.vAddress.setValue(v & 0b000010000011111 | t & 0b111101111100000); } } /** * A VRAM address, used for fetching the right tile during render. * yyy NN YYYYY XXXXX * ||| || ||||| +++++-- coarse X scroll * ||| || +++++-------- coarse Y scroll * ||| ++-------------- nametable select * +++----------------- fine Y scroll */ exports.default = LoopyRegister; class LoopyAddress { constructor() { this.coarseX = 0; this.coarseY = 0; this.nameTableId = 0; this.fineY = 0; } /** Increments X, wrapping when needed. */ incrementX() { if (this.coarseX === 31) { this.coarseX = 0; this._switchHorizontalNameTable(); } else { this.coarseX++; } } /** Increments Y, wrapping when needed. */ incrementY() { if (this.fineY < 7) { this.fineY++; } else { this.fineY = 0; if (this.coarseY === 29) { this.coarseY = 0; this._switchVerticalNameTable(); } else if (this.coarseY === 31) { this.coarseY = 0; } else { this.coarseY++; } } } /** Converts the address to a 15-bit number. */ toNumber() { return this.coarseX << LOOPY_ADDR_COARSE_X_OFFSET | this.coarseY << LOOPY_ADDR_COARSE_Y_OFFSET | this.nameTableId << LOOPY_ADDR_BASE_NAME_TABLE_ID_OFFSET | this.fineY << LOOPY_ADDR_FINE_Y_OFFSET; } /** * Returns the value as a 14-bit number. * The v register has 15 bits, but the PPU memory space is only 14 bits wide. * The highest bit is unused for access through $2007. */ getValue() { return this.toNumber() & 0b11111111111111; } /** Updates the address from a 15-bit number. */ setValue(number) { this.coarseX = number >> LOOPY_ADDR_COARSE_X_OFFSET & LOOPY_ADDR_COARSE_X_MASK; this.coarseY = number >> LOOPY_ADDR_COARSE_Y_OFFSET & LOOPY_ADDR_COARSE_Y_MASK; this.nameTableId = number >> LOOPY_ADDR_BASE_NAME_TABLE_ID_OFFSET & LOOPY_ADDR_BASE_NAME_TABLE_ID_MASK; this.fineY = number >> LOOPY_ADDR_FINE_Y_OFFSET & LOOPY_ADDR_FINE_Y_MASK; } _switchHorizontalNameTable() { this.nameTableId = this.nameTableId ^ 0b1; } _switchVerticalNameTable() { this.nameTableId = this.nameTableId ^ 0b10; } }