diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e1e16e9..080b8f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md index 8bf764fe..ed9744b2 100644 --- a/extension/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -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. diff --git a/extension/src/emulatorpanel.ts b/extension/src/emulatorpanel.ts index 02a64921..58716f38 100644 --- a/extension/src/emulatorpanel.ts +++ b/extension/src/emulatorpanel.ts @@ -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); diff --git a/extension/src/emuworker.ts b/extension/src/emuworker.ts index b19f3060..d07d2d9e 100644 --- a/extension/src/emuworker.ts +++ b/extension/src/emuworker.ts @@ -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, }; } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 7a5ec537..c1c38fb3 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -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); }, diff --git a/src/common/baseplatform.ts b/src/common/baseplatform.ts index 77712210..7620ea92 100644 --- a/src/common/baseplatform.ts +++ b/src/common/baseplatform.ts @@ -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 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)) { diff --git a/src/common/emucore.ts b/src/common/emucore.ts index fa142158..d7aebd3d 100644 --- a/src/common/emucore.ts +++ b/src/common/emucore.ts @@ -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; diff --git a/src/platform/verilog.ts b/src/platform/verilog.ts index 390ac17a..3301525e 100644 --- a/src/platform/verilog.ts +++ b/src/platform/verilog.ts @@ -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; } diff --git a/src/tools/runscript.ts b/src/tools/runscript.ts index 496d8706..19927c9a 100644 --- a/src/tools/runscript.ts +++ b/src/tools/runscript.ts @@ -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, diff --git a/test/cli/testcli.js b/test/cli/testcli.js index 833166a8..2271fa63 100644 --- a/test/cli/testcli.js +++ b/test/cli/testcli.js @@ -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 () {