2019-06-11 20:05:34 +08:00

211 lines
4.8 KiB
JavaScript

var CPU = require("./cpu");
var Controller = require("./controller");
var PPU = require("./ppu");
var PAPU = require("./papu");
var ROM = require("./rom");
var NES = function(opts) {
this.opts = {
onFrame: function() {},
onAudioSample: null,
onStatusUpdate: function() {},
onBatteryRamWrite: function() {},
// FIXME: not actually used except for in PAPU
preferredFrameRate: 60,
emulateSound: true,
sampleRate: 44100 // Sound sample rate in hz
};
if (typeof opts !== "undefined") {
var key;
for (key in this.opts) {
if (typeof opts[key] !== "undefined") {
this.opts[key] = opts[key];
}
}
}
this.frameTime = 1000 / this.opts.preferredFrameRate;
this.ui = {
writeFrame: this.opts.onFrame,
updateStatus: this.opts.onStatusUpdate
};
this.cpu = new CPU(this);
this.ppu = new PPU(this);
this.papu = new PAPU(this);
this.mmap = null; // set in loadROM()
this.controllers = {
1: new Controller(),
2: new Controller()
};
this.ui.updateStatus("Ready to load a ROM.");
this.frame = this.frame.bind(this);
this.buttonDown = this.buttonDown.bind(this);
this.buttonUp = this.buttonUp.bind(this);
this.zapperMove = this.zapperMove.bind(this);
this.zapperFireDown = this.zapperFireDown.bind(this);
this.zapperFireUp = this.zapperFireUp.bind(this);
};
NES.prototype = {
fpsFrameCount: 0,
romData: null,
// Resets the system
reset: function() {
if (this.mmap !== null) {
this.mmap.reset();
}
this.cpu.reset();
this.ppu.reset();
this.papu.reset();
this.lastFpsTime = null;
this.fpsFrameCount = 0;
},
frame: function() {
this.ppu.startFrame();
var cycles = 0;
var emulateSound = this.opts.emulateSound;
var cpu = this.cpu;
var ppu = this.ppu;
var papu = this.papu;
FRAMELOOP: for (;;) {
if (cpu.cyclesToHalt === 0) {
// Execute a CPU instruction
cycles = cpu.emulate();
if (emulateSound) {
papu.clockFrameCounter(cycles);
}
cycles *= 3;
} else {
if (cpu.cyclesToHalt > 8) {
cycles = 24;
if (emulateSound) {
papu.clockFrameCounter(8);
}
cpu.cyclesToHalt -= 8;
} else {
cycles = cpu.cyclesToHalt * 3;
if (emulateSound) {
papu.clockFrameCounter(cpu.cyclesToHalt);
}
cpu.cyclesToHalt = 0;
}
}
for (; cycles > 0; cycles--) {
if (
ppu.curX === ppu.spr0HitX &&
ppu.f_spVisibility === 1 &&
ppu.scanline - 21 === ppu.spr0HitY
) {
// Set sprite 0 hit flag:
ppu.setStatusFlag(ppu.STATUS_SPRITE0HIT, true);
}
if (ppu.requestEndFrame) {
ppu.nmiCounter--;
if (ppu.nmiCounter === 0) {
ppu.requestEndFrame = false;
ppu.startVBlank();
break FRAMELOOP;
}
}
ppu.curX++;
if (ppu.curX === 341) {
ppu.curX = 0;
ppu.endScanline();
}
}
}
this.fpsFrameCount++;
},
buttonDown: function(controller, button) {
this.controllers[controller].buttonDown(button);
},
buttonUp: function(controller, button) {
this.controllers[controller].buttonUp(button);
},
zapperMove: function(x, y) {
if (!this.mmap) return;
this.mmap.zapperX = x;
this.mmap.zapperY = y;
},
zapperFireDown: function() {
if (!this.mmap) return;
this.mmap.zapperFired = true;
},
zapperFireUp: function() {
if (!this.mmap) return;
this.mmap.zapperFired = false;
},
getFPS: function() {
var now = +new Date();
var fps = null;
if (this.lastFpsTime) {
fps = this.fpsFrameCount / ((now - this.lastFpsTime) / 1000);
}
this.fpsFrameCount = 0;
this.lastFpsTime = now;
return fps;
},
reloadROM: function() {
if (this.romData !== null) {
this.loadROM(this.romData);
}
},
// Loads a ROM file into the CPU and PPU.
// The ROM file is validated first.
loadROM: function(data) {
// Load ROM file:
this.rom = new ROM(this);
this.rom.load(data);
this.reset();
this.mmap = this.rom.createMapper();
this.mmap.loadROM();
this.ppu.setMirroring(this.rom.getMirroringType());
this.romData = data;
},
setFramerate: function(rate) {
this.opts.preferredFrameRate = rate;
this.frameTime = 1000 / rate;
this.papu.setSampleRate(this.opts.sampleRate, false);
},
toJSON: function() {
return {
romData: this.romData,
cpu: this.cpu.toJSON(),
mmap: this.mmap.toJSON(),
ppu: this.ppu.toJSON()
};
},
fromJSON: function(s) {
this.loadROM(s.romData);
this.cpu.fromJSON(s.cpu);
this.mmap.fromJSON(s.mmap);
this.ppu.fromJSON(s.ppu);
}
};
module.exports = NES;