broken-neees
Version:
A really broken NEEES emulator that introduces glitches and random bugs on purpose!
335 lines (308 loc) • 12.8 kB
JavaScript
"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;
}();