mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-09-27 08:54:48 +00:00
475 lines
16 KiB
TypeScript
475 lines
16 KiB
TypeScript
"use strict";
|
|
|
|
import { Platform, BasePlatform, cpuStateToLongString_6502, dumpStackToString, DisasmLine, CpuState } from "../common/baseplatform";
|
|
import { PLATFORMS, dumpRAM, EmuHalt } from "../common/emu";
|
|
import { hex, lpad, tobin } from "../common/util";
|
|
import { CodeAnalyzer_vcs } from "../common/analysis";
|
|
import { disassemble6502 } from "../common/cpu/disasm6502";
|
|
import { ProbeRecorder } from "../common/recorder";
|
|
import { NullProbe, ProbeAll } from "../common/devices";
|
|
import { BaseMAME6502Platform } from "../common/mameplatform";
|
|
|
|
declare var Javatari : any;
|
|
declare var jt : any; // 6502
|
|
|
|
const VCS_PRESETS = [
|
|
{id:'examples/hello.a', chapter:4, name:'Hello 6502 and TIA'},
|
|
{id:'examples/vsync.a', chapter:5, name:'Painting on the CRT', title:'Color Bars'},
|
|
{id:'examples/playfield.a', chapter:6, name:'Playfield Graphics'},
|
|
{id:'examples/sprite.a', chapter:7, name:'Players and Sprites'},
|
|
{id:'examples/colorsprites.a', chapter:8, name:'Color Sprites'},
|
|
{id:'examples/timing2.a', chapter:9, name:'Fine Positioning', title:'Fine Position'},
|
|
{id:'examples/missiles.a', chapter:10, name:'Player/Missile Graphics', title:'Player/Missile'},
|
|
{id:'examples/sethorizpos.a', chapter:11, name:'SetHorizPos Routine'},
|
|
{id:'examples/piatimer.a', chapter:12, name:'PIA Timer'},
|
|
{id:'examples/controls.a', chapter:13, name:'Joysticks'},
|
|
{id:'examples/complexscene.a', chapter:15, name:'Complex Scene I'},
|
|
{id:'examples/complexscene2.a', chapter:16, name:'Complex Scene II'},
|
|
{id:'examples/scoreboard.a', chapter:18, name:'Scoreboard'},
|
|
{id:'examples/collisions.a', chapter:19, name:'Collisions'},
|
|
{id:'examples/bitmap.a', chapter:20, name:'Async Playfield: Bitmap', title:'Async PF Bitmap'},
|
|
{id:'examples/brickgame.a', chapter:21, name:'Async Playfield: Bricks', title:'Async PF Bricks'},
|
|
// {id:'examples/multisprite1.a', chapter:8, name:'Sprite Kernel'},
|
|
{id:'examples/bigsprite.a', chapter:22, name:'A Big 48-Pixel Sprite', title:'48-Pixel Sprite'},
|
|
{id:'examples/tinyfonts2.a', chapter:23, name:'Tiny Text'},
|
|
{id:'examples/score6.a', chapter:24, name:'6-Digit Score'},
|
|
{id:'examples/retrigger.a', chapter:26, name:'Sprite Formations'},
|
|
// {id:'examples/tinyfonts.a', chapter:23, name:'Tiny Fonts, Slow'},
|
|
{id:'examples/multisprite3.a', chapter:28, name:'Multisprites'},
|
|
{id:'examples/procgen1.a', chapter:30, name:'Procedural Generation'},
|
|
{id:'examples/lines.a', chapter:31, name:'Drawing Lines'},
|
|
// {id:'examples/piatable.a', name:'Timer Table'},
|
|
{id:'examples/musicplayer.a', chapter:32, name:'Music Player'},
|
|
{id:'examples/road.a', chapter:33, name:'Pseudo 3D Road'},
|
|
{id:'examples/bankswitching.a', chapter:35, name:'Bankswitching'},
|
|
{id:'examples/wavetable.a', chapter:36, name:'Wavetable Sound'},
|
|
{id:'examples/fracpitch.a', name:'Fractional Pitch'},
|
|
{id:'examples/pal.a', name:'PAL Video Output'},
|
|
// {id:'examples/testlibrary.a', name:'VCS Library Demo'},
|
|
// {id:'examples/music2.a', name:'Pitch-Accurate Music'},
|
|
// {id:'examples/fullgame.a', name:'Thru Hike: The Game', title:'Thru Hike'},
|
|
{id:'bb/helloworld.bas', name:'Hello World (batariBASIC)'},
|
|
{id:'bb/draw.bas', name:'Playfield Draw (batariBASIC)'},
|
|
{id:'bb/sample.bas', name:'Sprite Test (batariBASIC)'},
|
|
{id:'bb/FIFA1977.bas', name:'2P Soccer Game (batariBASIC)'},
|
|
{id:'bb/duck_chase.bas', name:'Duck Chase (batariBASIC)'},
|
|
{id:'wiz/finalduck.wiz', name:'Final Duck (Wiz)'},
|
|
// {id:'bb/rblast106.bas', name:'Road Blasters (batariBASIC)'},
|
|
];
|
|
|
|
Javatari.AUTO_START = false;
|
|
Javatari.SHOW_ERRORS = false;
|
|
Javatari.CARTRIDGE_CHANGE_DISABLED = true;
|
|
Javatari.DEBUG_SCANLINE_OVERFLOW = false; // TODO: make a switch
|
|
Javatari.AUDIO_BUFFER_SIZE = 256;
|
|
|
|
class VCSPlatform extends BasePlatform {
|
|
|
|
lastBreakState; // last breakpoint state
|
|
|
|
// TODO: super hack for ProbeBitmap view
|
|
machine = {
|
|
cpuCyclesPerLine: 76 // NTSC
|
|
};
|
|
|
|
getPresets() { return VCS_PRESETS; }
|
|
|
|
start() {
|
|
var self : VCSPlatform = this;
|
|
$("#javatari-div").show();
|
|
Javatari.start();
|
|
var console = Javatari.room.console;
|
|
// intercept clockPulse function
|
|
console.oldClockPulse = console.clockPulse;
|
|
console.clockPulse = function() {
|
|
self.updateRecorder();
|
|
self.probe.logNewFrame();
|
|
this.oldClockPulse();
|
|
// look for KIL instruction
|
|
if (Javatari.room.console.getCPUState().o == 0x02 && Javatari.room.console.onBreakpointHit != null) {
|
|
Javatari.room.console.onBreakpointHit(Javatari.room.console.saveState());
|
|
//throw new EmuHalt("CPU STOPPED"); // TODO: requires browser reload
|
|
}
|
|
}
|
|
// intercept TIA end of line
|
|
var videoSignal = console.tia.getVideoOutput();
|
|
videoSignal.oldNextLine = videoSignal.nextLine;
|
|
videoSignal.nextLine = function(pixels, vsync) {
|
|
self.probe.logNewScanline();
|
|
return this.oldNextLine(pixels, vsync);
|
|
}
|
|
// setup mouse events
|
|
var rasterPosBreakFn = (e) => {
|
|
if (e.ctrlKey) {
|
|
console.resetDebug();
|
|
var vcanvas = $(e.target);
|
|
var x = e.pageX - vcanvas.offset().left;
|
|
var y = e.pageY - vcanvas.offset().top;
|
|
var new_x = Math.floor(x * 152 / vcanvas.width());
|
|
var new_y = Math.floor((y-10) * (192+37+30) / vcanvas.height());
|
|
this.runEval( (c) => {
|
|
var pos = this.getRasterPosition();
|
|
return (pos.x >= new_x) && (pos.y >= new_y);
|
|
});
|
|
}
|
|
};
|
|
var jacanvas = $("#javatari-screen").find("canvas");
|
|
jacanvas.mousedown(rasterPosBreakFn);
|
|
this.resize();
|
|
}
|
|
|
|
loadROM(title, data) {
|
|
if (data.length == 0 || ((data.length & 0x3ff) != 0))
|
|
throw new EmuHalt("Invalid ROM length: " + data.length);
|
|
// TODO: parse Log messages from Javatari?
|
|
var wasrunning = this.isRunning();
|
|
Javatari.loadROM(title, data);
|
|
if (!this.isRunning()) throw Error("Could not load ROM");
|
|
if (!wasrunning) this.pause();
|
|
}
|
|
|
|
getOpcodeMetadata(opcode, offset) {
|
|
return Javatari.getOpcodeMetadata(opcode, offset);
|
|
}
|
|
|
|
getRasterPosition() : {x:number,y:number,clk:number} {
|
|
var clkfs = Javatari.room.console.getClocksFromFrameStart() - 1;
|
|
var row = Math.floor(clkfs/76);
|
|
var col = clkfs - row*76;
|
|
var xpos = col*3-68;
|
|
var ypos = row-39;
|
|
return {x:xpos, y:ypos, clk:clkfs%76};
|
|
}
|
|
getRasterScanline() : number {
|
|
return this.getRasterPosition().y;
|
|
}
|
|
|
|
// TODO: Clock changes this on event, so it may not be current
|
|
isRunning() {
|
|
//console.log(Javatari.room.console.isRunning(), Javatari.room.console.isPowerOn);
|
|
return Javatari.room && Javatari.room.console.isRunning();
|
|
}
|
|
pause() {
|
|
Javatari.room.console.pause();
|
|
Javatari.room.speaker.mute();
|
|
}
|
|
resume() {
|
|
Javatari.room.console.go();
|
|
// for browser autostart
|
|
Javatari.room.speaker.powerOff();
|
|
Javatari.room.speaker.powerOn();
|
|
}
|
|
advance() : number {
|
|
Javatari.room.console.clockPulse();
|
|
return 0; // TODO: advanceFrameClock() and return 76*262 (or PAL)
|
|
}
|
|
/*
|
|
advanceFrameClock?(trap: DebugCondition, step: number) : number {
|
|
this.runEval( (c) => {
|
|
var clkfs = Javatari.room.console.getClocksFromFrameStart() - 1;
|
|
return clkfs >= step;
|
|
});
|
|
return step;
|
|
}
|
|
*/
|
|
// for unit test
|
|
nextFrame() {
|
|
Javatari.room.console.clockPulse();
|
|
}
|
|
|
|
step() { Javatari.room.console.debugSingleStepCPUClock(); }
|
|
stepBack() { Javatari.room.console.debugStepBackInstruction(); }
|
|
runEval(evalfunc) { Javatari.room.console.debugEval(evalfunc); }
|
|
|
|
setupDebug(callback) {
|
|
Javatari.room.console.onBreakpointHit = (state) => {
|
|
state.c.PC = (state.c.PC - 1) & 0xffff;
|
|
this.fixState(state);
|
|
Javatari.room.console.pause();
|
|
Javatari.room.speaker.mute();
|
|
this.lastBreakState = state;
|
|
callback(state);
|
|
}
|
|
Javatari.room.speaker.mute();
|
|
}
|
|
isDebugging() : boolean {
|
|
// TODO: always true
|
|
return Javatari.room.console.onBreakpointHit != null;
|
|
}
|
|
clearDebug() {
|
|
this.lastBreakState = null;
|
|
Javatari.room.console.disableDebug();
|
|
Javatari.room.console.onBreakpointHit = null;
|
|
if (this.isRunning()) Javatari.room.speaker.play();
|
|
}
|
|
|
|
reset() {
|
|
Javatari.room.console.powerOff();
|
|
Javatari.room.console.resetDebug();
|
|
Javatari.room.console.powerOn();
|
|
Javatari.room.speaker.play();
|
|
}
|
|
getOriginPC() {
|
|
return (this.readAddress(0xfffc) | (this.readAddress(0xfffd) << 8)) & 0xffff;
|
|
}
|
|
newCodeAnalyzer() {
|
|
return new CodeAnalyzer_vcs(this);
|
|
}
|
|
saveState() {
|
|
var state = Javatari.room.console.saveState();
|
|
this.fixState(state);
|
|
return state;
|
|
}
|
|
fixState(state) {
|
|
var ofs = (state.ca && state.ca.bo) || 0;
|
|
if (state.ca && state.ca.fo && (state.c.PC & 0xfff) >= 2048)
|
|
ofs = state.ca.fo; // 3E/3F fixed-slice formats
|
|
// TODO: for batari BASIC
|
|
state.c.EPC = state.c.PC + ofs; // EPC = effective PC for ROM
|
|
}
|
|
loadState(state) {
|
|
return Javatari.room.console.loadState(state);
|
|
}
|
|
getCPUState() : CpuState {
|
|
return Javatari.room.console.getCPUState();
|
|
}
|
|
saveControlsState() {
|
|
return Javatari.room.console.saveControlsState();
|
|
}
|
|
loadControlsState(state) {
|
|
Javatari.room.console.loadControlsState(state);
|
|
}
|
|
readAddress(addr) {
|
|
// TODO: shouldn't have to do this when debugging
|
|
if (this.lastBreakState && addr >= 0x80 && addr < 0x100)
|
|
return this.getRAMForState(this.lastBreakState)[addr & 0x7f];
|
|
else if ((addr & 0x1280) === 0x280)
|
|
return 0; // don't read PIA
|
|
else
|
|
return Javatari.room.console.readAddress(addr);
|
|
}
|
|
writeAddress(addr,value) {
|
|
Javatari.room.console.writeAddress(addr,value);
|
|
}
|
|
runUntilReturn() {
|
|
var depth = 1;
|
|
this.runEval( (c) => {
|
|
if (depth <= 0 && c.T == 0)
|
|
return true;
|
|
if (c.o == 0x20)
|
|
depth++;
|
|
else if (c.o == 0x60 || c.o == 0x40)
|
|
--depth;
|
|
return false;
|
|
});
|
|
}
|
|
runToVsync() {
|
|
this.advance();
|
|
this.runEval( (c) => { return true; } );
|
|
}
|
|
cpuStateToLongString(c) {
|
|
return cpuStateToLongString_6502(c);
|
|
}
|
|
getRAMForState(state) {
|
|
return jt.Util.byteStringToUInt8Array(atob(state.r.b));
|
|
}
|
|
ramStateToLongString(state) {
|
|
var ram = this.getRAMForState(state);
|
|
return "\n" + dumpRAM(ram, 0x80, 0x80);
|
|
}
|
|
getToolForFilename(fn) {
|
|
if (fn.endsWith(".wiz")) return "wiz";
|
|
if (fn.endsWith(".bb") || fn.endsWith(".bas")) return "bataribasic";
|
|
if (fn.endsWith(".ca65")) return "ca65";
|
|
return "dasm";
|
|
}
|
|
getDefaultExtension() { return ".a"; }
|
|
|
|
getROMExtension() { return ".a26"; }
|
|
|
|
getDebugCategories() {
|
|
return ['CPU','Stack','PIA','TIA'];
|
|
}
|
|
getDebugInfo(category, state) {
|
|
switch (category) {
|
|
case 'CPU': return this.cpuStateToLongString(state.c) + this.bankSwitchStateToString(state);
|
|
case 'Stack': return dumpStackToString(this, this.getRAMForState(state), 0x100, 0x1ff, 0x100+state.c.SP, 0x20);
|
|
case 'PIA': return this.ramStateToLongString(state) + "\n" + this.piaStateToLongString(state.p);
|
|
case 'TIA': return this.tiaStateToLongString(state.t);
|
|
}
|
|
}
|
|
bankSwitchStateToString(state) {
|
|
return (state.ca && state.ca.bo !== undefined) ? "BankOffset "+hex(state.ca.bo,4)+"\n" : "";
|
|
}
|
|
piaStateToLongString(p) {
|
|
return "Timer " + p.t + "/" + p.c + "\nINTIM $" + hex(p.IT,2) + " (" + p.IT + ")\nINSTAT $" + hex(p.IS,2) + "\n";
|
|
}
|
|
tiaStateToLongString(t) {
|
|
var pos = this.getRasterPosition();
|
|
var s = '';
|
|
s += "H" + lpad(pos.x.toString(),5) + " (clk " + lpad(pos.clk.toString(),3) + ") V" + lpad(pos.y.toString(),5) + " ";
|
|
s += (t.vs?"VSYNC ":"- ") + (t.vb?"VBLANK ":"- ") + "\n";
|
|
s += "\n";
|
|
s += "Playfield " + t.f + "\n";
|
|
s += " " + (t.fr?"REFLECT ":"- ") + (t.fs?"SCOREMODE ":"- ") + (t.ft?"PRIORITY ":"- ") + "\n";
|
|
for (var j=0; j<2; j++) {
|
|
var i = "p"+j;
|
|
s += "Player"+j+ lpad(tobin(t[i]),11) + lpad(tobin(t[i+'d']),11) + "\n";
|
|
}
|
|
s += "\n";
|
|
// TODO? s += " Color {color:0x" + hex(t.fc) + "} {color:0x" + hex(t.fb) + "}\n";
|
|
s += " Count Scan Speed\n";
|
|
for (var j=0; j<2; j++) {
|
|
var i = "p"+j;
|
|
s += "Player"+j+ lpad(t[i+'co'],8) + lpad(nonegstr(t[i+'sc']),5) + lpad(t[i+'ss'],6);
|
|
s += " " + (t[i+'rr']?"RESET":"") + " " + (t[i+'v']?"DELAY":"") + " " + (t[i+'cc']?"CLOSECOPY":"") + " " + (t[i+'mc']?"MEDCOPY":"") + " " + (t[i+'wc']?"WIDECOPY":"") + " " + (t[i+'r']?"REFLECT":"") + "\n";
|
|
}
|
|
for (var j=0; j<2; j++) {
|
|
var i = "m"+j;
|
|
s += "Missile"+j+ lpad(t[i+'co'],7) + lpad(nonegstr(t[i+'sc']),5) + lpad(t[i+'ss'],6);
|
|
s += " " + (t[i+'rr']?"RESET":"") + " " + (t[i+'r']?"RESET2PLAYER":"") + "\n";
|
|
}
|
|
s += "Ball"+ lpad(t['bco'],11) + lpad(nonegstr(t['bsc']),5) + lpad(t['bss'],6) + "\n";
|
|
return s;
|
|
}
|
|
|
|
disassemble(pc:number, read:(addr:number)=>number) : DisasmLine {
|
|
return disassemble6502(pc, read(pc), read(pc+1), read(pc+2));
|
|
}
|
|
|
|
showHelp(tool:string, ident:string) {
|
|
if (tool == 'bataribasic')
|
|
window.open("help/bataribasic/manual.html", "_help");
|
|
else if (tool == 'wiz')
|
|
window.open("https://github.com/wiz-lang/wiz/blob/master/readme.md#wiz", "_help");
|
|
else
|
|
window.open("https://8bitworkshop.com/docs/platforms/vcs/", "_help"); // TODO
|
|
}
|
|
|
|
getMemoryMap = function() { return {main:[
|
|
{name:'TIA Registers',start:0x00,size:0x80,type:'io'},
|
|
{name:'PIA RAM',start:0x80,size:0x80,type:'ram'},
|
|
{name:'PIA Ports and Timer',start:0x280,size:0x18,type:'io'},
|
|
{name:'Cartridge ROM',start:0xf000,size:0x1000,type:'rom'},
|
|
]}};
|
|
|
|
// probing
|
|
nullProbe = new NullProbe();
|
|
probe : ProbeAll = this.nullProbe;
|
|
|
|
startProbing?() : ProbeRecorder {
|
|
var self : VCSPlatform = this;
|
|
var rec = new ProbeRecorder(this);
|
|
this.connectProbe(rec);
|
|
var probe = this.probe;
|
|
// intercept CPU clock pulse
|
|
var cpu = Javatari.room.console.cpu;
|
|
if (cpu.oldCPUClockPulse == null) {
|
|
cpu.oldCPUClockPulse = cpu.clockPulse;
|
|
cpu.clockPulse = function() {
|
|
if (cpu.isPCStable())
|
|
probe.logExecute(cpu.getPC(), cpu.getSP());
|
|
this.oldCPUClockPulse();
|
|
probe.logClocks(1);
|
|
}
|
|
}
|
|
// intercept bus read/write
|
|
var bus = Javatari.room.console.bus;
|
|
if (bus.oldRead == null) {
|
|
bus.oldRead = bus.read;
|
|
bus.read = function(a) {
|
|
var v = this.oldRead(a);
|
|
probe.logRead(a,v);
|
|
return v;
|
|
}
|
|
bus.oldWrite = bus.write;
|
|
bus.write = function(a,v) {
|
|
this.oldWrite(a,v);
|
|
probe.logWrite(a,v);
|
|
}
|
|
}
|
|
return rec;
|
|
}
|
|
stopProbing?() : void {
|
|
this.connectProbe(null);
|
|
var cpu = Javatari.room.console.cpu;
|
|
if (cpu.oldCPUClockPulse != null) {
|
|
cpu.clockPulse = cpu.oldCPUClockPulse;
|
|
cpu.oldCPUClockPulse = null;
|
|
}
|
|
var bus = Javatari.room.console.bus;
|
|
if (bus.oldRead) {
|
|
bus.read = bus.oldRead;
|
|
bus.oldRead = null;
|
|
}
|
|
if (bus.oldWrite) {
|
|
bus.write = bus.oldWrite;
|
|
bus.oldWrite = null;
|
|
}
|
|
}
|
|
connectProbe(probe:ProbeAll) {
|
|
this.probe = probe || this.nullProbe;
|
|
}
|
|
|
|
// resizing
|
|
resize() {
|
|
var scale = Math.min(1, ($('#emulator').width() - 24) / 640);
|
|
var xt = (1 - scale) * 50;
|
|
$('#javatari-div').css('transform', `translateX(-${xt}%) translateY(-${xt}%) scale(${scale})`);
|
|
}
|
|
};
|
|
|
|
// TODO: mixin for Base6502Platform?
|
|
|
|
function nonegstr(n) {
|
|
return n < 0 ? "-" : n.toString();
|
|
}
|
|
|
|
///////////////
|
|
|
|
class VCSMAMEPlatform extends BaseMAME6502Platform implements Platform {
|
|
|
|
// MCFG_SCREEN_RAW_PARAMS( MASTER_CLOCK_NTSC, 228, 26, 26 + 160 + 16, 262, 24 , 24 + 192 + 31 )
|
|
|
|
start = function() {
|
|
this.startModule(this.mainElement, {
|
|
jsfile:'mame8bitws.js',
|
|
driver:'a2600',
|
|
width:176*2,
|
|
height:223,
|
|
romfn:'/emulator/cart.rom',
|
|
romsize:0x1000,
|
|
});
|
|
}
|
|
|
|
loadROM = function(title, data) {
|
|
this.loadROMFile(data);
|
|
this.loadRegion(":cartslot:cart:rom", data);
|
|
}
|
|
|
|
getPresets = function() { return VCS_PRESETS; }
|
|
|
|
getToolForFilename = function(fn) {
|
|
if (fn.endsWith(".wiz")) return "wiz";
|
|
if (fn.endsWith(".bb") || fn.endsWith(".bas")) return "bataribasic";
|
|
if (fn.endsWith(".ca65")) return "ca65";
|
|
return "dasm";
|
|
}
|
|
getDefaultExtension = function() { return ".a"; };
|
|
|
|
getOriginPC = function() {
|
|
return (this.readAddress(0xfffc) | (this.readAddress(0xfffd) << 8)) & 0xffff;
|
|
}
|
|
|
|
}
|
|
|
|
////////////////
|
|
|
|
export const Platforms = {
|
|
'vcs': VCSPlatform,
|
|
'vcs.mame': VCSMAMEPlatform,
|
|
}
|
|
|
|
PLATFORMS['vcs'] = VCSPlatform;
|
|
PLATFORMS['vcs.mame'] = VCSMAMEPlatform;
|