211 lines
4.8 KiB
JavaScript
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;
|