UNPKG

broken-neees

Version:

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

335 lines (308 loc) 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _byte = _interopRequireDefault(require("../byte")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } var LOOPY_ADDR_COARSE_X_OFFSET = 0; var LOOPY_ADDR_COARSE_X_MASK = 31; var LOOPY_ADDR_COARSE_Y_OFFSET = 5; var LOOPY_ADDR_COARSE_Y_MASK = 31; var LOOPY_ADDR_BASE_NAME_TABLE_ID_OFFSET = 10; var LOOPY_ADDR_BASE_NAME_TABLE_ID_MASK = 3; var LOOPY_ADDR_FINE_Y_OFFSET = 12; var LOOPY_ADDR_FINE_Y_MASK = 7; var NAME_TABLE_OFFSETS = [1, -1, 1, -1]; var TILE_SIZE_PIXELS = 8; var 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. */ var LoopyRegister = /*#__PURE__*/function () { function LoopyRegister() { _classCallCheck(this, LoopyRegister); 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. */ _createClass(LoopyRegister, [{ key: "scrolledX", value: function scrolledX(x) { var vAddress = this.vAddress, fineX = this.fineX; return vAddress.coarseX * TILE_SIZE_PIXELS + fineX + x % TILE_SIZE_PIXELS; } /** Returns the scrolled Y in Name table coordinates ([0..255]). */ }, { key: "scrolledY", value: function scrolledY() { var vAddress = this.vAddress; 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. */ }, { key: "nameTableId", value: function nameTableId(scrolledX) { var baseNameTableId = this.vAddress.nameTableId; var offset = scrolledX >= SCREEN_WIDTH ? NAME_TABLE_OFFSETS[baseNameTableId] : 0; return baseNameTableId + offset; } /** Executed on `PPUCtrl` writes (updates `nameTableId` of `t`). */ }, { key: "onPPUCtrlWrite", value: function 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`). */ }, { key: "onPPUStatusRead", value: function onPPUStatusRead() { // $2002 read // w: <- 0 this.latch = false; } /** Executed on `PPUScroll` writes (updates X and Y scrolling on `t`). */ }, { key: "onPPUScrollWrite", value: function 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`). */ }, { key: "onPPUAddrWrite", value: function 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 var number = this.tAddress.toNumber(); var 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 var _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. */ }, { key: "onPreLine", value: function 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. */ }, { key: "onVisibleLine", value: function onVisibleLine(cycle) { this._onLine(cycle); } /** Executed multiple times for each visible line (prefetch dots were ignored). */ }, { key: "onPlot", value: function onPlot(x) { var 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. */ }, { key: "getSaveState", value: function getSaveState() { return { v: this.vAddress.toNumber(), t: this.tAddress.toNumber(), x: this.fineX, w: this.latch }; } /** Restores state from a snapshot. */ }, { key: "setSaveState", value: function 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. */ }, { key: "_onLine", value: function _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(); } }, { key: "_copyX", value: function _copyX() { // (copies all bits related to horizontal position from `t` to `v`) var v = this.vAddress.toNumber(); var t = this.tAddress.toNumber(); // v: ....A.. ...BCDEF <- t: ....A.. ...BCDEF this.vAddress.setValue(v & 31712 | t & 1055); } }, { key: "_copyY", value: function _copyY() { // (copies all bits related to vertical position from `t` to `v`) var v = this.vAddress.toNumber(); var t = this.tAddress.toNumber(); // v: GHIA.BC DEF..... <- t: GHIA.BC DEF..... this.vAddress.setValue(v & 1055 | t & 31712); } }]); return LoopyRegister; }(); /** * 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; var LoopyAddress = /*#__PURE__*/function () { function LoopyAddress() { _classCallCheck(this, LoopyAddress); this.coarseX = 0; this.coarseY = 0; this.nameTableId = 0; this.fineY = 0; } /** Increments X, wrapping when needed. */ _createClass(LoopyAddress, [{ key: "incrementX", value: function incrementX() { if (this.coarseX === 31) { this.coarseX = 0; this._switchHorizontalNameTable(); } else { this.coarseX++; } } /** Increments Y, wrapping when needed. */ }, { key: "incrementY", value: function 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. */ }, { key: "toNumber", value: function 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. */ }, { key: "getValue", value: function getValue() { return this.toNumber() & 16383; } /** Updates the address from a 15-bit number. */ }, { key: "setValue", value: function 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; } }, { key: "_switchHorizontalNameTable", value: function _switchHorizontalNameTable() { this.nameTableId = this.nameTableId ^ 1; } }, { key: "_switchVerticalNameTable", value: function _switchVerticalNameTable() { this.nameTableId = this.nameTableId ^ 2; } }]); return LoopyAddress; }();