@henols/c64-re-tools 2.1.0 → 2.1.1
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/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +1 -0
- package/skills/c64-assembler/scripts/assembler.ts +10 -0
- package/skills/c64-disk/SKILL.md +2 -2
- package/skills/c64-disk/scripts/disk.ts +24 -10
- package/skills/c64-disk/scripts/lib/containers.ts +9 -9
- package/skills/c64-docs/scripts/lib/links.ts +3 -3
- package/skills/c64-docs/scripts/lib/reference.ts +2 -2
- package/skills/c64-emulator/SKILL.md +8 -2
- package/skills/c64-knowledge/SKILL.md +3 -1
- package/skills/c64-knowledge/scripts/knowledge.ts +59 -43
- package/skills/c64-memory-map/SKILL.md +1 -1
- package/skills/c64-memory-map/scripts/memory-map.ts +3 -2
- package/skills/c64-provenance/SKILL.md +5 -4
- package/skills/c64-provenance/scripts/lib/library.ts +18 -6
- package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
- package/skills/c64-provenance/scripts/provenance.ts +5 -4
- package/skills/c64-source/SKILL.md +2 -0
- package/skills/c64-source/scripts/lib/source.ts +2 -2
- package/skills/c64-source/scripts/source.ts +10 -6
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
- package/skills/c64-static-analysis/scripts/static-analysis.ts +4 -3
- package/skills/c64-testing/SKILL.md +11 -1
- package/skills/c64-testing/references/recording-format.md +2 -0
- package/skills/c64-testing/references/scenario-format.md +6 -0
- package/skills/c64-testing/scripts/lib/diff-files.ts +63 -13
- package/skills/c64-testing/scripts/lib/diff.ts +3 -3
- package/skills/c64-testing/scripts/lib/recording.ts +47 -12
- package/skills/c64-testing/scripts/lib/replay.ts +11 -0
- package/skills/c64-testing/scripts/lib/runs.ts +64 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
- package/skills/c64-testing/scripts/testing.ts +15 -2
- package/skills/c64-unpacker/SKILL.md +9 -5
- package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +3 -2
- package/skills/c64-unpacker/scripts/unpacker.ts +75 -25
- package/src/host/vice/session.ts +99 -10
- package/src/host-client/image.ts +32 -0
- package/src/host-client/steps.ts +106 -6
- package/src/host-client/tools.ts +1 -1
- package/src/host-client/transfer.ts +35 -8
- package/src/knowledge/expression.ts +1 -1
- package/src/knowledge/history.ts +19 -2
- package/src/knowledge/names.ts +2 -1
- package/src/knowledge/program.ts +1 -16
- package/src/knowledge/read.ts +43 -0
- package/src/knowledge/schema.ts +3 -2
- package/src/knowledge/text.ts +1 -0
- package/src/knowledge/write.ts +40 -6
- package/src/mcp/output.ts +5 -0
- package/src/mcp/server.ts +19 -4
- package/src/mcp/tools/debug.ts +7 -6
- package/src/mcp/tools/execution.ts +18 -5
- package/src/mcp/tools/media.ts +14 -2
- package/src/mcp/tools/memory.ts +7 -3
- package/src/mcp/tools/video.ts +5 -3
- package/src/project.ts +1 -1
- package/src/protocol/vice.ts +4 -1
|
@@ -12,10 +12,11 @@ import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
|
|
|
12
12
|
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
13
13
|
import { readActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
14
14
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
16
|
+
import { writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
16
17
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
17
18
|
import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
|
|
18
|
-
import { OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec } from "#src/script.ts";
|
|
19
|
+
import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec } from "#src/script.ts";
|
|
19
20
|
import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
|
|
20
21
|
import { loads, readLoadSteps } from "./lib/loads.ts";
|
|
21
22
|
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
|
|
@@ -38,10 +39,12 @@ async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise
|
|
|
38
39
|
return undefined;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
/** inspect, trace, stages and capture take a PRG: a disk, tape or cartridge file goes to loads. */
|
|
43
|
+
const NOT_PRG_REMEDY = "Use loads, which runs a disk, tape or cartridge image, or get the PRG out of it with disk.ts read of the c64-disk skill";
|
|
44
|
+
const readProgram = (path: string) => readPrg(path, NOT_PRG_REMEDY).bytes;
|
|
45
|
+
|
|
46
|
+
async function trace(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; max: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
47
|
+
const bytes = startable(program, options.entry);
|
|
45
48
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
46
49
|
try {
|
|
47
50
|
// Loaded but not started, so the memory map can start empty: the KERNAL start-up writes all of RAM.
|
|
@@ -54,7 +57,12 @@ async function trace(program: string, options: { frames: number; entry?: number;
|
|
|
54
57
|
const step = await client.execution({ action: "advance-frames", count: Math.min(MAX_ADVANCE, options.frames - ran), space: "c64" });
|
|
55
58
|
const advanced = step.advancedFrames ?? 0;
|
|
56
59
|
ran += advanced;
|
|
57
|
-
|
|
60
|
+
// A JAM or a point stops the run before its frames: the evidence would be of a run that did not happen.
|
|
61
|
+
if ((step.stopReason !== undefined && step.stopReason !== "frames") || advanced === 0) {
|
|
62
|
+
const stopReason = step.stopReason ?? "no-frames";
|
|
63
|
+
const at = step.pc ?? (await client.registersGet("c64")).pc;
|
|
64
|
+
return { output: { traced: false, frames: ran, stopReason, pc: formatC64Address(at), reason: stoppedEarly(stopReason, at, ran, options.frames) }, failed: true };
|
|
65
|
+
}
|
|
58
66
|
}
|
|
59
67
|
const { pc } = await client.registersGet("c64");
|
|
60
68
|
const ranges = await readMemoryMap(async (start) => {
|
|
@@ -80,7 +88,7 @@ async function trace(program: string, options: { frames: number; entry?: number;
|
|
|
80
88
|
traced: true,
|
|
81
89
|
frames: ran,
|
|
82
90
|
pc: formatC64Address(pc),
|
|
83
|
-
writtenThenExecuted: written.
|
|
91
|
+
writtenThenExecuted: listed(written, options.max, (range) => ({
|
|
84
92
|
start: formatC64Address(range.start),
|
|
85
93
|
end: formatC64Address(range.end),
|
|
86
94
|
...(underRom(range.start) ? { underRom: true } : {}),
|
|
@@ -107,6 +115,12 @@ async function trace(program: string, options: { frames: number; entry?: number;
|
|
|
107
115
|
|
|
108
116
|
const NO_BASIC = "the program is no BASIC program at $0801, so RUN cannot start it. Give --entry <address>.";
|
|
109
117
|
|
|
118
|
+
/** Why a run stopped before its frames: what stopped the CPU, where, and after how many frames. */
|
|
119
|
+
function stoppedEarly(stopReason: string, pc: number, ran: number, frames: number): string {
|
|
120
|
+
const what = stopReason === "jam" ? "a JAM instruction" : stopReason === "no-frames" ? "the emulator, which ran no frame" : `a ${stopReason}`;
|
|
121
|
+
return `The CPU stopped at ${formatC64Address(pc)} by ${what} after ${ran} of ${frames} frames. Check --entry and the steps: the program must run for the frames.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
110
124
|
/** The first PC in one of the ranges within SYS_FRAMES frames, found with exec breakpoints over them. */
|
|
111
125
|
async function firstPcIn(client: ViceSessionClient, ranges: Array<{ start: number; end: number }>): Promise<number | undefined> {
|
|
112
126
|
const points = [];
|
|
@@ -175,6 +189,8 @@ interface Followed {
|
|
|
175
189
|
reached: boolean;
|
|
176
190
|
/** Why the program did not start, when it did not. */
|
|
177
191
|
failed?: string;
|
|
192
|
+
/** What stopped the CPU before the frames ran: a JAM or a point of the program. */
|
|
193
|
+
halted?: { stopReason: string; pc: number };
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
type RunUntilStep = Extract<ActionStep, { runUntil: unknown }>;
|
|
@@ -197,13 +213,11 @@ function trackedRunUntil(step: ActionStep): step is RunUntilStep & { runUntil: {
|
|
|
197
213
|
async function follow(
|
|
198
214
|
client: ViceSessionClient,
|
|
199
215
|
program: string,
|
|
216
|
+
bytes: Uint8Array,
|
|
200
217
|
options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number; until?: number },
|
|
201
218
|
): Promise<Followed> {
|
|
202
|
-
const { bytes } = readProjectFile(program);
|
|
203
|
-
if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
|
|
204
219
|
const load = bytes[0]! | (bytes[1]! << 8);
|
|
205
220
|
const basic = basicStart(bytes);
|
|
206
|
-
if (options.entry === undefined && basic === undefined) throw new UsageError(NO_BASIC);
|
|
207
221
|
const notStarted = await startLoaded(client, program, bytes, options.entry);
|
|
208
222
|
if (notStarted !== undefined) return { stages: [], frames: 0, reached: false, failed: notStarted };
|
|
209
223
|
// The frames of each step end where a scenario's do: the first step counts from RUN, a later one from where it starts.
|
|
@@ -216,6 +230,7 @@ async function follow(
|
|
|
216
230
|
let firstFrame = 0;
|
|
217
231
|
let total = 0;
|
|
218
232
|
let ended = false;
|
|
233
|
+
let halted: { stopReason: string; pc: number } | undefined;
|
|
219
234
|
/** Where the run stops: the target of a runUntil step while it runs, then until. */
|
|
220
235
|
let stopAt: number | undefined;
|
|
221
236
|
const atTarget = (pc: number) => stopAt !== undefined && pc === stopAt;
|
|
@@ -235,7 +250,7 @@ async function follow(
|
|
|
235
250
|
* blocks, and stops at the first opcode that the stage wrote and the CPU
|
|
236
251
|
* then executes. Own code stops the run once; a write watch arms it again.
|
|
237
252
|
*/
|
|
238
|
-
const rerun = async (candidates: AddressRange[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean }> => {
|
|
253
|
+
const rerun = async (candidates: AddressRange[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean; halted?: { stopReason: string; pc: number } }> => {
|
|
239
254
|
const points = new Map<string, { kind: "exec" | "watch"; id: number }>();
|
|
240
255
|
const arm = async (wanted: { exec: AddressRange[]; watch: AddressRange[] }) => {
|
|
241
256
|
const keys = new Set([...wanted.exec.map((range) => `exec:${range.start}:${range.end}`), ...wanted.watch.map((range) => `watch:${range.start}:${range.end}`)]);
|
|
@@ -295,6 +310,7 @@ async function follow(
|
|
|
295
310
|
left -= advanced;
|
|
296
311
|
const pc = run.pc ?? (await client.registersGet("c64")).pc;
|
|
297
312
|
map = await readCoverage(client);
|
|
313
|
+
if (run.stopReason === "jam") return { used, map, atTarget: false, halted: { stopReason: "jam", pc } };
|
|
298
314
|
// A frame can end in written code that the stage called before: that stop starts no stage.
|
|
299
315
|
if (inRanges(candidates, pc) && !inRanges(stage.called, pc)) {
|
|
300
316
|
if (written(pc)) {
|
|
@@ -341,6 +357,14 @@ async function follow(
|
|
|
341
357
|
const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64", ...framed() });
|
|
342
358
|
const firstPc = first.pc ?? (await client.registersGet("c64")).pc;
|
|
343
359
|
const map = await readCoverage(client);
|
|
360
|
+
// A JAM, or a point that is not the target, ends the run before its frames.
|
|
361
|
+
if (first.stopReason !== undefined && first.stopReason !== "frames" && !atTarget(firstPc)) {
|
|
362
|
+
total += first.advancedFrames ?? 0;
|
|
363
|
+
mergeMaps(stage.map, map);
|
|
364
|
+
halted = { stopReason: first.stopReason, pc: firstPc };
|
|
365
|
+
ended = true;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
344
368
|
const candidates = stageCandidates(stage.map, map, stage.own, options.minStage, stage.called);
|
|
345
369
|
if (candidates.length === 0) {
|
|
346
370
|
const advanced = first.advancedFrames ?? 0;
|
|
@@ -369,6 +393,10 @@ async function follow(
|
|
|
369
393
|
if (again.next !== undefined) await startStage(again.next, again.map);
|
|
370
394
|
else mergeMaps(stage.map, again.map);
|
|
371
395
|
if (again.atTarget) stopped = true;
|
|
396
|
+
if (again.halted !== undefined) {
|
|
397
|
+
halted = again.halted;
|
|
398
|
+
ended = true;
|
|
399
|
+
}
|
|
372
400
|
}
|
|
373
401
|
return stopped;
|
|
374
402
|
};
|
|
@@ -389,7 +417,8 @@ async function follow(
|
|
|
389
417
|
else if (trackedRunUntil(step)) {
|
|
390
418
|
if (!(await trackTo(knowledgeAt()(step.runUntil.target.address), step.runUntil.timeoutFrames))) {
|
|
391
419
|
const { pc } = await client.registersGet("c64");
|
|
392
|
-
|
|
420
|
+
const why = halted === undefined ? "" : ` ${stoppedEarly(halted.stopReason, halted.pc, total, step.runUntil.timeoutFrames)}`;
|
|
421
|
+
return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It is at ${formatC64Address(pc)}.${why}` };
|
|
393
422
|
}
|
|
394
423
|
} else await runActionStep(client, step, knowledgeAt());
|
|
395
424
|
}
|
|
@@ -400,7 +429,7 @@ async function follow(
|
|
|
400
429
|
await client.snapshot({ action: "remove", name: STAGE_SNAPSHOT }).catch(() => undefined);
|
|
401
430
|
mergeMaps(stage.map, await readCoverage(client));
|
|
402
431
|
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage) });
|
|
403
|
-
return { stages: found, frames: total, reached };
|
|
432
|
+
return { stages: found, frames: total, reached, ...(halted === undefined ? {} : { halted }) };
|
|
404
433
|
}
|
|
405
434
|
|
|
406
435
|
const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
@@ -413,11 +442,23 @@ const showStages = (stages: Stage[]) =>
|
|
|
413
442
|
...(stage.next === undefined ? {} : { next: formatC64Address(stage.next) }),
|
|
414
443
|
}));
|
|
415
444
|
|
|
445
|
+
/** The bytes of a PRG that the emulator starts: RUN needs a BASIC program at $0801, else --entry gives the start. Checked before the emulator opens. */
|
|
446
|
+
function startable(program: string, entry: number | undefined): Uint8Array {
|
|
447
|
+
const bytes = readProgram(program);
|
|
448
|
+
if (entry === undefined && basicStart(bytes) === undefined) throw new UsageError(NO_BASIC);
|
|
449
|
+
return bytes;
|
|
450
|
+
}
|
|
451
|
+
|
|
416
452
|
async function stages(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
453
|
+
const bytes = startable(program, options.entry);
|
|
417
454
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
418
455
|
try {
|
|
419
|
-
const followed = await follow(client, program, options);
|
|
456
|
+
const followed = await follow(client, program, bytes, options);
|
|
420
457
|
if (followed.failed !== undefined) return { output: { followed: false, reason: followed.failed }, failed: true };
|
|
458
|
+
if (followed.halted !== undefined) {
|
|
459
|
+
const { stopReason, pc } = followed.halted;
|
|
460
|
+
return { output: { followed: false, frames: followed.frames, stopReason, pc: formatC64Address(pc), reason: stoppedEarly(stopReason, pc, followed.frames, options.frames), stages: showStages(followed.stages) }, failed: true };
|
|
461
|
+
}
|
|
421
462
|
return {
|
|
422
463
|
output: {
|
|
423
464
|
followed: true,
|
|
@@ -437,14 +478,20 @@ async function capture(
|
|
|
437
478
|
program: string,
|
|
438
479
|
options: { until: number; out: string; range?: AddressRange; exclude: AddressRange[]; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
|
|
439
480
|
): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
481
|
+
const bytes = startable(program, options.entry);
|
|
440
482
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
441
483
|
try {
|
|
442
|
-
const followed = await follow(client, program, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until, ...(options.entry === undefined ? {} : { entry: options.entry }) });
|
|
484
|
+
const followed = await follow(client, program, bytes, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until, ...(options.entry === undefined ? {} : { entry: options.entry }) });
|
|
443
485
|
if (followed.failed !== undefined) return { output: { captured: false, reason: followed.failed }, failed: true };
|
|
444
486
|
const registers = await client.registersGet("c64");
|
|
445
487
|
if (!followed.reached) {
|
|
446
488
|
return {
|
|
447
|
-
output: {
|
|
489
|
+
output: {
|
|
490
|
+
captured: false,
|
|
491
|
+
reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It is at ${formatC64Address(registers.pc)}.${followed.halted === undefined ? "" : ` ${stoppedEarly(followed.halted.stopReason, followed.halted.pc, followed.frames, options.timeoutFrames)}`}`,
|
|
492
|
+
...(followed.halted === undefined ? {} : { stopReason: followed.halted.stopReason }),
|
|
493
|
+
stages: showStages(followed.stages),
|
|
494
|
+
},
|
|
448
495
|
failed: true,
|
|
449
496
|
};
|
|
450
497
|
}
|
|
@@ -453,10 +500,6 @@ async function capture(
|
|
|
453
500
|
const state = await machineState(client);
|
|
454
501
|
// The segments: what the stages wrote, without loader leftovers, with no overlap.
|
|
455
502
|
const found = options.range !== undefined ? [options.range] : captureSegments(followed.stages, registers.pc);
|
|
456
|
-
const inside = options.exclude.find((range) => registers.pc >= range.start && registers.pc <= range.end);
|
|
457
|
-
if (inside !== undefined) {
|
|
458
|
-
return { output: { captured: false, reason: `The entry ${formatC64Address(registers.pc)} is in --exclude ${formatC64Address(inside.start)}-${formatC64Address(inside.end)}. A segment must hold the entry.` }, failed: true };
|
|
459
|
-
}
|
|
460
503
|
const segments = withoutRanges(found, options.exclude);
|
|
461
504
|
const file = (path: string) => `${options.out}/${path}`;
|
|
462
505
|
const written: Written[] = [];
|
|
@@ -534,14 +577,14 @@ const COMMANDS: CommandSpec[] = [
|
|
|
534
577
|
name: "inspect",
|
|
535
578
|
summary: "Packing evidence: the BASIC start and the entropy of the bytes.",
|
|
536
579
|
subjects: [PROGRAM],
|
|
537
|
-
run: (call) => inspect(
|
|
580
|
+
run: (call) => inspect(readProgram(call.subject(0))),
|
|
538
581
|
},
|
|
539
582
|
{
|
|
540
583
|
name: "trace",
|
|
541
584
|
summary: "Runs the program and reports the memory that it wrote and then executed.",
|
|
542
585
|
subjects: [PROGRAM],
|
|
543
|
-
options: { frames: FRAMES, entry: ENTRY, steps: STEPS },
|
|
544
|
-
run: async (call) => outcome(await trace(call.subject(0), { frames: call.count("frames")!, steps: stepsOf(call), ...entryOf(call) })),
|
|
586
|
+
options: { frames: FRAMES, entry: ENTRY, steps: STEPS, "max-listed": OPTIONS["max-listed"] },
|
|
587
|
+
run: async (call) => outcome(await trace(call.subject(0), { frames: call.count("frames")!, steps: stepsOf(call), max: call.count("max-listed")!, ...entryOf(call) })),
|
|
545
588
|
},
|
|
546
589
|
{
|
|
547
590
|
name: "stages",
|
|
@@ -573,7 +616,14 @@ const COMMANDS: CommandSpec[] = [
|
|
|
573
616
|
notes: "entry_state.json has the registers, $00/$01, pages 0-3, the VIC, SID and CIA registers, the vectors, and what differs from a clean reset.",
|
|
574
617
|
run: async (call) => {
|
|
575
618
|
const range = call.range("range");
|
|
576
|
-
|
|
619
|
+
const until = call.address("until")!;
|
|
620
|
+
// A segment must hold the entry: refused before the emulator runs.
|
|
621
|
+
const show = (item: AddressRange) => `${formatC64Address(item.start)}-${formatC64Address(item.end)}`;
|
|
622
|
+
const holds = (item: AddressRange) => until >= item.start && until <= item.end;
|
|
623
|
+
const excluded = call.ranges("exclude").find(holds);
|
|
624
|
+
if (excluded !== undefined) throw new UsageError(`--exclude ${show(excluded)} holds the entry --until ${formatC64Address(until)}. A segment must hold the entry: give an --exclude range without it`);
|
|
625
|
+
if (range !== undefined && !holds(range)) throw new UsageError(`--range ${show(range)} does not hold the entry --until ${formatC64Address(until)}. Give a --range that holds the entry`);
|
|
626
|
+
return outcome(await capture(call.subject(0), { until, out: call.string("out")!, ...(range === undefined ? {} : { range }), exclude: call.ranges("exclude"), timeoutFrames: call.count("timeout-frames")!, steps: stepsOf(call), minStage: call.count("min-stage")!, ...entryOf(call) }));
|
|
577
627
|
},
|
|
578
628
|
},
|
|
579
629
|
{
|
package/src/host/vice/session.ts
CHANGED
|
@@ -187,6 +187,16 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
187
187
|
#haltCount = 0;
|
|
188
188
|
/** What stopped the machine last, when a user breakpoint or watchpoint or a JAM did; cleared when it runs again. */
|
|
189
189
|
#lastStop: { stopReason: PointStopReason; pointId?: number } | undefined;
|
|
190
|
+
/**
|
|
191
|
+
* The pc of the JAM that holds the CPU. A jammed CPU runs again only after a
|
|
192
|
+
* reset, a snapshot restore or an autostart: VICE 3.10 keeps it jammed after a
|
|
193
|
+
* pc write, and reports the JAM only once.
|
|
194
|
+
*/
|
|
195
|
+
#jammed: number | undefined;
|
|
196
|
+
/** VICE records no CPU history after a JAM until the next reset. */
|
|
197
|
+
#historyStopped = false;
|
|
198
|
+
/** The CPU ran again after a JAM without a reset: the CPU history that VICE gives is old. */
|
|
199
|
+
#historyOld = false;
|
|
190
200
|
#queue: Promise<unknown> = Promise.resolve();
|
|
191
201
|
#closed = false;
|
|
192
202
|
#lost = false;
|
|
@@ -404,6 +414,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
404
414
|
centred.push(port);
|
|
405
415
|
}
|
|
406
416
|
await this.#runningCommand(() => this.#machine.autostart(file, index, run));
|
|
417
|
+
this.#clearJam();
|
|
407
418
|
if (type === "crt") this.#cartridge = true;
|
|
408
419
|
this.#replaceStaged(stagedKind(type), file);
|
|
409
420
|
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
@@ -431,7 +442,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
431
442
|
case "screenCapture": {
|
|
432
443
|
const { baseline } = params as ViceOperations["screenCapture"]["params"];
|
|
433
444
|
if (baseline !== undefined && !this.#baselines.has(baseline) && this.#baselines.size >= MAX_BASELINES) {
|
|
434
|
-
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_BASELINES} baselines.
|
|
445
|
+
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_BASELINES} baselines. Remove one first with c64_screen action remove.`);
|
|
435
446
|
}
|
|
436
447
|
const shot = await this.#observe(() => this.#machine.captureFrame(this.#videoStandard));
|
|
437
448
|
if (baseline !== undefined) this.#baselines.set(baseline, shot);
|
|
@@ -530,7 +541,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
530
541
|
if (status.state === "stopped") {
|
|
531
542
|
status.pc = await this.#pc();
|
|
532
543
|
// VICE 3.9 sends no JAM event: the opcode at the pc tells.
|
|
533
|
-
const stop = this.#lastStop ?? ((await this.#
|
|
544
|
+
const stop = this.#lastStop ?? ((await this.#jamAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
|
|
534
545
|
if (stop !== undefined) Object.assign(status, stop);
|
|
535
546
|
}
|
|
536
547
|
return status;
|
|
@@ -538,7 +549,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
538
549
|
|
|
539
550
|
async #execution(params: ExecutionParams): Promise<ExecutionResult> {
|
|
540
551
|
if (params.action === "resume") {
|
|
541
|
-
if (this.#state === "stopped")
|
|
552
|
+
if (this.#state === "stopped") {
|
|
553
|
+
await this.#refuseJammed("resume");
|
|
554
|
+
await this.#resume();
|
|
555
|
+
}
|
|
542
556
|
return { state: "running" };
|
|
543
557
|
}
|
|
544
558
|
if (params.action === "pause") {
|
|
@@ -546,6 +560,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
546
560
|
// A drive checkpoint can stop the machine just before the pause does.
|
|
547
561
|
return { state: "stopped", pc: (await this.#completeDriveStop()) ?? (await this.#pc()) };
|
|
548
562
|
}
|
|
563
|
+
if (params.space === "drive8" && params.action === "advance-frames") {
|
|
564
|
+
throw new WireFailure("unsupported-in-space", "advance-frames counts the video frames of the computer: use space c64. A breakpoint or watchpoint in space drive8 also stops it.");
|
|
565
|
+
}
|
|
549
566
|
if (params.space === "drive8") {
|
|
550
567
|
// Stock VICE runs the 1541 CPU in batches that catch up with the computer's clock;
|
|
551
568
|
// neither monitor can stop it after exactly one of its instructions.
|
|
@@ -565,13 +582,16 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
565
582
|
const outcome = await this.#runFrames(params.count, undefined, params.anchor);
|
|
566
583
|
return {
|
|
567
584
|
state: "stopped",
|
|
568
|
-
|
|
585
|
+
// A stop event carries the computer's PC; a stop in the drive reports the drive's.
|
|
586
|
+
pc: outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc,
|
|
587
|
+
space: outcome.space,
|
|
569
588
|
advancedFrames: outcome.frames,
|
|
570
589
|
stopReason: outcome.end === "target" ? "frames" : outcome.end,
|
|
571
590
|
...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }),
|
|
572
591
|
};
|
|
573
592
|
}
|
|
574
593
|
await this.#stop();
|
|
594
|
+
await this.#refuseJammed(params.action);
|
|
575
595
|
if (params.action === "until-return") {
|
|
576
596
|
const stopped = this.#nextStop(this.#untilReturnLimitMs);
|
|
577
597
|
await this.#resumingCommand(() => this.#machine.untilReturn());
|
|
@@ -599,6 +619,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
599
619
|
|
|
600
620
|
async #reset(params: { mode: "soft" | "hard"; run: boolean }): Promise<{ state: RunState; pc?: number }> {
|
|
601
621
|
await this.#stop();
|
|
622
|
+
this.#clearJam();
|
|
602
623
|
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
603
624
|
this.#timingStart = 0n;
|
|
604
625
|
if (params.run) {
|
|
@@ -637,11 +658,15 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
637
658
|
const start = anchor ?? (await this.#machine.rasterPosition());
|
|
638
659
|
const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
|
|
639
660
|
const ending = rasterWindowExpression(start.line, start.cycle, lines);
|
|
661
|
+
const startPc = await this.#pc();
|
|
662
|
+
if (await this.#jamAt(startPc)) return { end: "jam", frames: 0, pc: startPc, space: "c64" };
|
|
640
663
|
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
641
664
|
try {
|
|
642
665
|
await this.#machine.setCondition(clock, arming);
|
|
643
666
|
let armingPhase = true;
|
|
644
667
|
let frames = 0;
|
|
668
|
+
// VICE reports a JAM only once until a reset: a jammed CPU stops the clock at the same pc again and again.
|
|
669
|
+
let clockPc = startPc;
|
|
645
670
|
for (;;) {
|
|
646
671
|
const next = this.#nextStop(HALF_FRAME_LIMIT_MS);
|
|
647
672
|
await this.#resume();
|
|
@@ -652,6 +677,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
652
677
|
}
|
|
653
678
|
const end = this.#stopCause(stop, clock, target?.checkpoint);
|
|
654
679
|
if (end === "clock") {
|
|
680
|
+
if (stop.pc === clockPc && (await this.#jamAt(stop.pc))) return { end: "jam", frames, pc: stop.pc, space: "c64" };
|
|
681
|
+
clockPc = stop.pc;
|
|
655
682
|
if (armingPhase) {
|
|
656
683
|
await this.#machine.setCondition(clock, ending);
|
|
657
684
|
if (target?.deferred) {
|
|
@@ -667,7 +694,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
667
694
|
continue;
|
|
668
695
|
}
|
|
669
696
|
if (end === "unknown") {
|
|
670
|
-
if (await this.#
|
|
697
|
+
if (await this.#jamAt(stop.pc)) return { end: "jam", frames, pc: stop.pc, space: "c64" };
|
|
671
698
|
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this run.");
|
|
672
699
|
}
|
|
673
700
|
// A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
|
|
@@ -699,6 +726,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
699
726
|
const points: number[] = [];
|
|
700
727
|
const hits: TraceHit[] = [];
|
|
701
728
|
let frames = 0;
|
|
729
|
+
const startPc = await this.#pc();
|
|
730
|
+
if (await this.#jamAt(startPc)) return { hits, frames, end: "jam", pc: startPc };
|
|
731
|
+
let clockPc = startPc;
|
|
702
732
|
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
703
733
|
try {
|
|
704
734
|
for (const point of params.points) points.push(await this.#machine.addCheckpoint({ start: point.start, end: point.end, operation: operation[point.kind], space: "c64" }));
|
|
@@ -718,6 +748,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
718
748
|
if (hits.length >= params.maxHits) return { hits, frames, end: "hits", pc: stop.pc };
|
|
719
749
|
}
|
|
720
750
|
if (stop.hits.includes(clock)) {
|
|
751
|
+
if (stop.pc === clockPc && (await this.#jamAt(stop.pc))) return { hits, frames, end: "jam", pc: stop.pc };
|
|
752
|
+
clockPc = stop.pc;
|
|
721
753
|
if (armingPhase) await this.#machine.setCondition(clock, ending);
|
|
722
754
|
else {
|
|
723
755
|
frames++;
|
|
@@ -730,7 +762,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
730
762
|
if (traced.length > 0) continue;
|
|
731
763
|
const point = this.#userPoint(stop.hits);
|
|
732
764
|
if (point !== undefined) return { hits, frames, end: point.stopReason, pc: stop.pc };
|
|
733
|
-
if (await this.#
|
|
765
|
+
if (await this.#jamAt(stop.pc)) return { hits, frames, end: "jam", pc: stop.pc };
|
|
734
766
|
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this trace.");
|
|
735
767
|
}
|
|
736
768
|
} finally {
|
|
@@ -844,9 +876,40 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
844
876
|
return undefined;
|
|
845
877
|
}
|
|
846
878
|
|
|
847
|
-
|
|
879
|
+
/**
|
|
880
|
+
* True when a JAM holds the CPU at `pc`: the session saw a JAM since the last
|
|
881
|
+
* reset, restore or autostart, or the opcode at `pc` is a JAM. The CPU stops
|
|
882
|
+
* on such an opcode, so it executes nothing more. The session keeps the JAM.
|
|
883
|
+
*/
|
|
884
|
+
async #jamAt(pc: number): Promise<boolean> {
|
|
885
|
+
if (this.#jammed !== undefined) return true;
|
|
848
886
|
const opcode = await this.#machine.readMemory({ address: pc, size: 1, space: "c64", view: "cpu" });
|
|
849
|
-
|
|
887
|
+
if (!JAM_OPCODES.has(Number.parseInt(opcode, 16))) return false;
|
|
888
|
+
this.#jam(pc);
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
#jam(pc: number): void {
|
|
893
|
+
this.#jammed = pc;
|
|
894
|
+
this.#historyStopped = true;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** A reset or an autostart starts a jammed CPU and the CPU history again. */
|
|
898
|
+
#clearJam(): void {
|
|
899
|
+
this.#jammed = undefined;
|
|
900
|
+
this.#historyStopped = false;
|
|
901
|
+
this.#historyOld = false;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Refuses an action that needs the CPU to execute, while a JAM holds it. */
|
|
905
|
+
async #refuseJammed(action: string): Promise<void> {
|
|
906
|
+
const pc = await this.#pc();
|
|
907
|
+
if (!(await this.#jamAt(pc))) return;
|
|
908
|
+
throw new WireFailure(
|
|
909
|
+
"operation-failed",
|
|
910
|
+
`The CPU is jammed by the JAM opcode at ${formatC64Address(this.#jammed ?? pc)}, so ${action} cannot execute an instruction. ` +
|
|
911
|
+
"The CPU runs again only after c64_reset, a snapshot restore or c64_autostart.",
|
|
912
|
+
);
|
|
850
913
|
}
|
|
851
914
|
|
|
852
915
|
/** Runs until a target is reached or `timeoutFrames` frames pass. */
|
|
@@ -1050,6 +1113,13 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1050
1113
|
* stop, whose position is known, mapped back through the frame.
|
|
1051
1114
|
*/
|
|
1052
1115
|
async #cpuHistory(params: { limit: number; space: "c64" | "drive8" }): Promise<{ entries: HistoryEntry[] }> {
|
|
1116
|
+
if (params.space === "c64" && this.#historyOld) {
|
|
1117
|
+
throw new WireFailure(
|
|
1118
|
+
"operation-failed",
|
|
1119
|
+
"VICE records no CPU history after a JAM until the next reset, and the CPU ran after the JAM. The history would show old instructions. " +
|
|
1120
|
+
"Use c64_reset to record again, or use c64_trace.",
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1053
1123
|
return this.#observe(async () => {
|
|
1054
1124
|
const raw = await this.#machine.history(params.limit, params.space);
|
|
1055
1125
|
if (params.space === "drive8") return { entries: raw.map(({ clock: _clock, ...entry }) => entry) };
|
|
@@ -1128,7 +1198,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1128
1198
|
const existing = this.#snapshots.get(params.name);
|
|
1129
1199
|
if (params.action === "save") {
|
|
1130
1200
|
if (existing === undefined && this.#snapshots.size >= MAX_SNAPSHOTS) {
|
|
1131
|
-
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_SNAPSHOTS} snapshots.
|
|
1201
|
+
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_SNAPSHOTS} snapshots. Remove one first with action remove.`);
|
|
1132
1202
|
}
|
|
1133
1203
|
const file = existing ?? join(this.#workDir, `snapshot-${++this.#snapshotFiles}.vsf`);
|
|
1134
1204
|
await this.#observe(() => this.#machine.saveSnapshot(file));
|
|
@@ -1149,6 +1219,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1149
1219
|
await this.#stop();
|
|
1150
1220
|
const elapsed = (await this.#machine.clock()) - this.#timingStart;
|
|
1151
1221
|
await this.#machine.restoreSnapshot(file);
|
|
1222
|
+
// A restore starts a jammed CPU again, but VICE still records no CPU history.
|
|
1223
|
+
this.#jammed = undefined;
|
|
1224
|
+
if (this.#historyStopped) this.#historyOld = true;
|
|
1152
1225
|
this.#state = "stopped";
|
|
1153
1226
|
// The cycle clock jumped with the restore; the stopwatch keeps its reading, as a window move does.
|
|
1154
1227
|
this.#timingStart = (await this.#machine.clock()) - elapsed;
|
|
@@ -1168,13 +1241,26 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1168
1241
|
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot.
|
|
1169
1242
|
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
1243
|
const file = this.#stage(bytes, "vsf");
|
|
1244
|
+
// VICE resets the machine after a snapshot file that it cannot read: a backup keeps the state before the restore.
|
|
1245
|
+
const backup = join(this.#workDir, "before-restore.vsf");
|
|
1246
|
+
await this.#stop();
|
|
1247
|
+
await this.#machine.saveSnapshot(backup);
|
|
1171
1248
|
try {
|
|
1172
1249
|
return await this.#restoreFrom(file);
|
|
1173
1250
|
} catch (error) {
|
|
1251
|
+
const kept = await this.#restoreFrom(backup).then(
|
|
1252
|
+
() => true,
|
|
1253
|
+
() => false,
|
|
1254
|
+
);
|
|
1255
|
+
if (!kept) throw new WireFailure("machine-state-lost", "The emulator did not restore this snapshot file, and the machine state from before the restore is lost. Load the program again.");
|
|
1174
1256
|
if (error instanceof WireFailure) throw error;
|
|
1175
|
-
throw new WireFailure(
|
|
1257
|
+
throw new WireFailure(
|
|
1258
|
+
"invalid-input",
|
|
1259
|
+
"The emulator did not restore this snapshot file. The machine state is as before. A snapshot file restores only in the emulator version and with the machine settings that saved it.",
|
|
1260
|
+
);
|
|
1176
1261
|
} finally {
|
|
1177
1262
|
rmSync(file, { force: true });
|
|
1263
|
+
rmSync(backup, { force: true });
|
|
1178
1264
|
}
|
|
1179
1265
|
}
|
|
1180
1266
|
|
|
@@ -1255,6 +1341,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1255
1341
|
if (this.#closed) throw new WireFailure("machine-unavailable", "The emulator session is closed.");
|
|
1256
1342
|
this.#vice = vice;
|
|
1257
1343
|
this.#adapter = adapter;
|
|
1344
|
+
// A new emulator: no JAM holds its CPU, and it records CPU history.
|
|
1345
|
+
this.#clearJam();
|
|
1258
1346
|
this.#mode = mode;
|
|
1259
1347
|
for (const [point, checkpoint] of points) point.checkpoint = checkpoint;
|
|
1260
1348
|
this.#timingStart = clock - handover.elapsed;
|
|
@@ -1515,6 +1603,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1515
1603
|
if (stop.hits.length > 0 || event.type === ResponseType.jam) this.#haltCount++;
|
|
1516
1604
|
if (stop.hits.some((hit) => this.#adapter?.isDriveCheckpoint(hit))) this.#driveStop = true;
|
|
1517
1605
|
this.#lastStop = event.type === ResponseType.jam ? { stopReason: "jam" } : this.#userPoint(stop.hits);
|
|
1606
|
+
if (event.type === ResponseType.jam) this.#jam(stop.pc);
|
|
1518
1607
|
this.#checkpointHits = [];
|
|
1519
1608
|
this.#state = "stopped";
|
|
1520
1609
|
this.#stopWaiters = this.#stopWaiters.filter((waiter) => !waiter(stop));
|
package/src/host-client/image.ts
CHANGED
|
@@ -21,6 +21,38 @@ export interface Image {
|
|
|
21
21
|
stages?: Array<Record<string, unknown>>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** The kinds of media files that are no PRG, by extension. */
|
|
25
|
+
const MEDIA_KINDS: Record<string, string> = {
|
|
26
|
+
d64: "a disk image",
|
|
27
|
+
d71: "a disk image",
|
|
28
|
+
d81: "a disk image",
|
|
29
|
+
g64: "a disk image",
|
|
30
|
+
x64: "a disk image",
|
|
31
|
+
t64: "a tape file",
|
|
32
|
+
tap: "a tape file",
|
|
33
|
+
crt: "a cartridge file",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The remedy for a disk, tape or cartridge file where a PRG must be: get the PRG out of it. */
|
|
37
|
+
export const MEDIA_REMEDY = "Get the PRG out of it with disk.ts read (or disk.ts crt for a cartridge) of the c64-disk skill";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Reads a PRG of the project: a 2-byte load address, and bytes that end at
|
|
41
|
+
* $ffff or before. Refuses a disk, tape or cartridge file and any other file
|
|
42
|
+
* that is no PRG with invalid-input; `remedy` tells what to do in its place.
|
|
43
|
+
*/
|
|
44
|
+
export function readPrg(path: string, remedy = MEDIA_REMEDY): { bytes: Buffer; start: number; end: number; body: Uint8Array } {
|
|
45
|
+
const { bytes, type } = readProjectFile(path);
|
|
46
|
+
const media = MEDIA_KINDS[type];
|
|
47
|
+
if (media !== undefined) throw new Refusal("invalid-input", `${path} is ${media} (.${type}), not a PRG. ${remedy}.`);
|
|
48
|
+
try {
|
|
49
|
+
return { bytes, ...prgRange(bytes) };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (isRefusal(error)) throw new Refusal("invalid-input", `${path} is not a PRG: ${error.message} ${remedy}.`);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
24
56
|
/** Reads an image.json or a PRG, relative to the project folder. */
|
|
25
57
|
export function readImage(path: string): Image {
|
|
26
58
|
const { bytes } = readProjectFile(path);
|