UNPKG

nes-emu

Version:

A NES emulator

164 lines (147 loc) 6.05 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _LoopyAddress = _interopRequireDefault(require("./LoopyAddress")); var _helpers = require("../../helpers"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * 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.default(); // v (current VRAM address) this.tAddress = new _LoopyAddress.default(); // t (temporary VRAM address) this.fineX = 0; // x (fine X scroll) this.latch = false; // w (first or second write toggle) } /** Executed on `PPUCtrl` writes (updates `nameTableId` of `t`). */ onPPUCtrlWrite(byte) { // $2000 write // t: ...GH.. ........ <- d: ......GH // <used elsewhere> <- d: ABCDEF.. this.tAddress.nameTableId = _helpers.Byte.getBits(byte, 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(byte) { if (!this.latch) { // $2005 first write (w is 0) // t: ....... ...ABCDE <- d: ABCDE... // x: FGH <- d: .....FGH // w: <- 1 this.tAddress.coarseX = _helpers.Byte.getBits(byte, 3, 5); this.fineX = _helpers.Byte.getBits(byte, 0, 3); } else { // $2005 second write (w is 1) // t: FGH..AB CDE..... <- d: ABCDEFGH // w: <- 0 this.tAddress.coarseY = _helpers.Byte.getBits(byte, 3, 5); this.tAddress.fineY = _helpers.Byte.getBits(byte, 0, 3); } this.latch = !this.latch; } /** Executed on `PPUAddr` writes (updates everything in a weird way, copying `t` to `v`). */ onPPUAddrWrite(byte) { 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 = _helpers.Byte.highPartOf(number); high = _helpers.Byte.setBits(high, 0, 6, _helpers.Byte.getBits(byte, 0, 6)); high = _helpers.Byte.setBits(high, 6, 1, 0); number = _helpers.Byte.to16Bit(high, _helpers.Byte.lowPartOf(number)); this.tAddress.update(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 = _helpers.Byte.to16Bit(_helpers.Byte.highPartOf(number), byte); this.tAddress.update(number); this.vAddress.update(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(); } /** Executed multiple times for each visible line (prefetch dots were ignored). */ onVisibleLine(cycle) { /** * 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(); } /** 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(); } /** 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.update(saveState.v); this.tAddress.update(saveState.t); this.fineX = saveState.x; this.latch = saveState.w; } _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.update(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.update(v & 0b000010000011111 | t & 0b111101111100000); } } exports.default = LoopyRegister;