8bitworkshop/gen/platform/nes.js

551 lines
20 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const baseplatform_1 = require("../common/baseplatform");
const emu_1 = require("../common/emu");
const util_1 = require("../common/util");
const analysis_1 = require("../common/analysis");
const audio_1 = require("../common/audio");
const probe_1 = require("../common/probe");
const devices_1 = require("../common/devices");
const Mousetrap = require("mousetrap");
const jsnes = require("../../jsnes");
const mameplatform_1 = require("../common/mameplatform");
const JSNES_PRESETS = [
{ id: 'hello.c', name: 'Hello World' },
{ id: 'attributes.c', name: 'Attribute Table' },
{ id: 'scroll.c', name: 'Scrolling' },
{ id: 'sprites.c', name: 'Sprites' },
{ id: 'metasprites.c', name: 'Metasprites' },
{ id: 'flicker.c', name: 'Flickering Sprites' },
{ id: 'metacursor.c', name: 'Controllers' },
{ id: 'vrambuffer.c', name: 'VRAM Buffer' },
{ id: 'statusbar.c', name: 'Split Status Bar' },
{ id: 'siegegame.c', name: 'Siege Game' },
{ id: 'tint.c', name: 'Color Emphasis' },
{ id: 'rletitle.c', name: 'Title Screen RLE' },
{ id: 'aputest.c', name: 'Sound Tester' },
{ id: 'music.c', name: 'Music Player' },
{ id: 'horizscroll.c', name: 'Offscreen Scrolling' },
{ id: 'monobitmap.c', name: 'Monochrome Bitmap' },
{ id: 'fami.c', name: 'Famitone Demo' },
{ id: 'shoot2.c', name: 'Solarian Game' },
{ id: 'climber.c', name: 'Climber Game' },
{ id: 'bankswitch.c', name: 'Bank Switching' },
{ id: 'irq.c', name: 'IRQ Scanline Counter' },
{ id: 'ex0.dasm', name: 'Initialization (ASM)' },
{ id: 'ex1.dasm', name: 'Hello World (ASM)' },
{ id: 'ex2.dasm', name: 'Scrolling Demo (ASM)' },
{ id: 'ex3.dasm', name: 'Sprite Demo (ASM)' },
{ id: 'ex4.dasm', name: 'Controller Demo (ASM)' },
{ id: 'musicdemo.dasm', name: 'Famitone Demo (ASM)' },
{ id: 'xyscroll.dasm', name: 'XY Split Scrolling (ASM)' },
// {id:'scrollrt.dasm', name:'Line-by-line Scrolling (ASM)'},
{ id: 'road.dasm', name: '3-D Road Demo (ASM)' },
{ id: 'chase/game.c', name: 'Shiru\'s Chase Game' },
{ id: 'hello.wiz', name: 'Hello (Wiz)' },
];
/// JSNES
const JSNES_KEYCODE_MAP = (0, emu_1.makeKeycodeMap)([
[emu_1.Keys.A, 0, 0],
[emu_1.Keys.B, 0, 1],
[emu_1.Keys.GP_A, 0, 0],
[emu_1.Keys.GP_B, 0, 1],
[emu_1.Keys.SELECT, 0, 2],
[emu_1.Keys.START, 0, 3],
[emu_1.Keys.UP, 0, 4],
[emu_1.Keys.DOWN, 0, 5],
[emu_1.Keys.LEFT, 0, 6],
[emu_1.Keys.RIGHT, 0, 7],
[emu_1.Keys.P2_A, 1, 0],
[emu_1.Keys.P2_B, 1, 1],
[emu_1.Keys.P2_SELECT, 1, 2],
[emu_1.Keys.P2_START, 1, 3],
[emu_1.Keys.P2_UP, 1, 4],
[emu_1.Keys.P2_DOWN, 1, 5],
[emu_1.Keys.P2_LEFT, 1, 6],
[emu_1.Keys.P2_RIGHT, 1, 7],
]);
class JSNESPlatform extends baseplatform_1.Base6502Platform {
constructor(mainElement) {
super();
this.audioFrequency = 44030; //44100
this.frameindex = 0;
this.machine = { cpuCyclesPerLine: 114 }; // TODO: hack for width of probe scope
this.getToolForFilename = (fn) => {
//if (fn.endsWith(".asm")) return "ca65"; // .asm uses ca65
if (fn.endsWith(".nesasm"))
return "nesasm";
else
return (0, baseplatform_1.getToolForFilename_6502)(fn);
};
// probing
this.nullProbe = new devices_1.NullProbe();
this.probe = this.nullProbe;
this.getMemoryMap = function () {
return { main: [
{ name: 'Zero Page RAM', start: 0x0, size: 0x100, type: 'ram' },
{ name: 'OAM Buffer', start: 0x200, size: 0x100, type: 'ram' },
{ name: 'Work RAM', start: 0x300, size: 0x1000 - 0x300, type: 'ram' },
{ name: 'PPU Registers', start: 0x2000, last: 0x2008, size: 0x2000, type: 'io' },
{ name: 'APU Registers', start: 0x4000, last: 0x4020, size: 0x2000, type: 'io' },
{ name: 'Optional Cartridge RAM', start: 0x6000, size: 0x2000, type: 'ram' },
] };
};
this.mainElement = mainElement;
}
getPresets() { return JSNES_PRESETS; }
start() {
this.debugPCDelta = 1;
var debugbar = $("<div>").appendTo(this.mainElement);
this.audio = new audio_1.SampleAudio(this.audioFrequency);
this.video = new emu_1.RasterVideo(this.mainElement, 256, 224, { overscan: true });
this.video.create();
// debugging view
this.ntvideo = new emu_1.RasterVideo(this.mainElement, 512, 480, { overscan: false });
this.ntvideo.create();
$(this.ntvideo.canvas).hide();
this.ntlastbuf = new Uint32Array(0x1000);
if (Mousetrap.bind)
Mousetrap.bind('ctrl+shift+alt+n', () => {
$(this.video.canvas).toggle();
$(this.ntvideo.canvas).toggle();
});
// toggle buttons (TODO)
/*
$('<button>').text("Video").appendTo(debugbar).click(() => { $(this.video.canvas).toggle() });
$('<button>').text("Nametable").appendTo(debugbar).click(() => { $(this.ntvideo.canvas).toggle() });
*/
var idata = this.video.getFrameData();
this.nes = new jsnes.NES({
onFrame: (frameBuffer) => {
for (var i = 0; i < frameBuffer.length; i++)
idata[i] = frameBuffer[i] | 0xff000000;
this.video.updateFrame();
this.frameindex++;
this.updateDebugViews();
},
onAudioSample: (left, right) => {
if (this.frameindex < 10)
this.audio.feedSample(0, 1); // avoid popping at powerup
else
this.audio.feedSample((left + right) * 0.5, 1);
},
onStatusUpdate: function (s) {
console.log(s);
},
//TODO: onBatteryRamWrite
});
//this.nes.ppu.showSpr0Hit = true;
//this.nes.ppu.clipToTvSize = false;
this.nes.stop = () => {
this.haltAndCatchFire("Illegal instruction");
throw new emu_1.EmuHalt("CPU STOPPED"); //TODO: haltEmulation()
};
// insert debug hook
this.nes.cpu._emulate = this.nes.cpu.emulate;
this.nes.cpu.emulate = () => {
if (this.nes.cpu.irqRequested)
this.probe.logInterrupt(this.nes.cpu.irqType || 0);
this.probe.logExecute(this.nes.cpu.REG_PC + 1, this.nes.cpu.REG_SP);
var cycles = this.nes.cpu._emulate();
this.evalDebugCondition();
this.probe.logClocks(cycles);
return cycles > 0 ? cycles : 1;
};
this.timer = new emu_1.AnimationTimer(60, this.nextFrame.bind(this));
// set keyboard map
this.poller = (0, emu_1.setKeyboardFromMap)(this.video, [], JSNES_KEYCODE_MAP, (o, key, code, flags) => {
if (flags & emu_1.KeyFlags.KeyDown)
this.nes.buttonDown(o.index + 1, o.mask); // controller, button
else if (flags & emu_1.KeyFlags.KeyUp)
this.nes.buttonUp(o.index + 1, o.mask); // controller, button
});
//var s = ''; nes.ppu.palTable.curTable.forEach((rgb) => { s += "0x"+hex(rgb,6)+", "; }); console.log(s);
}
pollControls() { this.poller.poll(); }
advance(novideo) {
this.nes.frame();
return 29780; //TODO
}
updateDebugViews() {
// don't update if view is hidden
if (!$(this.ntvideo.canvas).is(":visible"))
return;
var a = 0;
var attraddr = 0;
var idata = this.ntvideo.getFrameData();
var baseTile = this.nes.ppu.regS === 0 ? 0 : 256;
for (var row = 0; row < 60; row++) {
for (var col = 0; col < 64; col++) {
a = 0x2000 + (col & 31) + ((row % 30) * 32);
if (col >= 32)
a += 0x400;
if (row >= 30)
a += 0x800;
var name = this.nes.ppu.mirroredLoad(a) + baseTile;
var t = this.nes.ppu.ptTile[name];
attraddr = (a & 0x2c00) | 0x3c0 | (a & 0x0C00) | ((a >> 4) & 0x38) | ((a >> 2) & 0x07);
var attr = this.nes.ppu.mirroredLoad(attraddr);
var tag = name ^ (attr << 9) ^ 0x80000000;
if (tag != this.ntlastbuf[a & 0xfff]) {
this.ntlastbuf[a & 0xfff] = tag;
var i = row * 64 * 8 * 8 + col * 8;
var j = 0;
var attrshift = (col & 2) + ((a & 0x40) >> 4);
var coloradd = ((attr >> attrshift) & 3) << 2;
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
var color = t.pix[j++];
if (color)
color += coloradd;
var rgb = this.nes.ppu.imgPalette[color];
idata[i++] = rgb | 0xff000000;
}
i += 64 * 8 - 8;
}
}
}
}
this.ntvideo.updateFrame();
}
loadROM(title, data) {
var romstr = (0, util_1.byteArrayToString)(data);
this.nes.loadROM(romstr);
this.frameindex = 0;
this.installIntercepts();
}
installIntercepts() {
// intercept bus calls, unless we did it already
var mmap = this.nes.mmap;
if (!mmap.haveProxied) {
var oldload = mmap.load.bind(mmap);
var oldwrite = mmap.write.bind(mmap);
var oldregLoad = mmap.regLoad.bind(mmap);
var oldregWrite = mmap.regWrite.bind(mmap);
var lastioaddr = -1;
mmap.load = (addr) => {
var val = oldload(addr);
if (addr != lastioaddr)
this.probe.logRead(addr, val);
return val;
};
mmap.write = (addr, val) => {
if (addr != lastioaddr)
this.probe.logWrite(addr, val);
oldwrite(addr, val);
};
// try not to read/write then IOread/IOwrite at same time
mmap.regLoad = (addr) => {
var val = oldregLoad(addr);
this.probe.logIORead(addr, val);
lastioaddr = addr;
return val;
};
mmap.regWrite = (addr, val) => {
this.probe.logIOWrite(addr, val);
lastioaddr = addr;
oldregWrite(addr, val);
};
mmap.haveProxied = true;
}
var ppu = this.nes.ppu;
if (!ppu.haveProxied) {
var old_endScanline = ppu.endScanline.bind(ppu);
var old_startFrame = ppu.startFrame.bind(ppu);
var old_writeMem = ppu.writeMem.bind(ppu);
ppu.endScanline = () => {
old_endScanline();
this.probe.logNewScanline();
};
ppu.startFrame = () => {
old_startFrame();
this.probe.logNewFrame();
};
ppu.writeMem = (a, v) => {
old_writeMem(a, v);
this.probe.logVRAMWrite(a, v);
};
ppu.haveProxied = true;
}
}
newCodeAnalyzer() {
return new analysis_1.CodeAnalyzer_nes(this);
}
getOriginPC() {
return (this.readAddress(0xfffa) | (this.readAddress(0xfffb) << 8)) & 0xffff;
}
getDefaultExtension() { return ".c"; }
getROMExtension() { return ".nes"; }
reset() {
//this.nes.cpu.reset(); // doesn't work right, crashes
this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
this.installIntercepts();
}
isRunning() {
return this.timer.isRunning();
}
pause() {
this.timer.stop();
this.audio.stop();
}
resume() {
this.timer.start();
this.audio.start();
}
runToVsync() {
var frame0 = this.frameindex;
this.runEval((c) => { return this.frameindex > frame0; });
}
getRasterScanline() {
return this.nes.ppu.scanline;
}
getRasterLineClock() {
return this.nes.ppu.curX;
}
getCPUState() {
var c = this.nes.cpu.toJSON();
this.copy6502REGvars(c);
return c;
}
// TODO don't need to save ROM?
saveState() {
//var s = $.extend(true, {}, this.nes);
var s;
if (this.nes.mmap) {
s = this.nes.toJSON();
}
else {
console.log("no nes.mmap!");
s = { cpu: this.nes.cpu.toJSON(), ppu: this.nes.ppu.toJSON() };
}
s.c = s.cpu;
this.copy6502REGvars(s.c);
s.b = s.cpu.mem = s.cpu.mem.slice(0);
s.ppu.vramMem = s.ppu.vramMem.slice(0);
s.ppu.spriteMem = s.ppu.spriteMem.slice(0);
s.ctrl = this.saveControlsState();
return s;
}
loadState(state) {
this.unfixPC(state.cpu);
this.nes.fromJSON(state);
this.fixPC(state.cpu);
//this.nes.cpu.fromJSON(state.cpu);
//this.nes.mmap.fromJSON(state.mmap);
//this.nes.ppu.fromJSON(state.ppu);
this.nes.cpu.mem = state.cpu.mem.slice(0);
this.nes.ppu.vramMem = state.ppu.vramMem.slice(0);
this.nes.ppu.spriteMem = state.ppu.spriteMem.slice(0);
this.loadControlsState(state.ctrl);
//$.extend(this.nes, state);
this.installIntercepts();
}
saveControlsState() {
return {
c1: this.nes.controllers[1].state.slice(0),
c2: this.nes.controllers[2].state.slice(0),
};
}
loadControlsState(state) {
this.nes.controllers[1].state = state.c1;
this.nes.controllers[2].state = state.c2;
}
readAddress(addr) {
return this.nes.cpu.mem[addr];
}
readVRAMAddress(addr) {
return this.nes.ppu.vramMem[addr];
}
copy6502REGvars(c) {
c.T = 0;
c.PC = c.REG_PC;
this.fixPC(c);
c.A = c.REG_ACC;
c.X = c.REG_X;
c.Y = c.REG_Y;
c.SP = c.REG_SP & 0xff;
c.Z = c.F_ZERO;
c.N = c.F_SIGN;
c.V = c.F_OVERFLOW;
c.D = c.F_DECIMAL;
c.C = c.F_CARRY;
c.I = c.F_INTERRUPT;
c.R = 1;
c.o = this.readAddress(c.PC + 1);
return c;
}
getDebugCategories() {
return super.getDebugCategories().concat(['PPU', 'Mapper']);
}
getDebugInfo(category, state) {
switch (category) {
case 'PPU': return this.ppuStateToLongString(state.ppu, state.b);
case 'Mapper': return this.mapperStateToLongString(state.mmap, state.b);
default: return super.getDebugInfo(category, state);
}
}
ppuStateToLongString(ppu, mem) {
var s = '';
var PPUFLAGS = [
["f_nmiOnVblank", "NMI_ON_VBLANK"],
["f_spVisibility", "SPRITES"],
["f_spClipping", "NO_CLIP_SPRITES"],
["f_dispType", "MONOCHROME"],
["f_bgVisibility", "BACKGROUND"],
["f_bgClipping", "NO_CLIP_BACKGROUND"],
];
for (var i = 0; i < PPUFLAGS.length; i++) {
var flag = PPUFLAGS[i];
s += (ppu[flag[0]] ? flag[1] : "-") + " ";
if (i == 2 || i == 5)
s += "\n";
}
var status = mem[0x2002];
s += "\n Status ";
s += (status & 0x80) ? "VBLANK " : "- ";
s += (status & 0x40) ? "SPRITE0HIT " : "- ";
s += "\n";
if (ppu.f_color)
s += " Tint " + ((ppu.f_color & 1) ? "RED " : "") + ((ppu.f_color & 2) ? "BLUE " : "") + ((ppu.f_color & 4) ? "GREEN " : "") + "\n";
if (ppu.f_spVisibility) {
s += "SprSize " + (ppu.f_spriteSize ? "8x16" : "8x8") + "\n";
s += "SprBase $" + (ppu.f_spPatternTable ? "1000" : "0000") + "\n";
}
if (ppu.f_bgVisibility) {
s += " BgBase $" + (ppu.f_bgPatternTable ? "1000" : "0000") + "\n";
s += " NTBase $" + (0, util_1.hex)(ppu.f_nTblAddress * 0x400 + 0x2000) + "\n";
s += "AddrInc " + (ppu.f_addrInc ? "32" : "1") + "\n";
}
var scrollX = ppu.regFH + ppu.regHT * 8;
var scrollY = ppu.regFV + ppu.regVT * 8;
s += "ScrollX $" + (0, util_1.hex)(scrollX) + " (" + ppu.regHT + " * 8 + " + ppu.regFH + " = " + scrollX + ")\n";
s += "ScrollY $" + (0, util_1.hex)(scrollY) + " (" + ppu.regVT + " * 8 + " + ppu.regFV + " = " + scrollY + ")\n";
s += "\n";
s += " Scan Y: " + ppu.scanline + " X: " + ppu.curX + "\n";
s += "VramCur" + (ppu.firstWrite ? " " : "?") + "$" + (0, util_1.hex)(ppu.vramAddress, 4) + "\n";
s += "VramTmp $" + (0, util_1.hex)(ppu.vramTmpAddress, 4) + "\n";
/*
var PPUREGS = [
'cntFV',
'cntV',
'cntH',
'cntVT',
'cntHT',
'regV',
'regH',
'regS',
];
s += "\n";
for (var i=0; i<PPUREGS.length; i++) {
var reg = PPUREGS[i];
s += lpad(reg.toUpperCase(),7) + " $" + hex(ppu[reg]) + " (" + ppu[reg] + ")\n";
}
*/
return s;
}
mapperStateToLongString(mmap, mem) {
//console.log(mmap, mem);
var s = "";
if (this.nes.rom) {
s += "Mapper " + this.nes.rom.mapperType + "\n";
}
if (mmap.irqCounter !== undefined) {
s += "\nIRQ Counter: " + mmap.irqCounter;
s += "\n IRQ Latch: " + mmap.irqLatchValue;
s += "\n IRQ Reload: " + mmap.irqReload;
s += "\n IRQ Enable: " + mmap.irqEnable;
s += "\n PRG Select: " + mmap.prgAddressSelect;
s += "\n CHR Select: " + mmap.chrAddressSelect;
}
s += "\n";
return s;
}
startProbing() {
var rec = new probe_1.ProbeRecorder(this);
this.connectProbe(rec);
return rec;
}
stopProbing() {
this.connectProbe(null);
}
connectProbe(probe) {
this.probe = probe || this.nullProbe;
}
showHelp() {
return "https://8bitworkshop.com/docs/platforms/nes/";
}
getDebugSymbolFile() {
var sym = this.debugSymbols.addr2symbol;
var text = "";
$.each(sym, function (k, v) {
let symType;
if (k < 0x2000) {
k = k % 0x800;
symType = "R";
}
else if (k < 0x6000)
symType = "G";
else if (k < 0x8000) {
k = k - 0x6000;
symType = "S";
}
else {
k = k - 0x8000;
symType = "P";
}
let addr = Number(k).toString(16).padStart(4, '0').toUpperCase();
// Mesen doesn't allow lables to start with digits
if (v[0] >= '0' && v[0] <= '9') {
v = "L" + v;
}
// nor does it allow dots
v = v.replaceAll('.', '_');
text += `${symType}:${addr}:${v}\n`;
});
return {
extension: ".mlb",
blob: new Blob([text], { type: "text/plain" })
};
}
}
/// MAME support
class NESMAMEPlatform extends mameplatform_1.BaseMAME6502Platform {
constructor() {
super(...arguments);
this.getToolForFilename = baseplatform_1.getToolForFilename_6502;
this.getOpcodeMetadata = baseplatform_1.getOpcodeMetadata_6502;
}
start() {
}
loadROM(title, data) {
if (!this.started) {
this.startModule(this.mainElement, {
jsfile: 'mame8bitws.js',
//cfgfile:'nes.cfg',
driver: 'nes',
width: 256,
height: 240,
romfn: '/emulator/cart.nes',
romdata: new Uint8Array(data),
preInit: function (_self) {
},
});
}
else {
// look at iNES header for PRG and CHR ROM lengths
var prgromlen = data[4] * 0x4000;
var chrromlen = data[5] * 0x2000;
this.loadROMFile(data);
this.loadRegion(":nes_slot:cart:prg_rom", data.slice(0x10, 0x10 + prgromlen));
this.loadRegion(":nes_slot:cart:chr_rom", data.slice(0x10 + prgromlen, 0x10 + prgromlen + chrromlen));
}
}
getPresets() { return JSNES_PRESETS; }
getDefaultExtension() { return ".c"; }
;
}
///
emu_1.PLATFORMS['nes'] = JSNESPlatform;
emu_1.PLATFORMS['nes-asm'] = JSNESPlatform;
emu_1.PLATFORMS['nes.mame'] = NESMAMEPlatform;
//# sourceMappingURL=nes.js.map