mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 15:05:25 +00:00
718 lines
32 KiB
JavaScript
718 lines
32 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const baseplatform_1 = require("../common/baseplatform");
|
|
const vcs_1 = require("../machine/vcs");
|
|
const toolselect_1 = require("../common/toolselect");
|
|
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)' },
|
|
{ id: 'helloworld.cc2600', name: 'Hello World (cc2600)' },
|
|
];
|
|
class VCSPlatform extends baseplatform_1.BasePlatform {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.frame = null; // see captureVideo()
|
|
this.capturing = false;
|
|
// TODO: super hack for ProbeBitmap view
|
|
this.machine = {
|
|
cpuCyclesPerLine: 76 // NTSC
|
|
};
|
|
this.getToolForFilename = toolselect_1.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
|
|
const jadiv = document.getElementById("javatari-div");
|
|
if (jadiv)
|
|
jadiv.style.display = '';
|
|
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
|
|
// Halt passively, like vcs.jt4, instead of stopping as a breakpoint would
|
|
// (which moves the cursor). Throw outside Javatari's clock, since it
|
|
// can't recover from an exception.
|
|
if (Javatari.room.console.getCPUState().o == 0x02 && self.isRunning()) {
|
|
self.pause();
|
|
const pc = (Javatari.room.console.saveState().c.PC - 1) & 0xffff;
|
|
setTimeout(() => { throw new emu_1.EmuHalt(`CPU halted at $${pc.toString(16)}`); }, 0);
|
|
}
|
|
};
|
|
// intercept TIA end of line
|
|
var videoSignal = jaconsole.tia.getVideoOutput();
|
|
videoSignal.oldNextLine = videoSignal.nextLine;
|
|
videoSignal.nextLine = function (pixels, vsync) {
|
|
self.probe.logNewScanline();
|
|
if (self.capturing && pixels)
|
|
self.captureLine(this.monitor, pixels);
|
|
return this.oldNextLine(pixels, vsync);
|
|
};
|
|
// resize after added to dom tree
|
|
var jacanvas = document.querySelector("#javatari-screen canvas");
|
|
if (jacanvas && typeof ResizeObserver !== 'undefined') {
|
|
const resizeObserver = new ResizeObserver(entries => {
|
|
this.resize();
|
|
});
|
|
resizeObserver.observe(jacanvas);
|
|
}
|
|
this.canvas = jacanvas;
|
|
}
|
|
/**
|
|
* Javatari draws to its own screen, so hosts with no screen (EmuCore) call
|
|
* this to get the visible part of each frame instead.
|
|
*/
|
|
captureVideo() {
|
|
this.capturing = true;
|
|
return () => this.frame;
|
|
}
|
|
captureLine(monitor, pixels) {
|
|
var _a, _b;
|
|
const p = monitor.getDisplayParameters();
|
|
const width = p.displayWidth, height = p.displayHeight;
|
|
if (((_a = this.frame) === null || _a === void 0 ? void 0 : _a.width) != width || ((_b = this.frame) === null || _b === void 0 ? void 0 : _b.height) != height) {
|
|
this.frame = {
|
|
pixels: new Uint32Array(width * height), width, height,
|
|
aspect: (width * p.displayScaleX) / (height * p.displayScaleY),
|
|
};
|
|
}
|
|
const y = monitor.currentLine() - p.displayOriginY;
|
|
if (y < 0 || y >= height)
|
|
return;
|
|
const dest = this.frame.pixels;
|
|
for (let x = 0, i = y * width; x < width; x++)
|
|
dest[i++] = pixels[p.displayOriginX + x];
|
|
}
|
|
/** Keys from hosts that have no Javatari screen to type into. */
|
|
setKeyInput(key, code, flags) {
|
|
const press = (flags & emu_1.KeyFlags.KeyDown) != 0;
|
|
if (!press && !(flags & emu_1.KeyFlags.KeyUp))
|
|
return;
|
|
// Javatari's modifier masks
|
|
const modifiers = (flags & emu_1.KeyFlags.Ctrl ? 1 : 0) | (flags & emu_1.KeyFlags.Alt ? 2 : 0) | (flags & emu_1.KeyFlags.Shift ? 4 : 0);
|
|
Javatari.room.controls.processKeyEvent(key, press, modifiers);
|
|
}
|
|
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);
|
|
}
|
|
getDefaultExtensions() { return [".dasm", ".a", ".bb", ".acme", ".xa", ".ca65", ".cc2600", ".ecs", ".wiz", ".c"]; }
|
|
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() {
|
|
const emulator = document.getElementById('emulator');
|
|
const jadiv = document.getElementById('javatari-div');
|
|
if (!emulator || !jadiv)
|
|
return;
|
|
var scale = Math.min(1, (emulator.clientWidth - 24) / 640);
|
|
var xt = (1 - scale) * 50;
|
|
jadiv.style.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 = toolselect_1.getToolForFilename_vcs;
|
|
this.getOriginPC = function () {
|
|
return (this.readAddress(0xfffc) | (this.readAddress(0xfffd) << 8)) & 0xffff;
|
|
};
|
|
}
|
|
getDefaultExtensions() { return [".c", ".bb", ".acme", ".xa", ".ca65", ".dasm", ".cc2600", ".ecs", ".wiz"]; }
|
|
getROMExtension() { return ".a26"; }
|
|
}
|
|
////////////////
|
|
class VCSStellaPlatform {
|
|
constructor(mainElement) {
|
|
this.running = false;
|
|
this.getToolForFilename = toolselect_1.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;
|
|
}
|
|
getDefaultExtensions() { return [".c", ".bb", ".acme", ".xa", ".ca65", ".dasm", ".cc2600", ".ecs", ".wiz"]; }
|
|
getROMExtension() { return ".a26"; }
|
|
getPresets() { return VCS_PRESETS; }
|
|
}
|
|
////////////////
|
|
// the ROM itself, and the cartridge format properties that never change
|
|
const CART_CONTENT_KEYS = 'f r b ra e rnd'.split(' ');
|
|
const CART_STATIC_KEYS = 'bb es tb sm m fo s sa'.split(' ');
|
|
const CART_BANK_LABELS = {
|
|
bo: 'BankOffset', s0: 'Slice0', s1: 'Slice1', s2: 'Slice2',
|
|
b0o: 'Bank0Offset', b1o: 'Bank1Offset',
|
|
};
|
|
/**
|
|
* The headless Javatari core (src/machine/vcs.ts) as a Platform, registered
|
|
* as "vcs.jt4" so it runs beside VCSPlatform, which still drives the Javatari
|
|
* room UI. The two share presets, tools and the memory map, and differ only in
|
|
* the core underneath.
|
|
*
|
|
* Because this one is built on a Machine, EmuCore drives it with MachineCore:
|
|
* it stops mid-frame and mid-instruction, and it can rewind, which is what
|
|
* the room UI could not do.
|
|
*/
|
|
class VCSMachinePlatform extends baseplatform_1.Base6502MachinePlatform {
|
|
constructor() {
|
|
super(...arguments);
|
|
// Javatari's 6502 saves its PC one past the opcode it has fetched, though
|
|
// its getPC() returns the opcode's address
|
|
this.debugPCDelta = -1;
|
|
/**
|
|
* A 2600 takes joysticks or paddles in the same ports, and the core reads the
|
|
* paddle triggers as the joystick directions, so the paddles stay off until
|
|
* this is set. See JavatariMachine.setPaddlesConnected.
|
|
*/
|
|
this.paddles = false;
|
|
this.getToolForFilename = toolselect_1.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' },
|
|
] };
|
|
};
|
|
}
|
|
newMachine() { return new vcs_1.JavatariMachine(); }
|
|
getPresets() { return VCS_PRESETS; }
|
|
pollControls() {
|
|
this.machine.setPaddlesConnected(this.paddles);
|
|
super.pollControls();
|
|
}
|
|
getDefaultExtensions() { return [".c", ".bb", ".acme", ".xa", ".ca65", ".dasm", ".cc2600", ".ecs", ".wiz"]; }
|
|
getROMExtension() { return ".a26"; }
|
|
// readConst() has no side effects: it skips the TIA and PIA registers and
|
|
// undoes a bank switch, so reading memory for a view cannot change the run
|
|
readAddress(addr) { return this.machine.readConst(addr); }
|
|
writeAddress(addr, value) { this.machine.write(addr, value); }
|
|
getOriginPC() {
|
|
return (this.machine.readConst(0xfffc) | (this.machine.readConst(0xfffd) << 8)) & 0xffff;
|
|
}
|
|
newCodeAnalyzer() { return new analysis_1.CodeAnalyzer_vcs(this); }
|
|
showHelp() { return "https://8bitworkshop.com/docs/platforms/vcs/"; }
|
|
getDebugCategories() { return ['CPU', 'Stack', 'PIA RAM', '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 RAM': return this.ramStateToLongString(state);
|
|
case 'PIA': return this.piaStateToLongString(state.p);
|
|
case 'TIA': return this.tiaStateToLongString(state);
|
|
}
|
|
}
|
|
cpuStateToLongString(c) { return (0, baseplatform_1.cpuStateToLongString_6502)(c); }
|
|
bankSwitchStateToString(state) {
|
|
const ca = state.ca;
|
|
if (!ca)
|
|
return '';
|
|
// The cartridge formats keep their live bank selection under different
|
|
// keys, and ro/rs mean something different in each of them, so the ones
|
|
// that are unambiguous get a name and the rest are shown as the core
|
|
// spells them. The ROM contents and the fixed format properties are noise.
|
|
let s = 'Cart ' + ca.f + '\n';
|
|
for (const k of Object.keys(ca)) {
|
|
if (CART_CONTENT_KEYS.indexOf(k) >= 0 || CART_STATIC_KEYS.indexOf(k) >= 0)
|
|
continue;
|
|
const v = ca[k];
|
|
if (typeof v !== 'number')
|
|
continue; // e.g. the AR rom page offsets
|
|
s += (0, util_1.lpad)(CART_BANK_LABELS[k] || k, 12) + ' ' + (v < 0 ? String(v) : '$' + (0, util_1.hex)(v, 4)) + '\n';
|
|
}
|
|
return s;
|
|
}
|
|
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)
|
|
+ "\nSWCHA $" + (0, util_1.hex)(p.SA, 2) + " SWCHB $" + (0, util_1.hex)(p.SB, 2) + "\n";
|
|
}
|
|
// The core's raw states are plain signed byte arrays, not base64 strings.
|
|
// Mask to unsigned: toradix() would print a negative byte as "-1".
|
|
getRAMForState(state) {
|
|
var _a;
|
|
const b = (_a = state.r) === null || _a === void 0 ? void 0 : _a.b;
|
|
if (!b)
|
|
return new Uint8Array(0);
|
|
return Uint8Array.from(b, (v) => v & 0xff);
|
|
}
|
|
ramStateToLongString(state) {
|
|
return "\n" + (0, emu_1.dumpRAM)(this.getRAMForState(state), 0x80, 0x80);
|
|
}
|
|
/**
|
|
* The new TIA keeps per-object fields (p0e, p0lp, p0c, ...) and also saves
|
|
* every register by name, so the registers are listed from whatever the
|
|
* state happens to carry rather than from a hand-written list that the next
|
|
* core change would invalidate.
|
|
*/
|
|
tiaStateToLongString(state) {
|
|
const t = state.t;
|
|
let s = "H" + (0, util_1.lpad)(this.machine.getRasterX().toString(), 5)
|
|
+ " V" + (0, util_1.lpad)(this.machine.getRasterY().toString(), 5) + " "
|
|
+ (t.vs ? "VSYNC " : "- ") + (t.vb ? "VBLANK " : "- ") + "\n\n";
|
|
// pfl is the 20-bit pattern ((PF2<<12)|(rev PF1<<4)|(PF0&f0)>>4); pfr
|
|
// only differs from it while the playfield is reflected
|
|
s += "Playfield " + (0, util_1.lpad)((0, util_1.tobin)(t.pfl & 0xfffff), 21) + "\n";
|
|
if ((t.pfr | 0) !== (t.pfl | 0))
|
|
s += " " + (0, util_1.lpad)((0, util_1.tobin)(t.pfr & 0xfffff), 21) + "\n";
|
|
s += " " + (t.pfe ? "ON " : "- ") + (t.pfrl ? "REFLECT " : "- ")
|
|
+ (t.pfsc ? "SCOREMODE " : "- ") + (t.pfp ? "PRIORITY " : "- ") + "\n";
|
|
s += "\n Graphics Copy\n";
|
|
for (let j = 0; j < 2; j++) {
|
|
const i = "p" + j;
|
|
s += "Player" + j + (0, util_1.lpad)((0, util_1.tobin)(t[i + "lp"]), 9) + (0, util_1.lpad)((0, util_1.tobin)(t["GRP" + j + "d"]), 9) + "\n";
|
|
s += " NUSIZ " + t["NUSIZ" + j]
|
|
+ " Alt " + t[i + "a"] + "/" + t[i + "af"] + "/" + t[i + "al"] + "/" + t[i + "ao"]
|
|
+ (t[i + "e"] ? " ON" : "") + "\n";
|
|
}
|
|
s += "\n";
|
|
for (let j = 0; j < 2; j++) {
|
|
const i = "m" + j;
|
|
s += "Missile" + j + (0, util_1.lpad)(t[i + "e"] ? "ON" : "-", 3) + (0, util_1.lpad)((0, util_1.tobin)(t[i + "lp"]), 9)
|
|
+ " HMP" + (0, util_1.hex)(t["HMP" + j] & 0xff, 2) + (t["HMM" + j] ? " HM" : "") + "\n";
|
|
}
|
|
s += "Ball" + (0, util_1.lpad)(t.be ? "ON" : "-", 6) + (0, util_1.lpad)((0, util_1.tobin)(t.bx), 9) + "\n";
|
|
const regs = Object.keys(t).filter((k) => /^[A-Z][A-Z0-9]*d?$/.test(k) && typeof t[k] == "number");
|
|
if (regs.length) {
|
|
s += "\nRegisters\n";
|
|
regs.forEach((k, i) => {
|
|
s += (0, util_1.lpad)(k, 7) + (0, util_1.hex)(t[k] & 0xff, 2);
|
|
if (i % 4 == 3)
|
|
s += "\n";
|
|
});
|
|
if (regs.length % 4)
|
|
s += "\n";
|
|
}
|
|
return s;
|
|
}
|
|
}
|
|
////////////////
|
|
emu_1.PLATFORMS['vcs'] = VCSPlatform;
|
|
emu_1.PLATFORMS['vcs.jt4'] = VCSMachinePlatform;
|
|
emu_1.PLATFORMS['vcs.mame'] = VCSMAMEPlatform;
|
|
emu_1.PLATFORMS['vcs.stellerator'] = VCSStellaPlatform;
|
|
//# sourceMappingURL=vcs.js.map
|