8bitworkshop/src/common/mameplatform.ts

385 lines
10 KiB
TypeScript

/// MAME SUPPORT
import { EmuState, DisasmLine, cpuStateToLongString_6502, cpuStateToLongString_Z80 } from "./baseplatform";
import { disassemble6502 } from "./cpu/disasm6502";
import { disassembleZ80 } from "./cpu/disasmz80";
import { AnimationTimer, RAM, RasterVideo } from "./emu";
declare var FS, ENV, Module; // mame emscripten
export abstract class BaseMAMEPlatform {
loaded : boolean = false;
preinitted : boolean = false;
started : boolean = false;
romfn : string;
romdata : Uint8Array;
romtype : string = 'cart';
video;
running = false;
initluavars : boolean = false;
luadebugscript : string;
js_lua_string;
onBreakpointHit;
mainElement : HTMLElement;
timer : AnimationTimer;
constructor(mainElement) {
this.mainElement = mainElement;
this.timer = new AnimationTimer(20, this.poll.bind(this));
}
// http://docs.mamedev.org/techspecs/luaengine.html
luacall(s:string) : string {
if (!this.js_lua_string) this.js_lua_string = Module.cwrap('_Z13js_lua_stringPKc', 'string', ['string']);
return this.js_lua_string(s || "");
}
_pause() {
this.running = false;
this.timer.stop();
}
pause() {
if (this.loaded && this.running) {
this.luacall('emu.pause()');
this._pause();
}
}
_resume() {
this.luacall('emu.unpause()');
this.running = true;
this.timer.start();
}
resume() {
if (this.loaded && !this.running) { // TODO
this._resume();
}
}
reset() {
if (this.loaded) {
this.luacall('manager:machine():soft_reset()');
this.running = true;
this.initluavars = false;
}
}
isRunning() {
return this.running;
}
bufferConsoleOutput(s) {
if (typeof s !== 'string') return;
console.log(s);
}
startModule(mainElement, opts) {
this.started = true;
var romfn = this.romfn = this.romfn || opts.romfn;
var romdata = this.romdata = this.romdata || opts.romdata || new RAM(opts.romsize).mem;
var romtype = this.romtype = this.romtype || opts.romtype;
// create canvas
var video = this.video = new RasterVideo(this.mainElement, opts.width, opts.height);
video.create();
$(video.canvas).attr('id','canvas');
// load asm.js module
console.log("loading", opts.jsfile);
var modargs = [opts.driver,
'-debug',
'-debugger', 'none',
'-verbose', '-window', '-nokeepaspect',
'-resolution', video.canvas.width+'x'+video.canvas.height
];
if (romfn) {
modargs.push('-'+romtype, romfn);
}
if (opts.extraargs) {
modargs = modargs.concat(opts.extraargs);
}
console.log(modargs);
window['JSMESS'] = {};
window['Module'] = {
arguments: modargs,
screenIsReadOnly: true,
print: this.bufferConsoleOutput,
canvas:video.canvas,
doNotCaptureKeyboard:true,
keyboardListeningElement:video.canvas,
preInit: () => {
console.log("loading FS");
ENV.SDL_EMSCRIPTEN_KEYBOARD_ELEMENT = 'canvas';
if (opts.cfgfile) {
FS.mkdir('/cfg');
FS.writeFile('/cfg/' + opts.cfgfile, opts.cfgdata, {encoding:'utf8'});
}
if (opts.biosfile) {
FS.mkdir('/roms');
FS.mkdir('/roms/' + opts.driver);
FS.writeFile('/roms/' + opts.biosfile, opts.biosdata, {encoding:'binary'});
}
FS.mkdir('/emulator');
if (romfn) {
FS.writeFile(romfn, romdata, {encoding:'binary'});
}
//FS.writeFile('/debug.ini', 'debugger none\n', {encoding:'utf8'});
if (opts.preInit) {
opts.preInit(self);
}
this.preinitted = true;
},
preRun: [
() => {
$(video.canvas).click((e) =>{
video.canvas.focus();
});
this.loaded = true;
console.log("about to run...");
}
]
};
// preload files
// TODO: ensure loaded
var fetch_cfg, fetch_lua;
var fetch_bios = $.Deferred();
var fetch_wasm = $.Deferred();
// fetch config file
if (opts.cfgfile) {
fetch_cfg = $.get('mame/cfg/' + opts.cfgfile, (data) => {
opts.cfgdata = data;
console.log("loaded " + opts.cfgfile);
}, 'text');
}
// fetch BIOS file
if (opts.biosfile) {
var oReq1 = new XMLHttpRequest();
oReq1.open("GET", 'mame/roms/' + opts.biosfile, true);
oReq1.responseType = "arraybuffer";
oReq1.onload = (oEvent) => {
opts.biosdata = new Uint8Array(oReq1.response);
console.log("loaded " + opts.biosfile); // + " (" + oEvent.total + " bytes)");
fetch_bios.resolve();
};
oReq1.ontimeout = function (oEvent) {
throw Error("Timeout loading " + opts.biosfile);
}
oReq1.send();
} else {
fetch_bios.resolve();
}
// load debugger Lua script
fetch_lua = $.get('mame/debugger.lua', (data) => {
this.luadebugscript = data;
console.log("loaded debugger.lua");
}, 'text');
// load WASM
{
var oReq2 = new XMLHttpRequest();
oReq2.open("GET", 'mame/' + opts.jsfile.replace('.js','.wasm'), true);
oReq2.responseType = "arraybuffer";
oReq2.onload = (oEvent) => {
console.log("loaded WASM file");
window['Module'].wasmBinary = new Uint8Array(oReq2.response);
fetch_wasm.resolve();
};
oReq2.ontimeout = function (oEvent) {
throw Error("Timeout loading " + opts.jsfile);
}
oReq2.send();
}
// start loading script
$.when(fetch_lua, fetch_cfg, fetch_bios, fetch_wasm).done( () => {
var script = document.createElement('script');
script.src = 'mame/' + opts.jsfile;
document.getElementsByTagName('head')[0].appendChild(script);
console.log("created script element");
});
// for debugging via browser console
window['mamelua'] = (s:string) => {
this.initlua();
return [s, this.luacall(s)];
};
}
loadROMFile(data) {
this.romdata = data;
if (this.preinitted && this.romfn) {
FS.writeFile(this.romfn, data, {encoding:'binary'});
}
}
loadRegion(region, data) {
if (this.loaded && data.length > 0) {
//this.luacall('cart=manager:machine().images["cart"]\nprint(cart:filename())\ncart:load("' + region + '")\n');
var s = 'rgn = manager:machine():memory().regions["' + region + '"]\n';
//s += 'print(rgn.size)\n';
for (var i=0; i<data.length; i+=4) {
var v = data[i] + (data[i+1]<<8) + (data[i+2]<<16) + (data[i+3]<<24);
s += 'rgn:write_u32(' + i + ',' + v + ')\n'; // TODO: endian?
}
this.luacall(s);
this.reset();
}
}
// DEBUGGING SUPPORT
initlua() {
if (!this.initluavars) {
this.luacall(this.luadebugscript);
this.luacall('mamedbg.init()')
this.initluavars = true;
}
}
readAddress(a:number) : number {
this.initlua();
return parseInt(this.luacall('return mem:read_u8(' + a + ')'));
}
getCPUReg(reg:string) {
if (!this.loaded) return 0; // TODO
this.initlua();
return parseInt(this.luacall('return cpu.state.'+reg+'.value'));
}
grabState(expr:string) {
this.initlua();
return {
c:this.getCPUState(),
buf:this.luacall("return string.tohex(" + expr + ")")
}
}
saveState() {
return this.grabState("manager:machine():buffer_save()");
}
loadState(state) {
this.initlua();
return this.luacall("manager:machine():buffer_load(string.fromhex('" + state.buf + "'))");
}
poll() {
if (this.onBreakpointHit && this.luacall("return tostring(mamedbg.is_stopped())") == 'true') {
this._pause();
//this.luacall("manager:machine():buffer_load(lastBreakState)");
var state = this.grabState("lastBreakState");
this.onBreakpointHit(state);
}
}
clearDebug() {
this.onBreakpointHit = null;
if (this.loaded) {
this.initlua();
this.luacall('mamedbg.reset()');
}
}
getDebugCallback() {
return this.onBreakpointHit;// TODO?
}
setupDebug(callback) {
this.onBreakpointHit = callback;
}
debugcmd(s) {
this.initlua()
this.luacall(s);
this._resume();
}
runToPC(pc) {
this.debugcmd('mamedbg.runTo(' + pc + ')');
}
runToVsync() {
this.debugcmd('mamedbg.runToVsync()');
}
runUntilReturn() {
this.debugcmd('mamedbg.runUntilReturn()');
}
// TODO
runEval() {
this.reset();
this.step();
}
step() {
this.debugcmd('mamedbg.step()');
}
getDebugCategories() {
return ['CPU'];
}
getDebugInfo(category:string, state:EmuState) : string {
switch (category) {
case 'CPU': return this.cpuStateToLongString(state.c);
}
}
getDebugTree() {
this.initlua();
var devices = JSON.parse(this.luacall(`return table.tojson(manager:machine().devices)`));
var images = JSON.parse(this.luacall(`return table.tojson(manager:machine().images)`));
var regions = JSON.parse(this.luacall(`return table.tojson(manager:machine():memory().regions)`));
return {
devices: devices,
images: images,
regions: regions,
}
}
abstract cpuStateToLongString(c) : string;
abstract getCPUState() : any;
}
export abstract class BaseMAME6502Platform extends BaseMAMEPlatform {
getPC() : number {
return this.getCPUReg('PC');
}
getSP() : number {
return this.getCPUReg('SP');
}
isStable() { return true; }
getCPUState() {
return {
PC:this.getPC(),
SP:this.getSP(),
A:this.getCPUReg('A'),
X:this.getCPUReg('X'),
Y:this.getCPUReg('Y'),
flags:this.getCPUReg('P'),
};
}
disassemble(pc:number, read:(addr:number)=>number) : DisasmLine {
return disassemble6502(pc, read(pc), read(pc+1), read(pc+2));
}
cpuStateToLongString(c) {
return cpuStateToLongString_6502(c);
}
}
export abstract class BaseMAMEZ80Platform extends BaseMAMEPlatform {
getPC() : number {
return this.getCPUReg('PC');
}
getSP() : number {
return this.getCPUReg('SP');
}
isStable() { return true; }
getCPUState() {
return {
PC:this.getPC(),
SP:this.getSP(),
AF:this.getCPUReg('AF'),
BC:this.getCPUReg('BC'),
DE:this.getCPUReg('DE'),
HL:this.getCPUReg('HL'),
IX:this.getCPUReg('IX'),
IY:this.getCPUReg('IY'),
IR:this.getCPUReg('R') + (this.getCPUReg('I') << 8),
};
}
disassemble(pc:number, read:(addr:number)=>number) : DisasmLine {
return disassembleZ80(pc, read(pc), read(pc+1), read(pc+2), read(pc+3));
}
cpuStateToLongString(c) {
return cpuStateToLongString_Z80(c);
}
}