419 lines
9.5 KiB
JavaScript
419 lines
9.5 KiB
JavaScript
function GameBoyAdvance() {
|
|
this.LOG_ERROR = 1;
|
|
this.LOG_WARN = 2;
|
|
this.LOG_STUB = 4;
|
|
this.LOG_INFO = 8;
|
|
this.LOG_DEBUG = 16;
|
|
|
|
this.SYS_ID = 'com.endrift.gbajs';
|
|
|
|
this.logLevel = this.LOG_ERROR | this.LOG_WARN;
|
|
|
|
this.rom = null;
|
|
|
|
this.cpu = new ARMCore();
|
|
this.mmu = new GameBoyAdvanceMMU()
|
|
this.irq = new GameBoyAdvanceInterruptHandler();
|
|
this.io = new GameBoyAdvanceIO();
|
|
this.audio = new GameBoyAdvanceAudio();
|
|
this.video = new GameBoyAdvanceVideo();
|
|
this.keypad = new GameBoyAdvanceKeypad();
|
|
this.sio = new GameBoyAdvanceSIO();
|
|
|
|
// TODO: simplify this graph
|
|
this.cpu.mmu = this.mmu;
|
|
this.cpu.irq = this.irq;
|
|
|
|
this.mmu.cpu = this.cpu;
|
|
this.mmu.core = this;
|
|
|
|
this.irq.cpu = this.cpu;
|
|
this.irq.io = this.io;
|
|
this.irq.audio = this.audio;
|
|
this.irq.video = this.video;
|
|
this.irq.core = this;
|
|
|
|
this.io.cpu = this.cpu;
|
|
this.io.audio = this.audio;
|
|
this.io.video = this.video;
|
|
this.io.keypad = this.keypad;
|
|
this.io.sio = this.sio;
|
|
this.io.core = this;
|
|
|
|
this.audio.cpu = this.cpu;
|
|
this.audio.core = this;
|
|
|
|
this.video.cpu = this.cpu;
|
|
this.video.core = this;
|
|
|
|
this.keypad.core = this;
|
|
|
|
this.sio.core = this;
|
|
|
|
this.keypad.registerHandlers();
|
|
this.doStep = this.waitFrame;
|
|
this.paused = false;
|
|
|
|
this.seenFrame = false;
|
|
this.seenSave = false;
|
|
this.lastVblank = 0;
|
|
|
|
this.queue = null;
|
|
this.reportFPS = null;
|
|
this.throttle = 16; // This is rough, but the 2/3ms difference gives us a good overhead
|
|
|
|
var self = this;
|
|
window.queueFrame = function (f) {
|
|
self.queue = window.setTimeout(f, self.throttle);
|
|
};
|
|
|
|
window.URL = window.URL || window.webkitURL;
|
|
|
|
this.video.vblankCallback = function() {
|
|
self.seenFrame = true;
|
|
};
|
|
};
|
|
|
|
GameBoyAdvance.prototype.setCanvas = function(canvas) {
|
|
var self = this;
|
|
if (canvas.offsetWidth != 240 || canvas.offsetHeight != 160) {
|
|
this.indirectCanvas = document.createElement("canvas");
|
|
this.indirectCanvas.setAttribute("height", "160");
|
|
this.indirectCanvas.setAttribute("width", "240");
|
|
this.targetCanvas = canvas;
|
|
this.setCanvasDirect(this.indirectCanvas);
|
|
var targetContext = canvas.getContext('2d');
|
|
this.video.drawCallback = function() {
|
|
targetContext.drawImage(self.indirectCanvas, 0, 0, canvas.offsetWidth, canvas.offsetHeight);
|
|
}
|
|
} else {
|
|
this.setCanvasDirect(canvas);
|
|
var self = this;
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.setCanvasDirect = function(canvas) {
|
|
this.context = canvas.getContext('2d');
|
|
this.video.setBacking(this.context);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.setBios = function(bios, real) {
|
|
this.mmu.loadBios(bios, real);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.setRom = function(rom) {
|
|
this.reset();
|
|
|
|
this.rom = this.mmu.loadRom(rom, true);
|
|
if (!this.rom) {
|
|
return false;
|
|
}
|
|
this.retrieveSavedata();
|
|
return true;
|
|
};
|
|
|
|
GameBoyAdvance.prototype.hasRom = function() {
|
|
return !!this.rom;
|
|
};
|
|
|
|
GameBoyAdvance.prototype.loadRomFromFile = function(romFile, callback) {
|
|
var reader = new FileReader();
|
|
var self = this;
|
|
reader.onload = function(e) {
|
|
var result = self.setRom(e.target.result);
|
|
if (callback) {
|
|
callback(result);
|
|
}
|
|
}
|
|
reader.readAsArrayBuffer(romFile);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.reset = function() {
|
|
this.audio.pause(true);
|
|
|
|
this.mmu.clear();
|
|
this.io.clear();
|
|
this.audio.clear();
|
|
this.video.clear();
|
|
this.sio.clear();
|
|
|
|
this.mmu.mmap(this.mmu.REGION_IO, this.io);
|
|
this.mmu.mmap(this.mmu.REGION_PALETTE_RAM, this.video.renderPath.palette);
|
|
this.mmu.mmap(this.mmu.REGION_VRAM, this.video.renderPath.vram);
|
|
this.mmu.mmap(this.mmu.REGION_OAM, this.video.renderPath.oam);
|
|
|
|
this.cpu.resetCPU(0);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.step = function() {
|
|
while (this.doStep()) {
|
|
this.cpu.step();
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.waitFrame = function() {
|
|
var seen = this.seenFrame;
|
|
this.seenFrame = false;
|
|
return !seen;
|
|
};
|
|
|
|
GameBoyAdvance.prototype.pause = function() {
|
|
this.paused = true;
|
|
this.audio.pause(true);
|
|
if (this.queue) {
|
|
clearTimeout(this.queue);
|
|
this.queue = null;
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.advanceFrame = function() {
|
|
this.step();
|
|
if (this.seenSave) {
|
|
if (!this.mmu.saveNeedsFlush()) {
|
|
this.storeSavedata();
|
|
this.seenSave = false;
|
|
} else {
|
|
this.mmu.flushSave();
|
|
}
|
|
} else if (this.mmu.saveNeedsFlush()) {
|
|
this.seenSave = true;
|
|
this.mmu.flushSave();
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.runStable = function() {
|
|
if (this.interval) {
|
|
return; // Already running
|
|
}
|
|
var self = this;
|
|
var timer = 0;
|
|
var frames = 0;
|
|
var runFunc;
|
|
var start = Date.now();
|
|
this.paused = false;
|
|
this.audio.pause(false);
|
|
|
|
if (this.reportFPS) {
|
|
runFunc = function() {
|
|
try {
|
|
timer += Date.now() - start;
|
|
if (self.paused) {
|
|
return;
|
|
} else {
|
|
queueFrame(runFunc);
|
|
}
|
|
start = Date.now();
|
|
self.advanceFrame();
|
|
++frames;
|
|
if (frames == 60) {
|
|
self.reportFPS((frames * 1000) / timer);
|
|
frames = 0;
|
|
timer = 0;
|
|
}
|
|
} catch(exception) {
|
|
self.ERROR(exception);
|
|
if (exception.stack) {
|
|
self.logStackTrace(exception.stack.split('\n'));
|
|
}
|
|
throw exception;
|
|
}
|
|
};
|
|
} else {
|
|
runFunc = function() {
|
|
try {
|
|
if (self.paused) {
|
|
return;
|
|
} else {
|
|
queueFrame(runFunc);
|
|
}
|
|
self.advanceFrame();
|
|
} catch(exception) {
|
|
self.ERROR(exception);
|
|
if (exception.stack) {
|
|
self.logStackTrace(exception.stack.split('\n'));
|
|
}
|
|
throw exception;
|
|
}
|
|
};
|
|
}
|
|
queueFrame(runFunc);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.setSavedata = function(data) {
|
|
this.mmu.loadSavedata(data);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.loadSavedataFromFile = function(saveFile) {
|
|
var reader = new FileReader();
|
|
var self = this;
|
|
reader.onload = function(e) { self.setSavedata(e.target.result); }
|
|
reader.readAsArrayBuffer(saveFile);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.decodeSavedata = function(string) {
|
|
this.setSavedata(this.decodeBase64(string));
|
|
};
|
|
|
|
GameBoyAdvance.prototype.decodeBase64 = function(string) {
|
|
var length = (string.length * 3 / 4);
|
|
if (string[string.length - 2] == '=') {
|
|
length -= 2;
|
|
} else if (string[string.length - 1] == '=') {
|
|
length -= 1;
|
|
}
|
|
var buffer = new ArrayBuffer(length);
|
|
var view = new Uint8Array(buffer);
|
|
var bits = string.match(/..../g);
|
|
for (var i = 0; i + 2 < length; i += 3) {
|
|
var s = atob(bits.shift());
|
|
view[i] = s.charCodeAt(0);
|
|
view[i + 1] = s.charCodeAt(1);
|
|
view[i + 2] = s.charCodeAt(2);
|
|
}
|
|
if (i < length) {
|
|
var s = atob(bits.shift());
|
|
view[i++] = s.charCodeAt(0);
|
|
if (s.length > 1) {
|
|
view[i++] = s.charCodeAt(1);
|
|
}
|
|
}
|
|
|
|
return buffer;
|
|
};
|
|
|
|
GameBoyAdvance.prototype.encodeBase64 = function(view) {
|
|
var data = [];
|
|
var b;
|
|
var wordstring = [];
|
|
var triplet;
|
|
for (var i = 0; i < view.byteLength; ++i) {
|
|
b = view.getUint8(i, true);
|
|
wordstring.push(String.fromCharCode(b));
|
|
while (wordstring.length >= 3) {
|
|
triplet = wordstring.splice(0, 3);
|
|
data.push(btoa(triplet.join('')));
|
|
}
|
|
};
|
|
if (wordstring.length) {
|
|
data.push(btoa(wordstring.join('')));
|
|
}
|
|
return data.join('');
|
|
};
|
|
|
|
GameBoyAdvance.prototype.downloadSavedata = function() {
|
|
var sram = this.mmu.save;
|
|
if (!sram) {
|
|
this.WARN("No save data available");
|
|
return null;
|
|
}
|
|
if (window.URL) {
|
|
var url = window.URL.createObjectURL(new Blob([sram.buffer], { type: 'application/octet-stream' }));
|
|
window.open(url);
|
|
} else {
|
|
var data = this.encodeBase64(sram.view);
|
|
window.open('data:application/octet-stream;base64,' + data, this.rom.code + '.sav');
|
|
}
|
|
};
|
|
|
|
|
|
GameBoyAdvance.prototype.storeSavedata = function() {
|
|
var sram = this.mmu.save;
|
|
try {
|
|
var storage = window.localStorage;
|
|
storage[this.SYS_ID + '.' + this.mmu.cart.code] = this.encodeBase64(sram.view);
|
|
} catch (e) {
|
|
this.WARN('Could not store savedata! ' + e);
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.retrieveSavedata = function() {
|
|
try {
|
|
var storage = window.localStorage;
|
|
var data = storage[this.SYS_ID + '.' + this.mmu.cart.code];
|
|
if (data) {
|
|
this.decodeSavedata(data);
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
this.WARN('Could not retrieve savedata! ' + e);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
GameBoyAdvance.prototype.freeze = function() {
|
|
return {
|
|
'cpu': this.cpu.freeze(),
|
|
'mmu': this.mmu.freeze(),
|
|
'irq': this.irq.freeze(),
|
|
'io': this.io.freeze(),
|
|
'audio': this.audio.freeze(),
|
|
'video': this.video.freeze()
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.defrost = function(frost) {
|
|
this.cpu.defrost(frost.cpu);
|
|
this.mmu.defrost(frost.mmu);
|
|
this.audio.defrost(frost.audio);
|
|
this.video.defrost(frost.video);
|
|
this.irq.defrost(frost.irq);
|
|
this.io.defrost(frost.io);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.log = function(level, message) {};
|
|
|
|
GameBoyAdvance.prototype.setLogger = function(logger) {
|
|
this.log = logger;
|
|
};
|
|
|
|
GameBoyAdvance.prototype.logStackTrace = function(stack) {
|
|
var overflow = stack.length - 32;
|
|
this.ERROR('Stack trace follows:');
|
|
if (overflow > 0) {
|
|
this.log(-1, '> (Too many frames)');
|
|
}
|
|
for (var i = Math.max(overflow, 0); i < stack.length; ++i) {
|
|
this.log(-1, '> ' + stack[i]);
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.ERROR = function(error) {
|
|
if (this.logLevel & this.LOG_ERROR) {
|
|
this.log(this.LOG_ERROR, error);
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.WARN = function(warn) {
|
|
if (this.logLevel & this.LOG_WARN) {
|
|
this.log(this.LOG_WARN, warn);
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.STUB = function(func) {
|
|
if (this.logLevel & this.LOG_STUB) {
|
|
this.log(this.LOG_STUB, func);
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.INFO = function(info) {
|
|
if (this.logLevel & this.LOG_INFO) {
|
|
this.log(this.LOG_INFO, info);
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.DEBUG = function(info) {
|
|
if (this.logLevel & this.LOG_DEBUG) {
|
|
this.log(this.LOG_DEBUG, info);
|
|
}
|
|
};
|
|
|
|
GameBoyAdvance.prototype.ASSERT_UNREACHED = function(err) {
|
|
throw new Error("Should be unreached: " + err);
|
|
};
|
|
|
|
GameBoyAdvance.prototype.ASSERT = function(test, err) {
|
|
if (!test) {
|
|
throw new Error("Assertion failed: " + err);
|
|
}
|
|
};
|