nes-emu
Version:
A NES emulator
194 lines (182 loc) • 6.62 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _cpu = _interopRequireDefault(require("./cpu"));
var _ppu = _interopRequireDefault(require("./ppu"));
var _apu = _interopRequireDefault(require("./apu"));
var _Bus = require("./memory/Bus");
var _cartridge = _interopRequireDefault(require("./cartridge"));
var _controller = _interopRequireDefault(require("./controller"));
var _constants = _interopRequireDefault(require("./constants"));
var _helpers = require("./helpers");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/** The NES Emulator. */
class NES {
constructor() {
let onFrame = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : () => {};
let onSample = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : () => {};
let logger = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
_helpers.WithContext.apply(this);
this.onFrame = onFrame;
this.onSample = sample => {
this.sampleCount++;
onSample(sample);
};
this.logger = logger;
this.cpu = new _cpu.default();
this.ppu = new _ppu.default();
this.apu = new _apu.default();
this.sampleCount = 0;
this.pendingPPUCycles = 0;
this.pendingAPUCycles = 0;
}
/** Loads a `rom` as the current cartridge. */
load(rom) {
let saveFileBytes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
const cartridge = new _cartridge.default(rom);
const mapper = cartridge.createMapper();
const controllerPorts = _controller.default.createPorts();
this.loadContext({
nes: this,
logger: this.logger,
cpu: this.cpu,
ppu: this.ppu,
apu: this.apu,
memoryBus: {
cpu: new _Bus.CPUBus(mapper)
},
cartridge,
mapper,
controllers: [new _controller.default(controllerPorts.primary), new _controller.default(controllerPorts.secondary)],
inDebugMode(action) {
try {
this.isDebugging = true;
return action();
} finally {
this.isDebugging = false;
}
}
});
this._setSaveFile(saveFileBytes);
}
/** Runs the emulation for a whole video frame. */
frame() {
this.requireContext();
const currentFrame = this.ppu.frame;
while (this.ppu.frame === currentFrame) this.step();
}
/** Runs the emulation until the audio system generates `requestedSamples`. */
samples(requestedSamples) {
this.requireContext();
this.sampleCount = 0;
while (this.sampleCount < requestedSamples) this.step();
}
/** Runs the emulation until the next scanline. */
scanline() {
let debug = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
this.requireContext();
const currentScanline = this.ppu.scanline;
while (this.ppu.scanline === currentScanline) this.step();
let oldFrameBuffer;
if (debug) {
oldFrameBuffer = new Uint32Array(this.ppu.frameBuffer.length);
for (let i = 0; i < this.ppu.frameBuffer.length; i++) oldFrameBuffer[i] = this.ppu.frameBuffer[i];
for (let i = 0; i < _constants.default.SCREEN_WIDTH; i++) this.ppu.plot(i, this.ppu.scanline, 0xff0000ff);
}
this.onFrame(this.ppu.frameBuffer);
if (debug) {
for (let i = 0; i < this.ppu.frameBuffer.length; i++) this.ppu.frameBuffer[i] = oldFrameBuffer[i];
}
}
/** Executes a step in the emulation (1 CPU instruction). */
step() {
let cpuCycles = this.cpu.step();
cpuCycles = this._clockPPU(cpuCycles);
this._clockAPU(cpuCycles);
}
/** Sets the `button` state of `player` to `isPressed`. */
setButton(player, button, isPressed) {
this.requireContext();
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. */
clearButtons(player) {
this.requireContext();
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. */
getSaveFile() {
this.requireContext();
const {
prgRam
} = this.context.mapper;
if (!prgRam) return null;
const bytes = [];
for (let i = 0; i < prgRam.memorySize; i++) bytes[i] = prgRam.readAt(i);
return bytes;
}
/** Returns a snapshot of the current state. */
getSaveState() {
this.requireContext();
return {
cpu: this.cpu.getSaveState(),
ppu: this.ppu.getSaveState(),
apu: this.apu.getSaveState(),
mapper: this.context.mapper.getSaveState(),
saveFile: this.getSaveFile()
};
}
/** Restores state from a snapshot. */
setSaveState(saveState) {
this.requireContext();
this.cpu.setSaveState(saveState.cpu);
this.ppu.setSaveState(saveState.ppu);
this.apu.setSaveState(saveState.apu);
this.context.mapper.setSaveState(saveState.mapper);
this._setSaveFile(saveState.saveFile);
}
/** When a context is loaded. */
onLoad(context) {
this.pendingPPUCycles = 0;
this.pendingAPUCycles = 0;
context.mapper.loadContext(context);
this.ppu.loadContext(context);
this.apu.loadContext(context);
this.cpu.loadContext(context);
}
_clockPPU(cpuCycles) {
let unitCycles = this.pendingPPUCycles + cpuCycles * _constants.default.PPU_STEPS_PER_CPU_CYCLE;
this.pendingPPUCycles = 0;
this.ppu.step(unitCycles, this.onFrame, interrupt => {
const newCPUCycles = this.cpu.interrupt(interrupt);
cpuCycles += newCPUCycles;
this.pendingPPUCycles += newCPUCycles * _constants.default.PPU_STEPS_PER_CPU_CYCLE;
});
return cpuCycles;
}
_clockAPU(cpuCycles) {
let unitCycles = this.pendingAPUCycles + cpuCycles * _constants.default.APU_STEPS_PER_CPU_CYCLE;
while (unitCycles >= 1) {
const interrupt = this.apu.step(this.onSample);
unitCycles--;
if (interrupt != null) {
const newCPUCycles = this.cpu.interrupt(interrupt);
unitCycles += newCPUCycles * _constants.default.APU_STEPS_PER_CPU_CYCLE;
this.pendingPPUCycles += newCPUCycles * _constants.default.PPU_STEPS_PER_CPU_CYCLE;
}
}
this.pendingAPUCycles = unitCycles;
}
_setSaveFile(prgRamBytes) {
const {
prgRam
} = this.context.mapper;
if (!prgRam) return;
for (let i = 0; i < prgRamBytes.length; i++) prgRam.writeAt(i, prgRamBytes[i]);
}
}
exports.default = NES;