mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
vscode: paddle support
This commit is contained in:
1 parent
11461c811c
commit
96d2db2896
10 files changed
+98
-1
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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); },
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in new issue
Block a user