broken-neees
Version:
A really broken NEEES emulator that introduces glitches and random bugs on purpose!
376 lines (352 loc) • 20.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _CPUMemory = _interopRequireDefault(require("./CPUMemory"));
var _CPU = _interopRequireDefault(require("./cpu/CPU"));
var _PPU = _interopRequireDefault(require("./ppu/PPU"));
var _APU = _interopRequireDefault(require("./apu/APU"));
var _Cartridge = _interopRequireDefault(require("./Cartridge"));
var _Controller = _interopRequireDefault(require("./controller/Controller"));
var _mappers = _interopRequireDefault(require("./mappers/mappers"));
var _interrupts = _interopRequireDefault(require("./lib/cpu/interrupts"));
var _saveStates = _interopRequireDefault(require("./saveStates"));
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 ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return 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 PPU_STEPS_PER_CPU_CYCLE = 3;
var APU_STEPS_PER_CPU_CYCLE = 0.5;
var TIMEOUT_MS = 3000;
/** The NEEES Emulator. */
var _default = function _default() {
var customComponents = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return /*#__PURE__*/function () {
function NEEES() {
var _this = this;
var onFrame = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {};
var onSample = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {};
_classCallCheck(this, NEEES);
this.customComponents = _objectSpread({}, customComponents);
var components = _objectSpread({}, customComponents);
if (!components.CPUMemory) components.CPUMemory = _CPUMemory.default;
if (!components.CPU) components.CPU = _CPU.default;
if (!components.PPU) components.PPU = _PPU.default;
if (!components.APU) components.APU = _APU.default;
if (!components.Cartridge) components.Cartridge = _Cartridge.default;
if (!components.Controller) components.Controller = _Controller.default;
if (!components.mappers) components.mappers = _mappers.default;
if (!components.omitReset) components.omitReset = false;
if (!components.unbroken) components.unbroken = false;
this.components = components;
this.onFrame = onFrame;
this.onSample = function (sample, pulse1, pulse2, triangle, noise, dmc) {
_this.sampleCount++;
onSample(sample, pulse1, pulse2, triangle, noise, dmc);
};
var cpuMemory = null;
try {
cpuMemory = new this.components.CPUMemory();
} catch (error) {
throw new Error("🐒 Failure instantiating new CPUMemory(): " + (error === null || error === void 0 ? void 0 : error.message));
}
try {
this.cpu = new this.components.CPU(cpuMemory);
if (components.unbroken) this.cpu.unbroken = true;
} catch (error) {
throw new Error("🐒 Failure instantiating new CPU(): " + (error === null || error === void 0 ? void 0 : error.message));
}
try {
this.ppu = new this.components.PPU(this.cpu);
} catch (error) {
throw new Error("🐒 Failure instantiating new PPU(cpu): " + (error === null || error === void 0 ? void 0 : error.message));
}
try {
this.apu = new this.components.APU(this.cpu);
} catch (error) {
throw new Error("🐒 Failure instantiating new APU(cpu): " + (error === null || error === void 0 ? void 0 : error.message));
}
this._patchCPUMemory();
this.sampleCount = 0;
this.pendingPPUCycles = 0;
this.pendingAPUCycles = 0;
this.onScanline = null;
}
/** Loads a `rom` as the current cartridge. */
_createClass(NEEES, [{
key: "load",
value: function load(rom) {
var saveFileBytes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
var cartridge = new this.components.Cartridge(rom);
if (this.components.unbroken) cartridge.unbroken = true;
var mapper = this.components.mappers.create(this.cpu, this.ppu, cartridge);
var controller1 = new this.components.Controller(1);
var controller2 = new this.components.Controller(2);
controller1.setOtherController(controller2);
controller2.setOtherController(controller1);
var controllers = [controller1, controller2];
try {
this.cpu.memory.onLoad(this.ppu, this.apu, mapper, controllers);
} catch (error) {
throw new Error("🐒 CPU::memory::onLoad(...) failed: " + (error === null || error === void 0 ? void 0 : error.message));
}
try {
var _this$ppu$onLoad, _this$ppu;
(_this$ppu$onLoad = (_this$ppu = this.ppu).onLoad) === null || _this$ppu$onLoad === void 0 ? void 0 : _this$ppu$onLoad.call(_this$ppu, mapper);
} catch (error) {
throw new Error("🐒 PPU::onLoad(...) failed: " + (error === null || error === void 0 ? void 0 : error.message));
}
try {
var _this$ppu$memory, _this$ppu$memory$onLo;
(_this$ppu$memory = this.ppu.memory) === null || _this$ppu$memory === void 0 ? void 0 : (_this$ppu$memory$onLo = _this$ppu$memory.onLoad) === null || _this$ppu$memory$onLo === void 0 ? void 0 : _this$ppu$memory$onLo.call(_this$ppu$memory, cartridge, mapper);
} catch (error) {
throw new Error("🐒 PPU::memory::onLoad(...) failed: " + (error === null || error === void 0 ? void 0 : error.message));
}
this.pendingPPUCycles = 0;
this.pendingAPUCycles = 0;
this.context = {
cartridge: cartridge,
mapper: mapper,
controllers: controllers
};
try {
if (!this.components.omitReset) this.cpu.interrupt(_interrupts.default.RESET);
} catch (error) {
throw new Error("🐒 RESET interrupt failed: " + (error === null || error === void 0 ? void 0 : error.message));
}
this._setSaveFile(saveFileBytes);
}
/** Runs the emulation for a whole video frame. */
}, {
key: "frame",
value: function frame() {
if (!this.context) return;
var startTime = Date.now();
var currentFrame = this.ppu.frame;
while (this.ppu.frame === currentFrame) {
this.step();
if (Date.now() > startTime + TIMEOUT_MS) {
throw new Error("\uD83D\uDC12 The PPU is taking more than ".concat(TIMEOUT_MS, " ms to generate a single frame. This looks bad!"));
}
}
}
/** Runs the emulation until the audio system generates `requestedSamples`. */
}, {
key: "samples",
value: function samples(requestedSamples) {
if (!this.context) return;
var startTime = Date.now();
this.sampleCount = 0;
while (this.sampleCount < requestedSamples) {
this.step();
if (Date.now() > startTime + TIMEOUT_MS) {
throw new Error("\uD83D\uDC12 The APU is taking more than ".concat(TIMEOUT_MS, " ms to generate ").concat(requestedSamples, " samples. This looks bad!"));
}
}
}
/** Runs the emulation until the next scanline. */
}, {
key: "scanline",
value: function scanline() {
var debug = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
if (!this.context) return;
var startTime = Date.now();
var currentScanline = this.ppu.scanline;
while (this.ppu.scanline === currentScanline) {
this.step();
if (Date.now() > startTime + TIMEOUT_MS) {
throw new Error("\uD83D\uDC12 The PPU is taking more than ".concat(TIMEOUT_MS, " ms to generate a single scanline. This looks bad!"));
}
}
var oldFrameBuffer;
if (debug) {
oldFrameBuffer = new Uint32Array(this.ppu.frameBuffer.length);
for (var i = 0; i < this.ppu.frameBuffer.length; i++) oldFrameBuffer[i] = this.ppu.frameBuffer[i];
for (var _i = 0; _i < 256; _i++) this.ppu.plot(_i, this.ppu.scanline, 0xff0000ff);
}
this.onFrame(this.ppu.frameBuffer);
if (debug) {
for (var _i2 = 0; _i2 < this.ppu.frameBuffer.length; _i2++) this.ppu.frameBuffer[_i2] = oldFrameBuffer[_i2];
}
}
/** Executes a step in the emulation (1 CPU instruction). */
}, {
key: "step",
value: function step() {
var cpuCycles = this.cpu.step();
cpuCycles = this._clockPPU(cpuCycles);
this._clockAPU(cpuCycles);
}
/** Sets the `button` state of `player` to `isPressed`. */
}, {
key: "setButton",
value: function setButton(player, button, isPressed) {
if (!this.context) return;
if (player !== 1 && player !== 2) throw new Error("Invalid player: ".concat(player, "."));
this.context.controllers[player - 1].update(button, isPressed);
}
/** Sets all buttons of `player` to a non-pressed state. */
}, {
key: "clearButtons",
value: function clearButtons(player) {
if (!this.context) return;
if (player !== 1 && player !== 2) throw new Error("Invalid player: ".concat(player, "."));
this.context.controllers[player - 1].clear();
}
/** Returns the PRG RAM bytes, or null. */
}, {
key: "getSaveFile",
value: function getSaveFile() {
if (!this.context) return;
var prgRam = this.context.mapper.prgRam;
if (!prgRam) return null;
var bytes = [];
for (var i = 0; i < prgRam.length; i++) bytes[i] = prgRam[i];
return bytes;
}
/** Returns a snapshot of the current state. */
}, {
key: "getSaveState",
value: function getSaveState() {
if (!this.context) return;
return _saveStates.default.getSaveState(this);
}
/** Restores state from a snapshot. */
}, {
key: "setSaveState",
value: function setSaveState(_saveState) {
if (!this.context) return;
var saveState = JSON.parse(JSON.stringify(_saveState)); // deep copy
_saveStates.default.setSaveState(this, saveState);
this._setSaveFile(saveState.saveFile);
}
}, {
key: "_clockPPU",
value: function _clockPPU(cpuCycles) {
var _this2 = this;
var scanline = this.ppu.scanline;
var unitCycles = this.pendingPPUCycles + cpuCycles * PPU_STEPS_PER_CPU_CYCLE;
this.pendingPPUCycles = 0;
var onIntr = function onIntr(interrupt) {
var newCPUCycles = _this2.cpu.interrupt(interrupt);
cpuCycles += newCPUCycles;
_this2.pendingPPUCycles += newCPUCycles * PPU_STEPS_PER_CPU_CYCLE;
};
for (var i = 0; i < unitCycles; i++) {
// <optimization>
if (this.ppu.cycle > 1 && this.ppu.cycle < 256 || this.ppu.cycle > 260 && this.ppu.cycle < 304 || this.ppu.cycle > 304 && this.ppu.cycle < 340) {
this.ppu.cycle++;
continue;
}
// </optimization>
this.ppu.step(this.onFrame, onIntr);
}
if (this.ppu.scanline !== scanline && this.onScanline != null) this.onScanline();
return cpuCycles;
}
}, {
key: "_clockAPU",
value: function _clockAPU(cpuCycles) {
var _this3 = this;
var unitCycles = this.pendingAPUCycles + cpuCycles * APU_STEPS_PER_CPU_CYCLE;
var onIntr = function onIntr(interrupt) {
var newCPUCycles = _this3.cpu.interrupt(interrupt);
unitCycles += newCPUCycles * APU_STEPS_PER_CPU_CYCLE;
_this3.pendingPPUCycles += newCPUCycles * PPU_STEPS_PER_CPU_CYCLE;
};
while (unitCycles >= 1) {
this.apu.step(this.onSample, onIntr);
unitCycles--;
}
this.pendingAPUCycles = unitCycles;
}
}, {
key: "_setSaveFile",
value: function _setSaveFile(prgRamBytes) {
var prgRam = this.context.mapper.prgRam;
if (!prgRam || !prgRamBytes) return;
for (var i = 0; i < prgRamBytes.length; i++) prgRam[i] = prgRamBytes[i];
}
}, {
key: "_patchCPUMemory",
value: function _patchCPUMemory() {
if (!this.cpu.memory) throw new Error("🐒 CPU::memory not found");
this._patchCPUMemoryReads();
this._patchCPUMemoryWrites();
}
}, {
key: "_patchCPUMemoryReads",
value: function _patchCPUMemoryReads() {
var components = this.customComponents;
var memory = this.cpu.memory;
var read = memory.read;
if (!read) throw new Error("🐒 CPU::memory::read(...) not found");
memory.read = function (address) {
// PPU registers
if (!components.PPU) {
var _this$ppu$registers$r, _this$ppu$registers, _this$ppu$registers$r2, _this$ppu$registers$r3, _this$ppu$registers2, _this$ppu$registers2$;
if (address >= 0x2000 && address <= 0x2007 || address === 0x4014) return (_this$ppu$registers$r = (_this$ppu$registers = this.ppu.registers) === null || _this$ppu$registers === void 0 ? void 0 : (_this$ppu$registers$r2 = _this$ppu$registers.read) === null || _this$ppu$registers$r2 === void 0 ? void 0 : _this$ppu$registers$r2.call(_this$ppu$registers, address)) !== null && _this$ppu$registers$r !== void 0 ? _this$ppu$registers$r : 0;else if (address >= 0x2008 && address <= 0x3fff) return (_this$ppu$registers$r3 = (_this$ppu$registers2 = this.ppu.registers) === null || _this$ppu$registers2 === void 0 ? void 0 : (_this$ppu$registers2$ = _this$ppu$registers2.read) === null || _this$ppu$registers2$ === void 0 ? void 0 : _this$ppu$registers2$.call(_this$ppu$registers2, 0x2000 + (address - 0x2008) % 0x0008)) !== null && _this$ppu$registers$r3 !== void 0 ? _this$ppu$registers$r3 : 0;
}
// APU registers
if (!components.APU) {
var _this$apu$registers$r, _this$apu$registers, _this$apu$registers$r2;
if (address >= 0x4000 && address <= 0x4013 || address === 0x4015) return (_this$apu$registers$r = (_this$apu$registers = this.apu.registers) === null || _this$apu$registers === void 0 ? void 0 : (_this$apu$registers$r2 = _this$apu$registers.read) === null || _this$apu$registers$r2 === void 0 ? void 0 : _this$apu$registers$r2.call(_this$apu$registers, address)) !== null && _this$apu$registers$r !== void 0 ? _this$apu$registers$r : 0;
}
// Controller ports
if (!components.Controller) {
if (address === 0x4016) return this.controllers[0].onRead();else if (address === 0x4017) return this.controllers[1].onRead();
}
// APU and I/O functionality that is normally disabled
if (address >= 0x4018 && address <= 0x401f) return 0;
// Cartridge space: PRG ROM, PRG RAM, and mapper registers
if (!components.mappers) {
if (address >= 0x4020 && address <= 0xffff) return this.mapper.cpuRead(address);
}
// Original method call
return read.call(this, address);
};
}
}, {
key: "_patchCPUMemoryWrites",
value: function _patchCPUMemoryWrites() {
var components = this.customComponents;
var memory = this.cpu.memory;
var write = memory.write;
if (!write) throw new Error("🐒 CPU::memory::write(...) not found");
memory.write = function (address, value) {
// PPU registers
if (!components.PPU) {
var _this$ppu$registers3, _this$ppu$registers3$, _this$ppu$registers4, _this$ppu$registers4$;
if (address >= 0x2000 && address <= 0x2007 || address === 0x4014) return (_this$ppu$registers3 = this.ppu.registers) === null || _this$ppu$registers3 === void 0 ? void 0 : (_this$ppu$registers3$ = _this$ppu$registers3.write) === null || _this$ppu$registers3$ === void 0 ? void 0 : _this$ppu$registers3$.call(_this$ppu$registers3, address, value);else if (address >= 0x2008 && address <= 0x3fff) return (_this$ppu$registers4 = this.ppu.registers) === null || _this$ppu$registers4 === void 0 ? void 0 : (_this$ppu$registers4$ = _this$ppu$registers4.write) === null || _this$ppu$registers4$ === void 0 ? void 0 : _this$ppu$registers4$.call(_this$ppu$registers4, 0x2000 + (address - 0x2008) % 0x0008, value);
}
// APU registers
if (!components.APU) {
var _this$apu$registers2, _this$apu$registers2$;
if (address >= 0x4000 && address <= 0x4013 || address === 0x4015 || address === 0x4017) return (_this$apu$registers2 = this.apu.registers) === null || _this$apu$registers2 === void 0 ? void 0 : (_this$apu$registers2$ = _this$apu$registers2.write) === null || _this$apu$registers2$ === void 0 ? void 0 : _this$apu$registers2$.call(_this$apu$registers2, address, value);
}
// Controller ports
if (!components.Controller) {
if (address === 0x4016) return this.controllers[0].onWrite(value);
}
// APU and I/O functionality that is normally disabled
if (address >= 0x4018 && address <= 0x401f) return;
// Cartridge space: PRG ROM, PRG RAM, and mapper registers
if (!components.mappers) {
if (address >= 0x4020 && address <= 0xffff) return this.mapper.cpuWrite(address, value);
}
// Original method call
return write.call(this, address, value);
};
}
}]);
return NEEES;
}();
};
exports.default = _default;