vscode: paddle support

This commit is contained in:
Steven Hugg committed 2026-10-03 17:16:10 +02:00
1 parent 11461c811c
commit 96d2db2896
10 files changed
+98 -1

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md).
- 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.
- Step Over for C and assembly sources, a Breakpoints pane, and more breakpoint conditions.
- `8bws run` script `paddle X Y [BUTTONS]` moves the paddle on platforms and Verilog designs that have one.
- Debug adapter: Verilog designs get a Signals scope (nested by module), and the `8bws` run script a `signals` command.
- Verilator warnings (width mismatches, etc.) no longer fail the build and show as warnings; a Verilator error no longer also reports "tool called exit(1)".
- Call Stack view renamed Call Graph, with clocks and more detail per line.
+2
View File
@@ -5,6 +5,8 @@
- The Verilog and Verilog (VGA) platforms now appear in New Project and Open
Example.
- Verilog syntax highlighting.
- 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.
- Debugging a Verilog program shows its signals in Variables, nested by module,
under a stack frame named for the frame and clock cycle.
+23
View File
@@ -9,6 +9,8 @@ export const VIEW_TYPE = '8bitworkshop.emulator';
export interface PanelEvents {
onKey(key: number, code: number, flags: number): void;
/** the mouse over the screen, 0..255 each way, and the buttons down */
onPaddle(x: number, y: number, buttons: boolean[]): void;
/** the user showed or hid the controls bar */
onControlsVisible(visible: boolean): void;
/** the user dragged the timeline to a recorded frame */
@@ -29,6 +31,7 @@ export class EmulatorPanel {
this.panel.webview.html = getHtml(controlsVisible);
this.panel.webview.onDidReceiveMessage(msg => {
if (msg.type === 'key') this.events.onKey(msg.key, msg.code, msg.flags);
else if (msg.type === 'paddle') this.events.onPaddle(msg.x, msg.y, msg.buttons);
else if (msg.type === 'frameDone') this.frameInFlight = false;
else if (msg.type === 'controlsVisible') this.events.onControlsVisible(!!msg.visible);
else if (msg.type === 'seek') this.events.onSeek?.(msg.frame);
@@ -147,6 +150,25 @@ function getHtml(controlsVisible: boolean) {
canvas.addEventListener('keyup', e => {
vscode.postMessage({ type: 'key', key: e.which, code: charCode(e), flags: KeyUp | modFlags(e) });
});
// the mouse as a paddle, only for programs that read one (status.paddles)
let paddles = false;
const buttons = [false, false, false];
function sendPaddle(e) {
const r = canvas.getBoundingClientRect();
const clamp = v => Math.max(0, Math.min(255, Math.round(v)));
vscode.postMessage({ type: 'paddle', x: clamp((e.clientX - r.left) * 255 / r.width),
y: clamp((e.clientY - r.top) * 255 / r.height), buttons });
}
canvas.addEventListener('mousemove', e => { if (paddles) sendPaddle(e); });
canvas.addEventListener('mousedown', e => {
if (!paddles) return;
// same mapping as the web IDE (RasterVideo.setupMouseEvents)
buttons[0] = !e.shiftKey && !e.altKey;
buttons[1] = e.shiftKey && !e.altKey;
buttons[2] = e.altKey && !e.shiftKey;
sendPaddle(e);
});
canvas.addEventListener('mouseup', e => { buttons.fill(false); if (paddles) sendPaddle(e); });
document.addEventListener('mousedown', e => { if (!(e.target instanceof HTMLButtonElement)) { canvas.focus(); ensureAudio(); } });
window.addEventListener('focus', () => canvas.focus());
@@ -298,6 +320,7 @@ function getHtml(controlsVisible: boolean) {
seekDone();
} else if (msg.type === 'status') {
const s = msg.status;
paddles = !!(s && s.paddles);
if (s && JSON.stringify(s.controls) !== lastControls) {
lastControls = JSON.stringify(s.controls);
showControls(s.controls);
+6 -1
View File
@@ -54,6 +54,8 @@ export interface EmuStatus {
message?: string;
/** the platform's controls, hand-written or from its key map */
controls?: ControlHint[];
/** the program reads paddles, so the panel sends the mouse */
paddles?: boolean;
/** the platform's audio output rate, if it makes sound */
audio?: { sampleRate: number };
timeline?: TimelineInfo;
@@ -195,6 +197,9 @@ const rpc: Rpc = new Rpc(parentPort, {
// platforms without keyboard input ignore keys
}
},
paddle(x: number, y: number, buttons: boolean[]) {
target?.setPaddles(x, y, buttons);
},
stop() {
stop();
},
@@ -232,7 +237,7 @@ function status(): EmuStatus | null {
const audio = target.getAudioParams();
return {
state: running ? 'running' : 'paused', platform: target.id, frame: target.frameCount,
controls, audio: audio ? { sampleRate: audio.sampleRate } : undefined,
controls, paddles: target.acceptsPaddles(), audio: audio ? { sampleRate: audio.sampleRate } : undefined,
timeline: service?.timeline() ?? undefined,
};
}
+1
View File
@@ -628,6 +628,7 @@ async function startEmulator(target: Target, build: BuildOutcome, opts: { paused
if (!panel) {
panel = new EmulatorPanel({
onKey: (key, code, flags) => { emu?.call('key', key, code, flags); },
onPaddle: (x, y, buttons) => { emu?.call('paddle', x, y, buttons); },
onControlsVisible: visible => context.globalState.update('controlsVisible', visible),
// don't burn CPU on a hidden screen
onVisible: visible => { emu?.call('setVisible', visible); },
+15
View File
@@ -152,6 +152,10 @@ export interface Platform {
getDebugTree?(): {};
/** HDL platforms: the design's signals as a tree of values, nested by module (see debugtree.ts) */
getSignals?(): {};
/** can the program read paddles (or a mouse)? A host only sends them if so. */
acceptsPaddles?(): boolean;
/** x and y in 0..255, and the buttons held */
setPaddles?(x: number, y: number, buttons: boolean[]): void;
/** HDL platforms: clock cycles since power-up */
getClockCount?(): number | undefined;
@@ -914,6 +918,17 @@ export abstract class BaseMachinePlatform<T extends Machine> extends BaseDebugPl
loadBIOS: (title, data) => void; // only set if hasBIOS() is true
acceptsPaddles() {
return hasPaddleInput(this.machine);
}
setPaddles(x: number, y: number, buttons: boolean[]) {
// pollControls() passes these on to the machine at the next frame
this.video.paddle_x = x;
this.video.paddle_y = y;
buttons.forEach((b, i) => this.video.paddle_buttons[i] = b);
}
pollControls() {
this.poller && this.poller.poll();
if (hasPaddleInput(this.machine)) {
+10
View File
@@ -330,6 +330,16 @@ export class EmuCore {
return this.platform.getSignals ? this.platform.getSignals() : null;
}
/** Does the program read paddles? */
acceptsPaddles(): boolean {
return !!this.platform.acceptsPaddles?.();
}
/** Move the paddles (x, y in 0..255) and press buttons; ignored if the platform has none. */
setPaddles(x: number, y: number, buttons: boolean[]) {
if (this.acceptsPaddles()) this.platform.setPaddles(x, y, buttons);
}
/** Clock cycles since power-up, for platforms that count them (HDL). */
getClockCount(): number | undefined {
return this.platform.getClockCount ? this.platform.getClockCount() : undefined;
+11
View File
@@ -733,6 +733,17 @@ var VerilogPlatform = function(mainElement, options) {
}
}
// only if the design has paddle inputs (see setGenInputs)
acceptsPaddles() {
return !!top && (top.state.hpaddle != null || top.state.vpaddle != null);
}
setPaddles(x: number, y: number, buttons: boolean[]) {
video.paddle_x = x;
video.paddle_y = y;
buttons.forEach((b, i) => video.paddle_buttons[i] = b);
}
getClockCount() {
return top && top.cycles;
}
+9
View File
@@ -47,6 +47,7 @@ export const RUN_SCRIPT_HELP = [
' serial - print what the program sent to its serial port (devel-6502)',
' pc [N] - print PC + disassembly of N instructions',
' info - platform/machine debug info',
' paddle X Y [BUTTONS] - move the paddle to X,Y (0-255); BUTTONS is a list of 0/1, like 1 0 0',
' signals [PATH] - HDL signals under PATH (modules separated by dots; default the top)',
' reset - reset the emulator',
' echo TEXT - print message',
@@ -517,6 +518,13 @@ export class RunScript {
}
}
cmdPaddle(tokens: string[]) {
if (!this.target.acceptsPaddles()) throw new Error(`'${this.target.id}' has no paddles`);
const [x, y] = [tokens[1], tokens[2]].map(t => parseInt(t));
if (isNaN(x) || isNaN(y)) throw new Error('usage: paddle X Y [BUTTONS]');
this.target.setPaddles(x, y, tokens.slice(3).map(t => t === '1'));
}
cmdSignals(tokens: string[]) {
this.target.settle();
const root = this.target.getSignals();
@@ -558,6 +566,7 @@ const COMMANDS: { [name: string]: Command } = {
'pc': RunScript.prototype.cmdPC,
'info': RunScript.prototype.cmdInfo,
'signals': RunScript.prototype.cmdSignals,
'paddle': RunScript.prototype.cmdPaddle,
'reset': RunScript.prototype.cmdReset,
'back': RunScript.prototype.cmdBack,
'seek': RunScript.prototype.cmdSeek,
+20
View File
@@ -252,6 +252,26 @@ describe('8bws CLI', function () {
});
});
describe('run --platform verilog: signals and paddles', function () {
it('should list signals with the clock count, and descend into modules', function () {
var out = cli('run', 'presets/verilog/ball_paddle.v', '-e', 'run 1; signals; signals ball_paddle_top');
assert.ok(/\(clock [1-9]\d*\)/.test(out), out);
assert.ok(/^ball_paddle_top\/ /m.test(out), out);
assert.ok(/^lives 3 /m.test(out), out);
assert.ok(!/__V/.test(out), 'no Verilator internals');
});
it('should move the paddle only if the design has paddle inputs', function () {
var shot = (x) => {
var png = path.join(fs.mkdtempSync(path.join(os.tmpdir(), '8bws-paddle-')), 'shot.png');
cli('run', 'presets/verilog/ball_paddle.v', '-e', `paddle ${x} 128; run 4`, '--png', png);
return fs.readFileSync(png);
};
assert.ok(!shot(40).equals(shot(200)), 'the screen follows the paddle');
var e = cliFails('run', 'presets/verilog/hvsync_generator.v', '-e', 'paddle 1 1');
assert.ok(/has no paddles/.test(e.stdout + e.stderr), e.stdout + e.stderr);
});
});
describe('run: emulator control', function () {
// these all reach through the Platform to its Machine (common/devices.ts)
it('should reject an unknown platform', function () {