mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
vscode: support vector video
This commit is contained in:
1 parent
f396bc4396
commit
8ce2bc5fc6
9 files changed
+202
-7
No files matched your search
@@ -8,6 +8,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md).
|
||||
|
||||
### Debugging and the `8bws` CLI
|
||||
|
||||
- Vector platforms (`vector-*`, `vectrex`) render headlessly: a software rasterizer with phosphor fade draws their lines into the frame buffer, so `--png` and the VS Code emulator panel show them. The size is `--vector-size <px>` (long side, default 512), `EmuCore.vectorSize`, or the extension's `8bitworkshop.vectorSize`.
|
||||
- New `8bws` command-line tool (`npm run cli`) to build and run programs headlessly: `build`, `run`, `verify-replay`, `profile`, `detect`, `list-platforms`, `list-tools`; scripted emulator control; `--png`, `--json`, `--symbols`, `--memdump`, `--info`; platform and main file detected from a project directory.
|
||||
- Platform detection no longer confuses the Atari 8-bit targets: `atari.h` means the 800 (it is guarded by `__ATARI__`), `atari5200.h` means the 5200, and a folder named for a platform breaks a near-tie.
|
||||
- Emulators now run on a timeline: step back, seek, reverse run, `rbreak`, and deterministic replay with a replay-verification test that found determinism bugs in several platforms.
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
- The Verilog and Verilog (VGA) platforms now appear in New Project and Open
|
||||
Example.
|
||||
- Verilog syntax highlighting.
|
||||
- Vector platforms (Atari vector, Vectrex) show their video in the emulator panel. The
|
||||
`8bitworkshop.vectorSize` setting sets its resolution (512 by default).
|
||||
- The mouse is a paddle in the emulator panel, for programs that read one
|
||||
(Verilog designs with `hpaddle`/`vpaddle` inputs, and platforms with paddles).
|
||||
- Reloading the window closes the emulator tab instead of restoring it blank.
|
||||
|
||||
@@ -342,6 +342,14 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"8bitworkshop.vectorSize": {
|
||||
"type": "number",
|
||||
"default": 512,
|
||||
"minimum": 128,
|
||||
"maximum": 2048,
|
||||
"scope": "window",
|
||||
"markdownDescription": "Pixels along the longer side of the screen for vector platforms (`vector`, `vectrex`). Takes effect on the next run."
|
||||
},
|
||||
"8bitworkshop.autoBuild": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -108,6 +108,8 @@ export interface ScriptResult {
|
||||
export interface LoadOptions {
|
||||
/** load it stopped, as a debug session does until it is configured */
|
||||
paused?: boolean;
|
||||
/** pixels along the longer side of a vector platform's screen */
|
||||
vectorSize?: number;
|
||||
}
|
||||
|
||||
/** DebugService methods the host may call through `debug`. */
|
||||
@@ -198,6 +200,7 @@ const rpc: Rpc = new Rpc(parentPort, {
|
||||
stop();
|
||||
clearLastKeycodeMap();
|
||||
target = await loadPlatform(EMULATOR_FOR[platform] || platform);
|
||||
if (opts.vectorSize) target.vectorSize = opts.vectorSize;
|
||||
await target.start();
|
||||
service = new DebugService(target, stopped);
|
||||
// machines build their keyboard handler as they start
|
||||
|
||||
@@ -198,6 +198,11 @@ function config(uri?: vscode.Uri) {
|
||||
return vscode.workspace.getConfiguration(CONFIG, uri);
|
||||
}
|
||||
|
||||
/** pixels along the longer side of a vector platform's screen */
|
||||
function vectorSize(): number {
|
||||
return config().get<number>('vectorSize') || 512;
|
||||
}
|
||||
|
||||
////// toolchains
|
||||
|
||||
/** A local copy (toolchainPath, or the repo in development), if there is one. */
|
||||
@@ -432,7 +437,7 @@ async function runScriptHeadless(target: Target, build: BuildOutcome, script: st
|
||||
cancel = token.onCancellationRequested(() => reject(new Error('cancelled')));
|
||||
});
|
||||
var run = async () => {
|
||||
await worker.call('start', target.platform, build.output, build.files, { paused: true });
|
||||
await worker.call('start', target.platform, build.output, build.files, { paused: true, vectorSize: vectorSize() });
|
||||
var mainPath = path.posix.basename(target.main.path);
|
||||
return worker.call<ScriptResult>('script', script, { ...buildProducts(build), mainPath, paths: build.paths });
|
||||
};
|
||||
@@ -708,7 +713,7 @@ async function startEmulator(target: Target, build: BuildOutcome, opts: { paused
|
||||
var worker = await getEmu(target.platform);
|
||||
var title = describeTarget(target);
|
||||
try {
|
||||
emuStatus = await worker.call<EmuStatus>('start', target.platform, build.output, build.files, opts);
|
||||
emuStatus = await worker.call<EmuStatus>('start', target.platform, build.output, build.files, { ...opts, vectorSize: vectorSize() });
|
||||
// (the worker's first frame comes after this reply, so the panel is up in time)
|
||||
if (emuStatus.screen !== false || panel) openPanel(title, target.platform);
|
||||
vscode.commands.executeCommand('setContext', '8bitworkshop.emuOpen', true);
|
||||
|
||||
+22
-5
@@ -39,6 +39,7 @@ import { disassembleF8 } from "./cpu/disasmF8";
|
||||
import { disassembleHuC6280 } from "./cpu/disasmHuC6280";
|
||||
import { CPU6809 } from "./cpu/6809";
|
||||
import * as emu from "./emu";
|
||||
import { VectorRaster, DEFAULT_VECTOR_SIZE } from "./vectorraster";
|
||||
|
||||
export interface VideoOutput {
|
||||
pixels: Uint32Array;
|
||||
@@ -98,7 +99,7 @@ export const DEFAULT_MAX_FRAMES = 1000;
|
||||
* Headless stand-ins for RasterVideo/VectorVideo/AnimationTimer. Platform
|
||||
* modules pick these up because Platform.start() reads them off the emu module.
|
||||
*/
|
||||
function installHeadlessVideo() {
|
||||
function installHeadlessVideo(vectorSize: number) {
|
||||
// Platform.start() builds its video/timer by reading these classes off the
|
||||
// emu module, so we swap in headless stand-ins for the duration of start()
|
||||
// and put the real ones back afterwards. Leaving the stubs installed would
|
||||
@@ -147,11 +148,24 @@ function installHeadlessVideo() {
|
||||
this.putImageData = function () { };
|
||||
this.style = {};
|
||||
};
|
||||
const VectorVideo: any = function () {
|
||||
// the first one is the screen, like RasterVideo; the platform draws in its own
|
||||
// (w x h) coordinates and the raster is scaled so its long side is `vectorSize`
|
||||
const VectorVideo: any = function (_el: any, w: number, h: number) {
|
||||
const raster = new VectorRaster(w, h, vectorSize);
|
||||
if (!pixels) {
|
||||
params = { width: raster.width, height: raster.height };
|
||||
pixels = raster.pixels;
|
||||
}
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
this.persistenceAlpha = raster.persistenceAlpha;
|
||||
this.create = function () { this.drawops = 0; };
|
||||
this.setKeyboardEvents = setKeyboardEvents;
|
||||
this.clear = function () { };
|
||||
this.drawLine = function () { this.drawops++; };
|
||||
this.clear = function () { raster.persistenceAlpha = this.persistenceAlpha; raster.clear(); };
|
||||
this.drawLine = function (x1: number, y1: number, x2: number, y2: number, intensity: number, color: number) {
|
||||
this.drawops++;
|
||||
raster.drawLine(x1, y1, x2, y2, intensity, color);
|
||||
};
|
||||
};
|
||||
const AnimationTimer: any = function (fps: number) {
|
||||
if (fps > 0) frameRate = fps;
|
||||
@@ -188,6 +202,9 @@ export class EmuCore {
|
||||
// frames run on a platform without a timeline
|
||||
private untimedFrames = 0;
|
||||
|
||||
/** pixels along the longer side of a vector platform's screen (set before start()) */
|
||||
vectorSize = DEFAULT_VECTOR_SIZE;
|
||||
|
||||
constructor(readonly id: string, readonly platform: Platform) {
|
||||
}
|
||||
|
||||
@@ -254,7 +271,7 @@ export class EmuCore {
|
||||
async start() {
|
||||
// start() is where platforms construct their video and timer, so install
|
||||
// the headless stand-ins just for that call, then restore the real classes.
|
||||
const headless = installHeadlessVideo();
|
||||
const headless = installHeadlessVideo(this.vectorSize);
|
||||
this.video = headless;
|
||||
try {
|
||||
await this.platform.start();
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Software rasterizer for vector platforms, for hosts with no canvas (the CLI
|
||||
// and the VS Code extension). It mirrors VectorVideo in emu.ts: clear() fades
|
||||
// the last frame, drawLine() adds a line on top.
|
||||
|
||||
/** the same 8-color palette as VectorVideo.COLORS, as [r, g, b] */
|
||||
const COLORS: number[][] = [
|
||||
[0x11, 0x11, 0x11], [0x11, 0x11, 0xff], [0x11, 0xff, 0x11], [0x11, 0xff, 0xff],
|
||||
[0xff, 0x11, 0x11], [0xff, 0x11, 0xff], [0xff, 0xff, 0x11], [0xff, 0xff, 0xff],
|
||||
];
|
||||
|
||||
export const DEFAULT_VECTOR_SIZE = 512;
|
||||
|
||||
export class VectorRaster {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
/** RGBA pixels, in the byte order of a little-endian Uint32 (0xAABBGGRR) */
|
||||
readonly pixels: Uint32Array;
|
||||
private bytes: Uint8Array;
|
||||
private sx: number;
|
||||
private sy: number;
|
||||
persistenceAlpha = 0.5;
|
||||
gamma = 0.8;
|
||||
|
||||
/**
|
||||
* @param vw,vh the coordinate space the platform draws in
|
||||
* @param size pixels along the longer side of the output
|
||||
*/
|
||||
constructor(vw: number, vh: number, size: number) {
|
||||
var scale = size / Math.max(vw, vh);
|
||||
this.width = Math.max(1, Math.round(vw * scale));
|
||||
this.height = Math.max(1, Math.round(vh * scale));
|
||||
this.sx = this.width / vw;
|
||||
this.sy = this.height / vh;
|
||||
var buffer = new ArrayBuffer(this.width * this.height * 4);
|
||||
this.pixels = new Uint32Array(buffer);
|
||||
this.bytes = new Uint8Array(buffer);
|
||||
this.clear(true);
|
||||
}
|
||||
|
||||
/** Fade the last frame toward black (all the way, if `hard`). */
|
||||
clear(hard = false) {
|
||||
var keep = hard ? 0 : 1 - this.persistenceAlpha;
|
||||
var b = this.bytes;
|
||||
for (var i = 0; i < b.length; i += 4) {
|
||||
b[i] *= keep; b[i + 1] *= keep; b[i + 2] *= keep;
|
||||
b[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
/** Add a line (in the platform's coordinates, y up) with antialiasing. */
|
||||
drawLine(x1: number, y1: number, x2: number, y2: number, intensity: number, color: number) {
|
||||
if (!(intensity > 0)) return;
|
||||
var alpha = Math.pow(Math.min(intensity, 255) / 255, this.gamma);
|
||||
var rgb = COLORS[color & 7];
|
||||
var px1 = x1 * this.sx, py1 = this.height - y1 * this.sy;
|
||||
var px2 = x2 * this.sx, py2 = this.height - y2 * this.sy;
|
||||
var dx = px2 - px1, dy = py2 - py1;
|
||||
if (dx == 0 && dy == 0) {
|
||||
this.splat(px1, py1, rgb, alpha);
|
||||
return;
|
||||
}
|
||||
// one point per pixel along the longer axis
|
||||
var steps = Math.ceil(Math.max(Math.abs(dx), Math.abs(dy)));
|
||||
for (var i = 0; i <= steps; i++) {
|
||||
var t = i / steps;
|
||||
this.splat(px1 + dx * t, py1 + dy * t, rgb, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
/** Add one antialiased point: its weight is shared among the 4 pixels around it. */
|
||||
private splat(x: number, y: number, rgb: number[], w: number) {
|
||||
var x0 = Math.floor(x - 0.5), y0 = Math.floor(y - 0.5);
|
||||
var fx = x - 0.5 - x0, fy = y - 0.5 - y0;
|
||||
this.add(x0, y0, rgb, w * (1 - fx) * (1 - fy));
|
||||
this.add(x0 + 1, y0, rgb, w * fx * (1 - fy));
|
||||
this.add(x0, y0 + 1, rgb, w * (1 - fx) * fy);
|
||||
this.add(x0 + 1, y0 + 1, rgb, w * fx * fy);
|
||||
}
|
||||
|
||||
private add(x: number, y: number, rgb: number[], w: number) {
|
||||
if (x < 0 || y < 0 || x >= this.width || y >= this.height || w <= 0) return;
|
||||
var i = (y * this.width + x) * 4;
|
||||
var b = this.bytes;
|
||||
b[i] = Math.min(255, b[i] + rgb[0] * w);
|
||||
b[i + 1] = Math.min(255, b[i + 1] + rgb[1] * w);
|
||||
b[i + 2] = Math.min(255, b[i + 2] + rgb[2] * w);
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,8 @@ function looksLikeROM(file: string): boolean {
|
||||
|
||||
async function openTarget(args: Args, platformId: string): Promise<EmuTarget> {
|
||||
const target = await loadPlatform(platformId);
|
||||
const vectorSize = str(args, 'vector-size');
|
||||
if (vectorSize) target.vectorSize = parseInt(vectorSize);
|
||||
await target.start();
|
||||
const bios = str(args, 'bios');
|
||||
if (bios && !target.loadBIOS(new Uint8Array(fs.readFileSync(bios)))) {
|
||||
@@ -590,6 +592,7 @@ function usage(error?: string): never {
|
||||
'--png <file>': 'write a screenshot of the last frame',
|
||||
'--symbols <file>': 'load a .lbl/.sym file for symbolic addresses',
|
||||
'--bios <file>': 'load a BIOS image',
|
||||
'--vector-size <px>': 'long side of a vector platform\'s screen (default 512)',
|
||||
'--info': 'dump debug info and disassembly when done',
|
||||
'--no-warnings': 'don\'t print compiler warnings',
|
||||
'--memdump <a,b>': 'hexdump a hex address range',
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
var assert = require('assert');
|
||||
var { VectorRaster } = require('../../gen/common/vectorraster.js');
|
||||
var { loadPlatform } = require('../../gen/tools/emutarget.js');
|
||||
|
||||
function lit(r) {
|
||||
var n = 0;
|
||||
for (var p of r.pixels) if (p & 0xffffff) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
// brightness of one channel (shift 0, 8, 16) summed down a pixel column
|
||||
function column(r, x, shift) {
|
||||
var n = 0;
|
||||
for (var y = 0; y < r.height; y++) n += (r.pixels[y * r.width + x] >> shift) & 0xff;
|
||||
return n;
|
||||
}
|
||||
|
||||
describe('VectorRaster', function () {
|
||||
it('scales the platform space so the long side is the size', function () {
|
||||
var r = new VectorRaster(900, 1100, 550);
|
||||
assert.equal(r.width, 450);
|
||||
assert.equal(r.height, 550);
|
||||
});
|
||||
it('draws a bright horizontal line, y up', function () {
|
||||
var r = new VectorRaster(1024, 1024, 512);
|
||||
r.drawLine(0, 768, 1023, 768, 255, 7); // 3/4 up is 1/4 down: row 128
|
||||
assert(Math.abs(column(r, 100, 0) - 255) < 4);
|
||||
assert.equal(r.pixels[128 * 512 + 100] & 0xffffff, r.pixels[127 * 512 + 100] & 0xffffff);
|
||||
assert.equal(r.pixels[400 * 512 + 100] & 0xffffff, 0);
|
||||
assert.equal(r.pixels[10 * 512 + 100] & 0xffffff, 0);
|
||||
});
|
||||
it('draws a point and ignores zero intensity', function () {
|
||||
var r = new VectorRaster(1024, 1024, 512);
|
||||
r.drawLine(100, 100, 100, 100, 0, 7);
|
||||
assert.equal(lit(r), 0);
|
||||
r.drawLine(100, 100, 100, 100, 255, 7);
|
||||
assert(lit(r) > 0);
|
||||
});
|
||||
it('fades by persistence on clear and wipes on a hard clear', function () {
|
||||
var r = new VectorRaster(256, 256, 256);
|
||||
r.drawLine(0, 128, 255, 128, 255, 7);
|
||||
var before = column(r, 100, 0);
|
||||
r.clear();
|
||||
var after = column(r, 100, 0);
|
||||
assert(Math.abs(after - before / 2) < 4);
|
||||
r.clear(true);
|
||||
assert.equal(lit(r), 0);
|
||||
});
|
||||
it('uses the palette', function () {
|
||||
var r = new VectorRaster(256, 256, 256);
|
||||
r.drawLine(0, 128, 255, 128, 255, 2); // green
|
||||
assert(column(r, 100, 8) > 200);
|
||||
assert(column(r, 100, 0) < 50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('headless vector platform', function () {
|
||||
this.timeout(60000);
|
||||
it('has a screen of the configured size', async function () {
|
||||
var target = await loadPlatform('vector-ataricolor');
|
||||
target.vectorSize = 256;
|
||||
await target.start();
|
||||
var video = target.getVideo();
|
||||
assert.equal(video.width, 256);
|
||||
assert.equal(video.height, 256);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user