@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/host/vice/session.ts
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
// session starts headless and can move the machine into a VICE with a window
|
|
4
4
|
// and back; breakpoints, watchpoints and the rest go along.
|
|
5
5
|
|
|
6
|
-
import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { isOneOf, WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
+
import { DISK_IMAGE_TYPES, type DiskFile, type DiskImageType } from "../../protocol/tools.ts";
|
|
8
|
+
import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type TraceHit, type TraceKind, type TraceResult, type PointStopReason, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
|
|
9
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
10
|
import { tmpdir } from "node:os";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
12
13
|
|
|
13
|
-
import { formatC64Address } from "../../c64.ts";
|
|
14
|
+
import { formatC64Address, instructionLength } from "../../c64.ts";
|
|
15
|
+
import { encodePng, type IndexedFrame } from "../../graphics.ts";
|
|
16
|
+
import { instructionText, operandAddress } from "../../opcodes.ts";
|
|
14
17
|
import type { ProcessSupervisor } from "../../native/processes.ts";
|
|
15
18
|
import { trace } from "../../trace.ts";
|
|
16
19
|
import type { ViceSessionFactory, ViceSessionHandle } from "../server.ts";
|
|
@@ -26,7 +29,7 @@ import {
|
|
|
26
29
|
} from "./adapter.ts";
|
|
27
30
|
import { decodeProgramCounter, MonitorConnectionError, MonitorError, MonitorTimeoutError, ResponseType, type MonitorResponse } from "./binary-monitor.ts";
|
|
28
31
|
import { decodeCia, decodeSid, decodeSprite, decodeVicii } from "./chips.ts";
|
|
29
|
-
import { compareFrames, differenceImage
|
|
32
|
+
import { compareFrames, differenceImage } from "./screen.ts";
|
|
30
33
|
import { launchVice, startErrorLines, type ViceMode, type ViceProcess } from "./process.ts";
|
|
31
34
|
import { TextMonitorError, TextMonitorTimeoutError } from "./text-monitor.ts";
|
|
32
35
|
|
|
@@ -74,6 +77,8 @@ const WINDOW_CLOSED =
|
|
|
74
77
|
"Breakpoints and watchpoints are kept. Check the machine state before you continue.";
|
|
75
78
|
|
|
76
79
|
/** Emulator state that a move to another VICE cannot carry. */
|
|
80
|
+
/** The first bytes of every VICE snapshot file. */
|
|
81
|
+
const VSF_MAGIC = Buffer.from("VICE Snapshot File\x1a", "latin1");
|
|
77
82
|
const NOT_CARRIED = ["cpu history", "memory map", "profile", "keyboard input not yet typed"];
|
|
78
83
|
|
|
79
84
|
/** Starts another VICE for this session; `log` also gets that launch's lines. */
|
|
@@ -119,6 +124,26 @@ export interface SessionOptions {
|
|
|
119
124
|
launch?: ViceLauncher;
|
|
120
125
|
/** Makes the host remove a path if it dies; returns the release. */
|
|
121
126
|
ownPath?: (path: string) => () => void;
|
|
127
|
+
/** The known flaws of this VICE version (D56); keyboard and joystick results name them. */
|
|
128
|
+
flaws?: readonly string[];
|
|
129
|
+
/** Lists the entries of a disk image, so an autostart index is checked first (R2-08). Without it, VICE gets the index unchecked. */
|
|
130
|
+
directory?: DiskDirectory;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type DiskDirectory = (type: DiskImageType, image: Buffer) => Promise<DiskFile[]>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Where VICE keeps a staged file: an autostarted disk stays in drive 8, a tape
|
|
137
|
+
* in the datasette and a cartridge in the port, until the same place gets
|
|
138
|
+
* another file. Only a program is read once.
|
|
139
|
+
*/
|
|
140
|
+
type StagedKind = "program" | "disk" | "tape" | "cartridge";
|
|
141
|
+
|
|
142
|
+
function stagedKind(type: string): StagedKind {
|
|
143
|
+
if (type === "crt") return "cartridge";
|
|
144
|
+
if (type === "tap" || type === "t64") return "tape";
|
|
145
|
+
if (type === "prg" || type === "p00") return "program";
|
|
146
|
+
return "disk";
|
|
122
147
|
}
|
|
123
148
|
|
|
124
149
|
export class ViceSession implements ViceSessionHandle {
|
|
@@ -128,9 +153,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
128
153
|
#mode: ViceMode = "headless";
|
|
129
154
|
readonly #videoStandard: VideoStandard;
|
|
130
155
|
readonly #log: (line: string) => void;
|
|
156
|
+
/** The known VICE flaws that keyboard and joystick input meet, as one text, or undefined. */
|
|
157
|
+
readonly #inputFlaw: string | undefined;
|
|
131
158
|
readonly #untilReturnLimitMs: number;
|
|
132
159
|
readonly #stepLimitMs: number;
|
|
133
160
|
readonly #launch: ViceLauncher | undefined;
|
|
161
|
+
readonly #directory: DiskDirectory | undefined;
|
|
134
162
|
/** Session files that outlive one VICE: snapshots, staged media, handovers. */
|
|
135
163
|
readonly #workDir: string;
|
|
136
164
|
readonly #releaseWorkDir: () => void;
|
|
@@ -157,6 +185,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
157
185
|
#driveStop = false;
|
|
158
186
|
/** Counts stops caused by checkpoints or a CPU JAM rather than by a monitor command. */
|
|
159
187
|
#haltCount = 0;
|
|
188
|
+
/** What stopped the machine last, when a user breakpoint or watchpoint or a JAM did; cleared when it runs again. */
|
|
189
|
+
#lastStop: { stopReason: PointStopReason; pointId?: number } | undefined;
|
|
160
190
|
#queue: Promise<unknown> = Promise.resolve();
|
|
161
191
|
#closed = false;
|
|
162
192
|
#lost = false;
|
|
@@ -173,8 +203,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
173
203
|
#timingStart = 0n;
|
|
174
204
|
/** Counts staged media files, for unique names. */
|
|
175
205
|
#staged = 0;
|
|
206
|
+
/** True after an autostart of a cartridge, until an autostart of another file detaches it. */
|
|
207
|
+
#cartridge = false;
|
|
176
208
|
/** The last staged file of each kind that VICE took; VICE can still read it. */
|
|
177
|
-
readonly #stagedFiles = new Map<
|
|
209
|
+
readonly #stagedFiles = new Map<StagedKind, string>();
|
|
178
210
|
/** Session-local ids: one counter for breakpoints and watchpoints, from 1. */
|
|
179
211
|
#nextPointId = 1;
|
|
180
212
|
/**
|
|
@@ -196,6 +228,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
196
228
|
this.#untilReturnLimitMs = options.untilReturnLimitMs ?? UNTIL_RETURN_LIMIT_MS;
|
|
197
229
|
this.#stepLimitMs = options.stepLimitMs ?? RUNNING_COMMAND_TIMEOUT_MS;
|
|
198
230
|
this.#launch = options.launch;
|
|
231
|
+
this.#directory = options.directory;
|
|
232
|
+
this.#inputFlaw = options.flaws === undefined || options.flaws.length === 0 ? undefined : options.flaws.join(" ");
|
|
199
233
|
this.#workDir = mkdtempSync(join(tmpdir(), "c64-re-tools-session-"));
|
|
200
234
|
this.#releaseWorkDir = options.ownPath?.(this.#workDir) ?? (() => {});
|
|
201
235
|
this.#watch(vice);
|
|
@@ -268,7 +302,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
268
302
|
session.#timingStart = await session.#adapter.clock();
|
|
269
303
|
// c64_profile reports what ran since the session started.
|
|
270
304
|
await session.#adapter.startProfiler();
|
|
271
|
-
//
|
|
305
|
+
// In VICE 3.10 and older the control-port lines start all pressed; release them before the C64 reads them.
|
|
272
306
|
for (const joystick of session.#joysticks.values()) await session.#adapter.setJoystick(joystick);
|
|
273
307
|
}),
|
|
274
308
|
);
|
|
@@ -283,9 +317,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
283
317
|
op: O,
|
|
284
318
|
params: ViceOperations[O]["params"],
|
|
285
319
|
attachments: Buffer[] = [],
|
|
320
|
+
reply: Buffer[] = [],
|
|
286
321
|
): Promise<ViceOperations[O]["result"]> {
|
|
287
322
|
// A window request for the mode the session is in sends nothing to VICE, so a held VICE does not stop it.
|
|
288
|
-
return this.#enqueue(() => this.#run(op, params, attachments), { whileHeld: op === "window" }) as Promise<ViceOperations[O]["result"]>;
|
|
323
|
+
return this.#enqueue(() => this.#run(op, params, attachments, reply), { whileHeld: op === "window" }) as Promise<ViceOperations[O]["result"]>;
|
|
289
324
|
}
|
|
290
325
|
|
|
291
326
|
async close(): Promise<void> {
|
|
@@ -303,10 +338,15 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
303
338
|
return this.#adapter;
|
|
304
339
|
}
|
|
305
340
|
|
|
306
|
-
async #run(op: ViceOperation, params: unknown, attachments: Buffer[]): Promise<unknown> {
|
|
341
|
+
async #run(op: ViceOperation, params: unknown, attachments: Buffer[], reply: Buffer[]): Promise<unknown> {
|
|
307
342
|
switch (op) {
|
|
308
343
|
case "status":
|
|
309
344
|
return this.#status();
|
|
345
|
+
case "ioRead": {
|
|
346
|
+
const read = params as ViceOperations["ioRead"]["params"];
|
|
347
|
+
const bytes = await this.#observe(() => this.#machine.readIo(read.address, read.size));
|
|
348
|
+
return { address: read.address, data: Buffer.from(bytes).toString("hex") };
|
|
349
|
+
}
|
|
310
350
|
case "memoryRead": {
|
|
311
351
|
const read = params as ViceOperations["memoryRead"]["params"];
|
|
312
352
|
const data = await this.#observe(() => this.#machine.readMemory(read));
|
|
@@ -345,10 +385,30 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
345
385
|
return this.#programLoad(params as ViceOperations["programLoad"]["params"], onlyAttachment(attachments));
|
|
346
386
|
case "autostart": {
|
|
347
387
|
const { type, index, run } = params as ViceOperations["autostart"]["params"];
|
|
348
|
-
const
|
|
388
|
+
const bytes = onlyAttachment(attachments);
|
|
389
|
+
if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
|
|
390
|
+
const file = this.#stage(bytes, type);
|
|
391
|
+
// VICE keeps a cartridge through every reset and autostart, as a real C64 does.
|
|
392
|
+
if (type !== "crt" && this.#cartridge) {
|
|
393
|
+
await this.#observe(() => this.#machine.detachCartridge());
|
|
394
|
+
this.#cartridge = false;
|
|
395
|
+
this.#replaceStaged("cartridge", undefined);
|
|
396
|
+
}
|
|
397
|
+
// Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types (R3-05).
|
|
398
|
+
const centred: Array<1 | 2> = [];
|
|
399
|
+
for (const [port, joystick] of this.#joysticks) {
|
|
400
|
+
if (joystick.direction === "center" && !joystick.fire) continue;
|
|
401
|
+
const centre: JoystickState = { port, direction: "center", fire: false };
|
|
402
|
+
await this.#observe(() => this.#machine.setJoystick(centre));
|
|
403
|
+
this.#joysticks.set(port, centre);
|
|
404
|
+
centred.push(port);
|
|
405
|
+
}
|
|
349
406
|
await this.#runningCommand(() => this.#machine.autostart(file, index, run));
|
|
350
|
-
|
|
351
|
-
|
|
407
|
+
if (type === "crt") this.#cartridge = true;
|
|
408
|
+
this.#replaceStaged(stagedKind(type), file);
|
|
409
|
+
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
410
|
+
this.#timingStart = 0n;
|
|
411
|
+
return { state: "running", ...(type === "tap" && this.#inputFlaw !== undefined ? { emulatorFlaw: this.#inputFlaw } : {}), ...(centred.length === 0 ? {} : { joysticksCentred: centred }) };
|
|
352
412
|
}
|
|
353
413
|
case "diskAttach": {
|
|
354
414
|
const { type } = params as ViceOperations["diskAttach"]["params"];
|
|
@@ -362,7 +422,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
362
422
|
case "keyboard": {
|
|
363
423
|
const bytes = Buffer.from((params as ViceOperations["keyboard"]["params"]).data, "hex");
|
|
364
424
|
await this.#observe(() => this.#machine.feedKeyboard(bytes));
|
|
365
|
-
return { queuedBytes: bytes.length };
|
|
425
|
+
return { queuedBytes: bytes.length, ...(this.#inputFlaw === undefined ? {} : { emulatorFlaw: this.#inputFlaw }) };
|
|
366
426
|
}
|
|
367
427
|
case "breakpoint":
|
|
368
428
|
return this.#breakpoint(params as BreakpointParams);
|
|
@@ -394,8 +454,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
394
454
|
}
|
|
395
455
|
case "snapshot":
|
|
396
456
|
return this.#snapshot(params as ViceOperations["snapshot"]["params"]);
|
|
457
|
+
case "snapshotFile":
|
|
458
|
+
return this.#snapshotFile(params as ViceOperations["snapshotFile"]["params"], attachments, reply);
|
|
397
459
|
case "cpuHistory":
|
|
398
460
|
return this.#cpuHistory(params as ViceOperations["cpuHistory"]["params"]);
|
|
461
|
+
case "trace":
|
|
462
|
+
return this.#trace(params as ViceOperations["trace"]["params"]);
|
|
399
463
|
case "backtrace": {
|
|
400
464
|
const { depth, space } = params as ViceOperations["backtrace"]["params"];
|
|
401
465
|
return this.#observe(async () => {
|
|
@@ -413,8 +477,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
413
477
|
return { cycles: (now - this.#timingStart).toString() };
|
|
414
478
|
}
|
|
415
479
|
case "profile": {
|
|
416
|
-
const
|
|
417
|
-
|
|
480
|
+
const request = params as ViceOperations["profile"]["params"];
|
|
481
|
+
if (request.action === "clear") {
|
|
482
|
+
await this.#observe(() => this.#machine.startProfiler());
|
|
483
|
+
return { cleared: true };
|
|
484
|
+
}
|
|
485
|
+
return { entries: await this.#observe(() => this.#machine.profile(request.limit)) };
|
|
418
486
|
}
|
|
419
487
|
case "memmap": {
|
|
420
488
|
const request = params as ViceOperations["memmap"]["params"];
|
|
@@ -422,7 +490,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
422
490
|
await this.#observe(() => this.#machine.clearMemmap());
|
|
423
491
|
return { cleared: true };
|
|
424
492
|
}
|
|
425
|
-
return
|
|
493
|
+
return await this.#observe(() => this.#machine.memmap(request.start, request.end, request.maxRanges));
|
|
426
494
|
}
|
|
427
495
|
case "vicii":
|
|
428
496
|
return this.#observe(() => this.#vicii());
|
|
@@ -442,7 +510,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
442
510
|
const joystick = params as JoystickState;
|
|
443
511
|
await this.#observe(() => this.#machine.setJoystick(joystick));
|
|
444
512
|
this.#joysticks.set(joystick.port, joystick);
|
|
445
|
-
return joystick;
|
|
513
|
+
return { ...joystick, ...(this.#inputFlaw === undefined ? {} : { emulatorFlaw: this.#inputFlaw }) };
|
|
446
514
|
}
|
|
447
515
|
case "warp": {
|
|
448
516
|
const { enabled } = params as ViceOperations["warp"]["params"];
|
|
@@ -458,8 +526,13 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
458
526
|
async #status(): Promise<MachineStatus> {
|
|
459
527
|
const warp = await this.#observe(() => this.#machine.warp());
|
|
460
528
|
// Read after #observe: it resumes a running machine, unless a breakpoint or a JAM stopped it meanwhile.
|
|
461
|
-
const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window" };
|
|
462
|
-
if (status.state === "stopped")
|
|
529
|
+
const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window", cartridge: this.#cartridge };
|
|
530
|
+
if (status.state === "stopped") {
|
|
531
|
+
status.pc = await this.#pc();
|
|
532
|
+
// VICE 3.9 sends no JAM event: the opcode at the pc tells.
|
|
533
|
+
const stop = this.#lastStop ?? ((await this.#jammedAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
|
|
534
|
+
if (stop !== undefined) Object.assign(status, stop);
|
|
535
|
+
}
|
|
463
536
|
return status;
|
|
464
537
|
}
|
|
465
538
|
|
|
@@ -485,8 +558,18 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
485
558
|
}
|
|
486
559
|
if (params.action === "advance-frames") {
|
|
487
560
|
if (params.count === undefined) throw new WireFailure("invalid-input", "advance-frames needs count, the number of frames");
|
|
488
|
-
const
|
|
489
|
-
|
|
561
|
+
const raster = RASTER[this.#videoStandard];
|
|
562
|
+
if (params.anchor !== undefined && (params.anchor.line >= raster.lines || params.anchor.cycle >= raster.cycles)) {
|
|
563
|
+
throw new WireFailure("invalid-input", `The anchor is outside the frame: a ${this.#videoStandard} frame has lines 0-${raster.lines - 1} and cycles 0-${raster.cycles - 1}.`);
|
|
564
|
+
}
|
|
565
|
+
const outcome = await this.#runFrames(params.count, undefined, params.anchor);
|
|
566
|
+
return {
|
|
567
|
+
state: "stopped",
|
|
568
|
+
pc: outcome.pc,
|
|
569
|
+
advancedFrames: outcome.frames,
|
|
570
|
+
stopReason: outcome.end === "target" ? "frames" : outcome.end,
|
|
571
|
+
...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }),
|
|
572
|
+
};
|
|
490
573
|
}
|
|
491
574
|
await this.#stop();
|
|
492
575
|
if (params.action === "until-return") {
|
|
@@ -516,6 +599,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
516
599
|
|
|
517
600
|
async #reset(params: { mode: "soft" | "hard"; run: boolean }): Promise<{ state: RunState; pc?: number }> {
|
|
518
601
|
await this.#stop();
|
|
602
|
+
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
603
|
+
this.#timingStart = 0n;
|
|
519
604
|
if (params.run) {
|
|
520
605
|
await this.#runningCommand(() => this.#machine.reset(params.mode));
|
|
521
606
|
return { state: "running" };
|
|
@@ -545,10 +630,11 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
545
630
|
* checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
|
|
546
631
|
* The machine is stopped afterwards.
|
|
547
632
|
*/
|
|
548
|
-
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }): Promise<{ end: RunEnd; frames: number; pc: number }> {
|
|
633
|
+
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8"; pointId?: number }> {
|
|
549
634
|
await this.#stop();
|
|
550
635
|
const lines = RASTER[this.#videoStandard].lines;
|
|
551
|
-
|
|
636
|
+
// A frame ends where the raster passes the anchor; without one, where this run starts.
|
|
637
|
+
const start = anchor ?? (await this.#machine.rasterPosition());
|
|
552
638
|
const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
|
|
553
639
|
const ending = rasterWindowExpression(start.line, start.cycle, lines);
|
|
554
640
|
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
@@ -574,24 +660,139 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
574
660
|
}
|
|
575
661
|
} else {
|
|
576
662
|
frames++;
|
|
577
|
-
if (frames >= limit) return { end: "frames", frames, pc: stop.pc };
|
|
663
|
+
if (frames >= limit) return { end: "frames", frames, pc: stop.pc, space: "c64" };
|
|
578
664
|
await this.#machine.setCondition(clock, arming);
|
|
579
665
|
}
|
|
580
666
|
armingPhase = !armingPhase;
|
|
581
667
|
continue;
|
|
582
668
|
}
|
|
583
669
|
if (end === "unknown") {
|
|
584
|
-
if (await this.#jammedAt(stop.pc)) return { end: "jam", frames, pc: stop.pc };
|
|
670
|
+
if (await this.#jammedAt(stop.pc)) return { end: "jam", frames, pc: stop.pc, space: "c64" };
|
|
585
671
|
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this run.");
|
|
586
672
|
}
|
|
587
|
-
// A drive stop is completed before the result names the computer's pc.
|
|
588
|
-
|
|
673
|
+
// A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
|
|
674
|
+
const driveStop = this.#driveStop;
|
|
675
|
+
const pointId = end === "target" ? undefined : this.#userPoint(stop.hits)?.pointId;
|
|
676
|
+
return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64", ...(pointId === undefined ? {} : { pointId }) };
|
|
589
677
|
}
|
|
590
678
|
} finally {
|
|
591
679
|
await this.#machine.deleteCheckpoint(clock);
|
|
592
680
|
}
|
|
593
681
|
}
|
|
594
682
|
|
|
683
|
+
/**
|
|
684
|
+
* A trace (D76): runs the machine for up to `frames` frames, counted as
|
|
685
|
+
* #runFrames counts them, with a stopping checkpoint for each point. At each
|
|
686
|
+
* hit the session records the instruction, its registers and the raster
|
|
687
|
+
* position, and runs on. It ends at the frame limit, at `maxHits` hits, or
|
|
688
|
+
* where a user point or a JAM stops the program. VICE also reports the hits
|
|
689
|
+
* of a checkpoint that does not stop, but without the PC, the registers or
|
|
690
|
+
* the raster position, so each point stops.
|
|
691
|
+
*/
|
|
692
|
+
async #trace(params: ViceOperations["trace"]["params"]): Promise<TraceResult> {
|
|
693
|
+
await this.#stop();
|
|
694
|
+
const lines = RASTER[this.#videoStandard].lines;
|
|
695
|
+
const start = await this.#machine.rasterPosition();
|
|
696
|
+
const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
|
|
697
|
+
const ending = rasterWindowExpression(start.line, start.cycle, lines);
|
|
698
|
+
const operation: Record<TraceKind, number> = { exec: 0x04, read: 0x01, write: 0x02 };
|
|
699
|
+
const points: number[] = [];
|
|
700
|
+
const hits: TraceHit[] = [];
|
|
701
|
+
let frames = 0;
|
|
702
|
+
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
703
|
+
try {
|
|
704
|
+
for (const point of params.points) points.push(await this.#machine.addCheckpoint({ start: point.start, end: point.end, operation: operation[point.kind], space: "c64" }));
|
|
705
|
+
await this.#machine.setCondition(clock, arming);
|
|
706
|
+
let armingPhase = true;
|
|
707
|
+
for (;;) {
|
|
708
|
+
const next = this.#nextStop(HALF_FRAME_LIMIT_MS);
|
|
709
|
+
await this.#resume();
|
|
710
|
+
const stop = await next;
|
|
711
|
+
if (stop === undefined) {
|
|
712
|
+
await this.#stop();
|
|
713
|
+
throw new WireFailure("operation-failed", "The emulator did not reach the next frame in time.");
|
|
714
|
+
}
|
|
715
|
+
const traced = points.flatMap((checkpoint, index) => (stop.hits.includes(checkpoint) ? [index] : []));
|
|
716
|
+
for (const index of traced) {
|
|
717
|
+
hits.push(await this.#traceHit(index, params.points[index]!.kind, frames));
|
|
718
|
+
if (hits.length >= params.maxHits) return { hits, frames, end: "hits", pc: stop.pc };
|
|
719
|
+
}
|
|
720
|
+
if (stop.hits.includes(clock)) {
|
|
721
|
+
if (armingPhase) await this.#machine.setCondition(clock, ending);
|
|
722
|
+
else {
|
|
723
|
+
frames++;
|
|
724
|
+
if (frames >= params.frames) return { hits, frames, end: "frames", pc: stop.pc };
|
|
725
|
+
await this.#machine.setCondition(clock, arming);
|
|
726
|
+
}
|
|
727
|
+
armingPhase = !armingPhase;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (traced.length > 0) continue;
|
|
731
|
+
const point = this.#userPoint(stop.hits);
|
|
732
|
+
if (point !== undefined) return { hits, frames, end: point.stopReason, pc: stop.pc };
|
|
733
|
+
if (await this.#jammedAt(stop.pc)) return { hits, frames, end: "jam", pc: stop.pc };
|
|
734
|
+
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this trace.");
|
|
735
|
+
}
|
|
736
|
+
} finally {
|
|
737
|
+
await this.#machine.deleteCheckpoint(clock);
|
|
738
|
+
for (const checkpoint of points) await this.#machine.deleteCheckpoint(checkpoint);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* One hit of a trace point. An exec point stops before its instruction: the
|
|
744
|
+
* registers and the raster position are the ones of now. A read or write
|
|
745
|
+
* point stops after the instruction that made the access: the last entry of
|
|
746
|
+
* the CPU history is that instruction, with its registers and its clock.
|
|
747
|
+
*/
|
|
748
|
+
async #traceHit(point: number, kind: TraceKind, frame: number): Promise<TraceHit> {
|
|
749
|
+
const position = await this.#machine.rasterPosition();
|
|
750
|
+
const byteAt = async (address: number) => Number.parseInt(await this.#machine.readMemory({ address, size: 1, space: "c64", view: "cpu" }), 16);
|
|
751
|
+
if (kind === "exec") {
|
|
752
|
+
const registers = await this.#machine.readRegisters("c64");
|
|
753
|
+
const bytes = [...Buffer.from(await this.#machine.readMemory({ address: registers.pc, size: 3, space: "c64", view: "cpu" }), "hex")];
|
|
754
|
+
const length = instructionLength(bytes[0]!);
|
|
755
|
+
return {
|
|
756
|
+
point,
|
|
757
|
+
pc: registers.pc,
|
|
758
|
+
bytes: Buffer.from(bytes.slice(0, length)).toString("hex"),
|
|
759
|
+
text: instructionText(bytes, registers.pc),
|
|
760
|
+
a: registers.a,
|
|
761
|
+
x: registers.x,
|
|
762
|
+
y: registers.y,
|
|
763
|
+
sp: registers.sp,
|
|
764
|
+
frame,
|
|
765
|
+
rasterLine: position.line,
|
|
766
|
+
rasterCycle: position.cycle,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const [entry] = await this.#machine.history(1, "c64");
|
|
770
|
+
if (entry === undefined) throw new WireFailure("operation-failed", "The emulator gave no CPU history at a trace hit.");
|
|
771
|
+
const now = await this.#machine.clock();
|
|
772
|
+
const { lines, cycles } = RASTER[this.#videoStandard];
|
|
773
|
+
const span = BigInt(lines * cycles);
|
|
774
|
+
const at = Number((((BigInt(position.line * cycles + position.cycle) - (now - entry.clock)) % span) + span) % span);
|
|
775
|
+
const bytes = [...Buffer.from(entry.bytes, "hex")];
|
|
776
|
+
// A zero-page pointer is read now: the instruction did not change it.
|
|
777
|
+
const pointer = new Map<number, number>();
|
|
778
|
+
for (const address of [bytes[1] ?? 0, ((bytes[1] ?? 0) + 1) & 0xff, ((bytes[1] ?? 0) + entry.x) & 0xff, ((bytes[1] ?? 0) + entry.x + 1) & 0xff]) pointer.set(address, await byteAt(address));
|
|
779
|
+
const address = operandAddress(bytes, entry.x, entry.y, (zp) => pointer.get(zp) ?? 0);
|
|
780
|
+
return {
|
|
781
|
+
point,
|
|
782
|
+
pc: entry.address,
|
|
783
|
+
bytes: entry.bytes,
|
|
784
|
+
text: instructionText(bytes, entry.address),
|
|
785
|
+
a: entry.a,
|
|
786
|
+
x: entry.x,
|
|
787
|
+
y: entry.y,
|
|
788
|
+
sp: entry.sp,
|
|
789
|
+
frame,
|
|
790
|
+
rasterLine: Math.floor(at / cycles),
|
|
791
|
+
rasterCycle: at % cycles,
|
|
792
|
+
...(address === undefined ? {} : { address, value: await byteAt(address) }),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
595
796
|
/**
|
|
596
797
|
* Completes a stop that a drive 8 checkpoint caused: the computer runs to
|
|
597
798
|
* its next instruction boundary, where a computer checkpoint stops it, so
|
|
@@ -602,6 +803,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
602
803
|
async #completeDriveStop(): Promise<number | undefined> {
|
|
603
804
|
if (!this.#driveStop) return undefined;
|
|
604
805
|
this.#driveStop = false;
|
|
806
|
+
// The drive checkpoint stopped the machine; the stop at the boundary keeps that reason.
|
|
807
|
+
const reason = this.#lastStop;
|
|
605
808
|
const boundary = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
606
809
|
try {
|
|
607
810
|
for (let attempt = 0; attempt < DRIVE_STOP_TRIES; attempt++) {
|
|
@@ -612,6 +815,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
612
815
|
// The drive can hit its checkpoint again before the computer gets to the next instruction.
|
|
613
816
|
if (stop.hits.includes(boundary)) {
|
|
614
817
|
this.#driveStop = false;
|
|
818
|
+
this.#lastStop = reason;
|
|
615
819
|
trace().event("vice.drive-stop", { pc: stop.pc, resumes: attempt + 1 });
|
|
616
820
|
return stop.pc;
|
|
617
821
|
}
|
|
@@ -626,14 +830,20 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
626
830
|
/** Names what caused a stop during a frame-clocked run. */
|
|
627
831
|
#stopCause(stop: Stop, clock: number, target: number | undefined): Exclude<RunEnd, "frames" | "jam"> | "clock" | "unknown" {
|
|
628
832
|
if (target !== undefined && stop.hits.includes(target)) return "target";
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
if (isPoint(this.#breakpoints)) return "breakpoint";
|
|
632
|
-
if (isPoint(this.#watchpoints)) return "watchpoint";
|
|
833
|
+
const point = this.#userPoint(stop.hits);
|
|
834
|
+
if (point !== undefined) return point.stopReason;
|
|
633
835
|
if (stop.hits.includes(clock)) return "clock";
|
|
634
836
|
return "unknown";
|
|
635
837
|
}
|
|
636
838
|
|
|
839
|
+
/** The user breakpoint, else the user watchpoint, among the hit VICE checkpoints. */
|
|
840
|
+
#userPoint(hits: number[]): { stopReason: "breakpoint" | "watchpoint"; pointId: number } | undefined {
|
|
841
|
+
for (const [stopReason, registry] of [["breakpoint", this.#breakpoints], ["watchpoint", this.#watchpoints]] as const) {
|
|
842
|
+
for (const [pointId, point] of registry) if (hits.includes(point.checkpoint)) return { stopReason, pointId };
|
|
843
|
+
}
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
|
|
637
847
|
async #jammedAt(pc: number): Promise<boolean> {
|
|
638
848
|
const opcode = await this.#machine.readMemory({ address: pc, size: 1, space: "c64", view: "cpu" });
|
|
639
849
|
return JAM_OPCODES.has(Number.parseInt(opcode, 16));
|
|
@@ -677,9 +887,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
677
887
|
if (expression !== undefined) await this.#machine.setCondition(checkpoint, expression);
|
|
678
888
|
const outcome = await this.#runFrames(params.timeoutFrames, { checkpoint, deferred });
|
|
679
889
|
const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
|
|
680
|
-
// A stop event carries the computer's PC; a drive
|
|
681
|
-
const pc =
|
|
682
|
-
return { reached: stopReason === "target", stopReason, state: "stopped", pc };
|
|
890
|
+
// A stop event carries the computer's PC; a stop in the drive reports the drive's (F-31).
|
|
891
|
+
const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
|
|
892
|
+
return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space, ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }) };
|
|
683
893
|
} finally {
|
|
684
894
|
await this.#machine.deleteCheckpoint(checkpoint);
|
|
685
895
|
}
|
|
@@ -716,34 +926,44 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
716
926
|
async #changePoint<T extends { checkpoint: number; enabled: boolean }>(
|
|
717
927
|
registry: Map<number, T>,
|
|
718
928
|
kind: string,
|
|
719
|
-
params: { action: "
|
|
929
|
+
params: { action: "enable" | "disable"; id: number },
|
|
720
930
|
): Promise<T> {
|
|
721
|
-
const point = registry
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
931
|
+
const point = this.#point(registry, kind, params.id);
|
|
932
|
+
const enabled = params.action === "enable";
|
|
933
|
+
await this.#observe(() => this.#machine.toggleCheckpoint(point.checkpoint, enabled));
|
|
934
|
+
point.enabled = enabled;
|
|
935
|
+
return point;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async #removePoint(registry: Map<number, { checkpoint: number }>, kind: string, id: number): Promise<PointRemoved> {
|
|
939
|
+
const point = this.#point(registry, kind, id);
|
|
940
|
+
await this.#observe(() => this.#machine.deleteCheckpoint(point.checkpoint));
|
|
941
|
+
registry.delete(id);
|
|
942
|
+
return { removed: true, id };
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
#point<T>(registry: Map<number, T>, kind: string, id: number): T {
|
|
946
|
+
const point = registry.get(id);
|
|
947
|
+
if (point === undefined) throw new WireFailure("not-found", `There is no ${kind} with id ${id}.`);
|
|
731
948
|
return point;
|
|
732
949
|
}
|
|
733
950
|
|
|
734
|
-
async #breakpoint(params: BreakpointParams): Promise<Breakpoint | { breakpoints: Breakpoint[] }> {
|
|
951
|
+
async #breakpoint(params: BreakpointParams): Promise<Breakpoint | PointRemoved | { breakpoints: Breakpoint[] }> {
|
|
735
952
|
const publicView = ({ checkpoint: _checkpoint, expression: _expression, ...point }: Breakpoint & { checkpoint: number; expression?: string }): Breakpoint =>
|
|
736
953
|
point;
|
|
737
954
|
if (params.action === "list") return { breakpoints: [...this.#breakpoints.values()].map(publicView) };
|
|
955
|
+
if (params.action === "remove") return this.#removePoint(this.#breakpoints, "breakpoint", params.id);
|
|
738
956
|
if (params.action !== "add") return publicView(await this.#changePoint(this.#breakpoints, "breakpoint", params));
|
|
957
|
+
// A breakpoint over a range stops at the first instruction executed in it: exec, not a watch (D43).
|
|
739
958
|
const added = await this.#addPoint({
|
|
740
|
-
...pointRange({ address: params.address }),
|
|
959
|
+
...(params.size === undefined ? pointRange({ address: params.address }) : { start: params.address, end: params.address + params.size - 1, operation: 0x04 }),
|
|
741
960
|
space: params.space,
|
|
742
961
|
...(params.condition === undefined ? {} : { condition: params.condition }),
|
|
743
962
|
});
|
|
744
963
|
const point = {
|
|
745
964
|
id: this.#nextPointId++,
|
|
746
965
|
address: params.address,
|
|
966
|
+
...(params.size === undefined ? {} : { size: params.size }),
|
|
747
967
|
space: params.space,
|
|
748
968
|
enabled: true,
|
|
749
969
|
...(params.condition === undefined ? {} : { condition: params.condition }),
|
|
@@ -753,10 +973,11 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
753
973
|
return publicView(point);
|
|
754
974
|
}
|
|
755
975
|
|
|
756
|
-
async #watchpoint(params: WatchpointParams): Promise<Watchpoint | { watchpoints: Watchpoint[] }> {
|
|
976
|
+
async #watchpoint(params: WatchpointParams): Promise<Watchpoint | PointRemoved | { watchpoints: Watchpoint[] }> {
|
|
757
977
|
const publicView = ({ checkpoint: _checkpoint, expression: _expression, ...point }: Watchpoint & { checkpoint: number; expression?: string }): Watchpoint =>
|
|
758
978
|
point;
|
|
759
979
|
if (params.action === "list") return { watchpoints: [...this.#watchpoints.values()].map(publicView) };
|
|
980
|
+
if (params.action === "remove") return this.#removePoint(this.#watchpoints, "watchpoint", params.id);
|
|
760
981
|
if (params.action !== "add") return publicView(await this.#changePoint(this.#watchpoints, "watchpoint", params));
|
|
761
982
|
const added = await this.#addPoint({
|
|
762
983
|
...pointRange({ address: params.address, size: params.size, access: params.access }),
|
|
@@ -790,14 +1011,21 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
790
1011
|
}
|
|
791
1012
|
|
|
792
1013
|
/**
|
|
793
|
-
* Records the staged file that VICE took for `kind`,
|
|
794
|
-
* it replaces: VICE does not read that one again.
|
|
795
|
-
* take stays until the session closes, because a
|
|
1014
|
+
* Records the staged file that VICE took for `kind`, or that it no longer
|
|
1015
|
+
* has, and removes the file it replaces: VICE does not read that one again.
|
|
1016
|
+
* A file that VICE did not take stays until the session closes, because a
|
|
1017
|
+
* held VICE can still read it. So does a file that Windows does not let go.
|
|
796
1018
|
*/
|
|
797
|
-
#replaceStaged(kind:
|
|
1019
|
+
#replaceStaged(kind: StagedKind, file: string | undefined): void {
|
|
798
1020
|
const previous = this.#stagedFiles.get(kind);
|
|
799
|
-
this.#stagedFiles.
|
|
800
|
-
|
|
1021
|
+
if (file === undefined) this.#stagedFiles.delete(kind);
|
|
1022
|
+
else this.#stagedFiles.set(kind, file);
|
|
1023
|
+
if (previous === undefined || previous === file) return;
|
|
1024
|
+
try {
|
|
1025
|
+
rmSync(previous, { force: true });
|
|
1026
|
+
} catch {
|
|
1027
|
+
// The session directory goes when the session closes.
|
|
1028
|
+
}
|
|
801
1029
|
}
|
|
802
1030
|
|
|
803
1031
|
/** Loads a PRG without reset or start; finishes stopped. */
|
|
@@ -858,7 +1086,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
858
1086
|
async #cias(which: CiaSelection): Promise<CiaState[]> {
|
|
859
1087
|
const ids: Array<1 | 2> = which === "both" ? [1, 2] : [Number(which) as 1 | 2];
|
|
860
1088
|
const chips = [];
|
|
861
|
-
for (const id of ids)
|
|
1089
|
+
for (const id of ids) {
|
|
1090
|
+
const base = id === 1 ? 0xdc00 : 0xdd00;
|
|
1091
|
+
chips.push(decodeCia(id, await this.#machine.readIo(base, 0x10), await this.#machine.ciaHidden(base)));
|
|
1092
|
+
}
|
|
862
1093
|
return chips;
|
|
863
1094
|
}
|
|
864
1095
|
|
|
@@ -910,15 +1141,43 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
910
1141
|
this.#snapshots.delete(params.name);
|
|
911
1142
|
return { discarded: true };
|
|
912
1143
|
}
|
|
1144
|
+
return this.#restoreFrom(existing);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** Restores the machine from a .vsf in the session directory; the CPU stays stopped. */
|
|
1148
|
+
async #restoreFrom(file: string): Promise<{ restored: true; state: "stopped" }> {
|
|
913
1149
|
await this.#stop();
|
|
914
1150
|
const elapsed = (await this.#machine.clock()) - this.#timingStart;
|
|
915
|
-
await this.#machine.restoreSnapshot(
|
|
1151
|
+
await this.#machine.restoreSnapshot(file);
|
|
916
1152
|
this.#state = "stopped";
|
|
917
1153
|
// The cycle clock jumped with the restore; the stopwatch keeps its reading, as a window move does.
|
|
918
1154
|
this.#timingStart = (await this.#machine.clock()) - elapsed;
|
|
919
1155
|
return { restored: true, state: "stopped" };
|
|
920
1156
|
}
|
|
921
1157
|
|
|
1158
|
+
/** A snapshot as bytes (D41): export a named one into `reply`, or restore from the attached .vsf. */
|
|
1159
|
+
async #snapshotFile(params: ViceOperations["snapshotFile"]["params"], attachments: Buffer[], reply: Buffer[]): Promise<ViceOperations["snapshotFile"]["result"]> {
|
|
1160
|
+
if (params.action === "export") {
|
|
1161
|
+
const file = this.#snapshots.get(params.name);
|
|
1162
|
+
if (file === undefined) throw new WireFailure("not-found", `There is no snapshot named ${params.name}.`);
|
|
1163
|
+
const bytes = readFileSync(file);
|
|
1164
|
+
reply.push(bytes);
|
|
1165
|
+
return { exported: true, bytes: bytes.length };
|
|
1166
|
+
}
|
|
1167
|
+
const bytes = onlyAttachment(attachments);
|
|
1168
|
+
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot (F-07).
|
|
1169
|
+
if (!bytes.subarray(0, VSF_MAGIC.length).equals(VSF_MAGIC)) throw new WireFailure("invalid-input", "The file is not a VICE snapshot file (.vsf). Give a file that c64_snapshot save wrote.");
|
|
1170
|
+
const file = this.#stage(bytes, "vsf");
|
|
1171
|
+
try {
|
|
1172
|
+
return await this.#restoreFrom(file);
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
if (error instanceof WireFailure) throw error;
|
|
1175
|
+
throw new WireFailure("invalid-input", "The emulator did not restore this snapshot file. A snapshot file restores only in the emulator version and with the machine settings that saved it.");
|
|
1176
|
+
} finally {
|
|
1177
|
+
rmSync(file, { force: true });
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
922
1181
|
/**
|
|
923
1182
|
* Moves the machine into a VICE of the other mode: a snapshot of this
|
|
924
1183
|
* one, restored in a new one, with the user points, joysticks, warp and the
|
|
@@ -933,6 +1192,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
933
1192
|
if (this.#held !== undefined) throw new WireFailure("machine-unavailable", this.#heldWhy);
|
|
934
1193
|
if (this.#launch === undefined) throw new WireFailure("operation-failed", "This session cannot start another emulator.");
|
|
935
1194
|
const running = this.#state === "running";
|
|
1195
|
+
this.#log(mode === "window" ? "moving the machine to a VICE with a window" : "moving the machine back to a headless VICE");
|
|
936
1196
|
await this.#stop();
|
|
937
1197
|
const handover: Handover = {
|
|
938
1198
|
file: join(this.#workDir, mode === "window" ? "window-open.vsf" : "window-close.vsf"),
|
|
@@ -1018,10 +1278,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1018
1278
|
if (baseline === undefined) throw new WireFailure("not-found", `There is no baseline named ${params.baseline}.`);
|
|
1019
1279
|
const current = await this.#observe(() => this.#machine.captureFrame(this.#videoStandard));
|
|
1020
1280
|
const difference = compareFrames(baseline.frame, current.frame, params.mask);
|
|
1021
|
-
|
|
1281
|
+
if (difference.comparedPixels === 0) throw new WireFailure("invalid-input", "The mask covers the whole screen: nothing to compare.");
|
|
1282
|
+
const mismatchRatio = difference.mismatchingPixels / difference.comparedPixels;
|
|
1022
1283
|
const result: ScreenComparison = {
|
|
1023
1284
|
match: mismatchRatio <= params.maxMismatchRatio,
|
|
1024
1285
|
mismatchingPixels: difference.mismatchingPixels,
|
|
1286
|
+
comparedPixels: difference.comparedPixels,
|
|
1025
1287
|
mismatchRatio,
|
|
1026
1288
|
};
|
|
1027
1289
|
if (difference.bounds !== undefined) result.bounds = difference.bounds;
|
|
@@ -1076,6 +1338,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1076
1338
|
*/
|
|
1077
1339
|
async #resumingCommand(send: () => Promise<void>): Promise<void> {
|
|
1078
1340
|
this.#state = "running";
|
|
1341
|
+
this.#lastStop = undefined;
|
|
1079
1342
|
try {
|
|
1080
1343
|
await send();
|
|
1081
1344
|
} catch (error) {
|
|
@@ -1251,10 +1514,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1251
1514
|
const stop: Stop = { pc: decodeProgramCounter(event.body), hits: this.#checkpointHits };
|
|
1252
1515
|
if (stop.hits.length > 0 || event.type === ResponseType.jam) this.#haltCount++;
|
|
1253
1516
|
if (stop.hits.some((hit) => this.#adapter?.isDriveCheckpoint(hit))) this.#driveStop = true;
|
|
1517
|
+
this.#lastStop = event.type === ResponseType.jam ? { stopReason: "jam" } : this.#userPoint(stop.hits);
|
|
1254
1518
|
this.#checkpointHits = [];
|
|
1255
1519
|
this.#state = "stopped";
|
|
1256
1520
|
this.#stopWaiters = this.#stopWaiters.filter((waiter) => !waiter(stop));
|
|
1257
1521
|
} else if (event.type === ResponseType.resumed) {
|
|
1522
|
+
this.#lastStop = undefined;
|
|
1258
1523
|
this.#state = "running";
|
|
1259
1524
|
}
|
|
1260
1525
|
}
|
|
@@ -1291,14 +1556,27 @@ async function setPoint(
|
|
|
1291
1556
|
return checkpoint;
|
|
1292
1557
|
}
|
|
1293
1558
|
|
|
1559
|
+
/**
|
|
1560
|
+
* Refuses an autostart index that selects no program: VICE numbers the
|
|
1561
|
+
* entries from 1, and the caller from 0, as the c64-disk directory list does.
|
|
1562
|
+
* A DEL entry holds no program (R2-08), and an index past the last entry selects nothing.
|
|
1563
|
+
*/
|
|
1564
|
+
function checkDirectoryEntry(entries: DiskFile[], index: number): void {
|
|
1565
|
+
const entry = entries[index - 1];
|
|
1566
|
+
if (entry === undefined) throw new WireFailure("invalid-input", `The disk has ${entries.length} directory entries, so index ${index - 1} selects no entry. The c64-disk directory list gives the index of each entry.`);
|
|
1567
|
+
if (entry.type === "del") throw new WireFailure("invalid-input", `Directory entry ${index - 1} (${entry.name}) is a DEL entry and holds no program. Give the index of a PRG entry.`);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1294
1570
|
/** The production factory: launch a headless VICE per session through the host's supervisor. */
|
|
1295
1571
|
export function viceSessionFactory(options: {
|
|
1296
1572
|
supervisor: ProcessSupervisor;
|
|
1297
1573
|
env?: NodeJS.ProcessEnv;
|
|
1298
1574
|
log?: (line: string) => void;
|
|
1575
|
+
/** The known flaws of the VICE version that the host checked at start. */
|
|
1576
|
+
flaws?: readonly string[];
|
|
1577
|
+
directory?: DiskDirectory;
|
|
1299
1578
|
}): ViceSessionFactory {
|
|
1300
|
-
|
|
1301
|
-
return async ({ videoStandard }) => {
|
|
1579
|
+
return async ({ videoStandard, log = options.log ?? (() => {}) }) => {
|
|
1302
1580
|
const launch: ViceLauncher = (mode, launchLog) =>
|
|
1303
1581
|
launchVice({
|
|
1304
1582
|
videoStandard,
|
|
@@ -1311,6 +1589,11 @@ export function viceSessionFactory(options: {
|
|
|
1311
1589
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
1312
1590
|
});
|
|
1313
1591
|
const vice = await launch("headless", () => {});
|
|
1314
|
-
return ViceSession.start(vice, videoStandard, log, {
|
|
1592
|
+
return ViceSession.start(vice, videoStandard, log, {
|
|
1593
|
+
launch,
|
|
1594
|
+
ownPath: (path) => options.supervisor.ownPath(path),
|
|
1595
|
+
...(options.flaws === undefined ? {} : { flaws: options.flaws }),
|
|
1596
|
+
...(options.directory === undefined ? {} : { directory: options.directory }),
|
|
1597
|
+
});
|
|
1315
1598
|
};
|
|
1316
1599
|
}
|