8bitworkshop/gen/platform/vcs.js

532 lines
22 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 disasm6502_1 = require("../common/cpu/disasm6502");
const probe_1 = require("../common/probe");
const devices_1 = require("../common/devices");
const mameplatform_1 = require("../common/mameplatform");
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/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: 'examples/fracpitch.a', name: 'Fractional Pitch', category: 'BASIC and Other Languages' },
{ 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)'},
{ id: 'vcslib/demo_vcslib.c', name: 'VCSLib Demo (C)' },
];
function getToolForFilename_vcs(fn) {
if (fn.endsWith("-llvm.c"))
return "remote:llvm-mos";
if (fn.endsWith(".wiz"))
return "wiz";
if (fn.endsWith(".bb") || fn.endsWith(".bas"))
return "bataribasic";
if (fn.endsWith(".ca65"))
return "ca65";
if (fn.endsWith(".acme"))
return "acme";
//if (fn.endsWith(".inc")) return "ca65";
if (fn.endsWith(".c"))
return "cc65";
//if (fn.endsWith(".h")) return "cc65";
if (fn.endsWith(".ecs"))
return "ecs";
return "dasm";
}
class VCSPlatform extends baseplatform_1.BasePlatform {
constructor() {
super(...arguments);
// TODO: super hack for ProbeBitmap view
this.machine = {
cpuCyclesPerLine: 76 // NTSC
};
this.getToolForFilename = getToolForFilename_vcs;
this.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 - 6, type: 'rom' },
{ name: 'CPU Vectors', start: 0xfffa, size: 0x6, type: 'rom' },
] };
};
// probing
this.nullProbe = new devices_1.NullProbe();
this.probe = this.nullProbe;
}
getPresets() { return VCS_PRESETS; }
async start() {
var self = this;
// load Javatari and configure settings
await (0, util_1.loadScript)("javatari/javatari.js");
Javatari.AUTO_START = false;
Javatari.SHOW_ERRORS = false;
Javatari.CARTRIDGE_CHANGE_DISABLED = true;
Javatari.DEBUG_SCANLINE_OVERFLOW = false; // TODO: integrate into probe API
Javatari.AUDIO_BUFFER_SIZE = 256;
// show console div and start
$("#javatari-div").show();
Javatari.start();
var jaconsole = Javatari.room.console;
// intercept clockPulse function
jaconsole.oldClockPulse = jaconsole.clockPulse;
jaconsole.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 = jaconsole.tia.getVideoOutput();
videoSignal.oldNextLine = videoSignal.nextLine;
videoSignal.nextLine = function (pixels, vsync) {
self.probe.logNewScanline();
return this.oldNextLine(pixels, vsync);
};
// resize after added to dom tree
var jacanvas = $("#javatari-screen").find("canvas")[0];
const resizeObserver = new ResizeObserver(entries => {
this.resize();
});
resizeObserver.observe(jacanvas);
this.canvas = jacanvas;
}
loadROM(title, data) {
if (data.length == 0 || ((data.length & 0x3ff) != 0))
throw new emu_1.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() {
var clkfs = Javatari.room.console.getClocksFromFrameStart() - 1;
var row = Math.floor(clkfs / 76);
var col = clkfs - row * 76;
var xpos = col * 3;
var ypos = row;
return { x: xpos, y: ypos, clk: clkfs % 76 };
}
getRasterScanline() {
return this.getRasterPosition().y;
}
getRasterLineClock() {
return this.getRasterPosition().x;
}
getRasterCanvasPosition() {
let p = Javatari.room.console.tia.getVideoOutput().monitor.getDisplayParameters();
let { x, y } = this.getRasterPosition();
let canvasPos = {
x: (x - p.displayOriginX) * p.displayWidth * p.displayScaleX / (p.signalWidth - p.displayOriginX),
y: (y - p.displayOriginY) * p.displayHeight * p.displayScaleY / p.displayHeight
};
console.log(x, y, canvasPos, p);
return canvasPos;
}
// 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() {
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);
// TODO: we have to delay because javatari timer is still running
setTimeout(() => this.updateVideoDebugger(), 100);
};
Javatari.room.speaker.mute();
}
isDebugging() {
// 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 analysis_1.CodeAnalyzer_vcs(this);
}
saveState() {
var state = Javatari.room.console.saveState();
this.fixState(state);
return state;
}
fixState(state) {
var _a, _b, _c;
// TODO: DASM listing prevents us from using RORG offset
// TODO: how to handle 1000/3000/etc vs overlapping addresses?
if (((_a = state.ca) === null || _a === void 0 ? void 0 : _a.f) != '3E' && ((_b = state.ca) === null || _b === void 0 ? void 0 : _b.f) != '3F') {
var ofs = ((_c = state.ca) === null || _c === void 0 ? void 0 : _c.bo) || 0;
// 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() {
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
// TODO: don't read bank switch addresses
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 (0, baseplatform_1.cpuStateToLongString_6502)(c);
}
getRAMForState(state) {
return jt.Util.byteStringToUInt8Array(atob(state.r.b));
}
ramStateToLongString(state) {
var ram = this.getRAMForState(state);
return "\n" + (0, emu_1.dumpRAM)(ram, 0x80, 0x80);
}
getDefaultExtension() { return ".dasm"; }
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 (0, baseplatform_1.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) {
var _a, _b;
if (((_a = state.ca) === null || _a === void 0 ? void 0 : _a.ro) >= 0)
return "RAMOffset " + (0, util_1.hex)(state.ca.ro, 4) + "\n";
return (((_b = state.ca) === null || _b === void 0 ? void 0 : _b.bo) >= 0) ? "BankOffset " + (0, util_1.hex)(state.ca.bo, 4) + "\n" : "";
}
piaStateToLongString(p) {
return "Timer " + p.t + "/" + p.c + "\nINTIM $" + (0, util_1.hex)(p.IT, 2) + " (" + p.IT + ")\nINSTAT $" + (0, util_1.hex)(p.IS, 2) + "\n";
}
tiaStateToLongString(t) {
var pos = this.getRasterPosition();
var s = '';
s += "H" + (0, util_1.lpad)(pos.x.toString(), 5) + " (clk " + (0, util_1.lpad)(pos.clk.toString(), 3) + ") V" + (0, util_1.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 + (0, util_1.lpad)((0, util_1.tobin)(t[i]), 11) + (0, util_1.lpad)((0, util_1.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 + (0, util_1.lpad)(t[i + 'co'], 8) + (0, util_1.lpad)(nonegstr(t[i + 'sc']), 5) + (0, util_1.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 + (0, util_1.lpad)(t[i + 'co'], 7) + (0, util_1.lpad)(nonegstr(t[i + 'sc']), 5) + (0, util_1.lpad)(t[i + 'ss'], 6);
s += " " + (t[i + 'rr'] ? "RESET" : "") + " " + (t[i + 'r'] ? "RESET2PLAYER" : "") + "\n";
}
s += "Ball" + (0, util_1.lpad)(t['bco'], 11) + (0, util_1.lpad)(nonegstr(t['bsc']), 5) + (0, util_1.lpad)(t['bss'], 6) + "\n";
return s;
}
disassemble(pc, read) {
return (0, disasm6502_1.disassemble6502)(pc, read(pc), read(pc + 1), read(pc + 2));
}
showHelp() {
return "https://8bitworkshop.com/docs/platforms/vcs/";
}
startProbing() {
var self = this;
var rec = new probe_1.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);
if (a > 0 && a < 0x80)
probe.logIORead(a, v); // (00),x reads $00?
else if (a > 0x280 && a < 0x300)
probe.logIORead(a, v);
else
probe.logRead(a, v);
return v;
};
bus.oldWrite = bus.write;
bus.write = function (a, v) {
this.oldWrite(a, v);
if (a == 0x02)
probe.logWait(a); // WSYNC
else if (a < 0x80)
probe.logIOWrite(a, v);
else if (a > 0x280 && a < 0x300)
probe.logIOWrite(a, v);
else
probe.logWrite(a, v);
};
}
return rec;
}
stopProbing() {
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) {
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})`);
}
updateVideoDebugger() {
var _a;
const { x, y } = this.getRasterCanvasPosition();
if (x >= 0 || y >= 0) {
const ctx = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getContext('2d');
if (ctx) {
(0, emu_1.drawCrosshair)(ctx, x, y, 2);
}
}
}
}
;
// TODO: mixin for Base6502Platform?
function nonegstr(n) {
return n < 0 ? "-" : n.toString();
}
///////////////
class VCSMAMEPlatform extends mameplatform_1.BaseMAME6502Platform {
constructor() {
// MCFG_SCREEN_RAW_PARAMS( MASTER_CLOCK_NTSC, 228, 26, 26 + 160 + 16, 262, 24 , 24 + 192 + 31 )
super(...arguments);
this.start = function () {
this.startModule(this.mainElement, {
jsfile: 'mame8bitws.js',
driver: 'a2600',
width: 176 * 2,
height: 223,
romfn: '/emulator/cart.rom',
romsize: 0x1000,
});
};
this.loadROM = function (title, data) {
this.loadROMFile(data);
this.loadRegion(":cartslot:cart:rom", data);
};
this.getPresets = function () { return VCS_PRESETS; };
this.getToolForFilename = getToolForFilename_vcs;
this.getOriginPC = function () {
return (this.readAddress(0xfffc) | (this.readAddress(0xfffd) << 8)) & 0xffff;
};
}
getDefaultExtension() { return ".dasm"; }
getROMExtension() { return ".a26"; }
}
////////////////
class VCSStellaPlatform {
constructor(mainElement) {
this.running = false;
this.getToolForFilename = getToolForFilename_vcs;
this.mainElement = mainElement;
}
async start() {
await (0, util_1.loadScript)('lib/stellerator/stellerator-embedded.min.js');
const $6502 = window['$6502'];
this.Stellerator = $6502.Stellerator;
// create a canvas, stellerator will override width/height but we need CSS aspect ratio
const canvas = (0, emu_1.__createCanvas)(window.document, this.mainElement, 28, 20);
// stellerator adds overscan, we don't need as much
canvas.style.padding = '10px';
this.stellerator = new this.Stellerator(canvas, 'lib/stellerator/stellerator.min.js', {
gamma: 0.8,
scalingMode: this.Stellerator.ScalingMode.qis,
tvEmulation: this.Stellerator.TvEmulation.composite,
phosphorLevel: 0.25,
scanlineLevel: 0.2,
keyboardTarget: this.mainElement
});
}
loadROM(title, data) {
this.stellerator.run(data, this.Stellerator.TvMode.ntsc);
}
reset() {
this.stellerator.reset();
}
pause() {
this.running = false;
this.stellerator.pause();
}
resume() {
this.running = true;
this.stellerator.resume();
}
isRunning() {
return this.running;
}
getDefaultExtension() { return ".dasm"; }
getROMExtension() { return ".a26"; }
getPresets() { return VCS_PRESETS; }
}
////////////////
emu_1.PLATFORMS['vcs'] = VCSPlatform;
emu_1.PLATFORMS['vcs.mame'] = VCSMAMEPlatform;
emu_1.PLATFORMS['vcs.stellerator'] = VCSStellaPlatform;
//# sourceMappingURL=vcs.js.map