@henols/c64-re-tools 2.0.0-rc.6 → 2.0.0-rc.8
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 -1
- package/skills/c64-basic/SKILL.md +2 -2
- package/skills/c64-basic/scripts/basic.ts +1 -1
- package/skills/c64-disk/SKILL.md +3 -3
- package/skills/c64-disk/scripts/disk.ts +2 -1
- package/skills/c64-emulator/SKILL.md +4 -1
- package/skills/c64-knowledge/SKILL.md +2 -2
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-provenance/SKILL.md +10 -5
- package/skills/c64-provenance/scripts/compare.ts +35 -6
- package/skills/c64-provenance/scripts/provenance.ts +35 -15
- package/skills/c64-provenance/scripts/separate.ts +12 -4
- package/skills/c64-reverse-engineering/SKILL.md +2 -2
- package/skills/c64-source/SKILL.md +18 -11
- package/skills/c64-source/scripts/generate.ts +55 -10
- package/skills/c64-source/scripts/loader.ts +253 -6
- package/skills/c64-source/scripts/source.ts +46 -11
- package/skills/c64-static-analysis/SKILL.md +4 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
- package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
- package/skills/c64-testing/SKILL.md +1 -1
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/scenario.ts +9 -7
- package/skills/c64-unpacker/SKILL.md +11 -9
- package/skills/c64-unpacker/scripts/loads.ts +16 -4
- package/skills/c64-unpacker/scripts/machine.ts +13 -1
- package/skills/c64-unpacker/scripts/stages.ts +66 -3
- package/skills/c64-unpacker/scripts/unpack.ts +135 -42
- package/src/c64.ts +19 -5
- package/src/cli/status.ts +26 -3
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/petcat.ts +5 -2
- package/src/host/tools/status.ts +6 -3
- package/src/host/vice/adapter.ts +37 -9
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +7 -2
- package/src/host/vice/screen.ts +3 -2
- package/src/host/vice/session.ts +68 -15
- package/src/host-client/autostart.ts +11 -1
- package/src/host-client/connect.ts +10 -10
- package/src/host-client/coverage.ts +19 -10
- package/src/host-client/tools.ts +6 -6
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/schema.ts +7 -0
- package/src/knowledge/write.ts +5 -0
- package/src/mcp/tools/debug.ts +9 -5
- package/src/mcp/tools/machine.ts +4 -1
- package/src/mcp/tools/media.ts +7 -1
- package/src/mcp/tools/video.ts +7 -2
- package/src/native/discover.ts +2 -1
- package/src/platform.ts +3 -2
- package/src/protocol/tools.ts +19 -4
- package/src/protocol/vice.ts +56 -5
package/src/host/vice/adapter.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type Comparison, type Condition, type BacktraceFrame, type HistoryEntry
|
|
|
6
6
|
import { COVERAGE, formatC64Address, formatC64Byte } from "../../c64.ts";
|
|
7
7
|
import { decodeDisplay, decodePalette, visibleFrame, type IndexedFrame } from "./screen.ts";
|
|
8
8
|
import type { TextMonitor } from "./text-monitor.ts";
|
|
9
|
+
import type { CiaHidden } from "./chips.ts";
|
|
9
10
|
import {
|
|
10
11
|
BinaryMonitor,
|
|
11
12
|
Command,
|
|
@@ -135,7 +136,7 @@ export function parseDisassembly(answer: string, start: number): Instruction[] {
|
|
|
135
136
|
for (const line of answer.split("\n")) {
|
|
136
137
|
const match = /^\.(?:C|\d+):([0-9a-f]{4})\s+((?:[0-9a-f]{2} )*[0-9a-f]{2})\s+(.*?)\s*$/i.exec(line);
|
|
137
138
|
if (match === null) {
|
|
138
|
-
if (line.trim() !== "") unknown.push(line.trim());
|
|
139
|
+
if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
|
|
139
140
|
continue;
|
|
140
141
|
}
|
|
141
142
|
const address = Number.parseInt(match[1]!, 16);
|
|
@@ -164,7 +165,7 @@ export function parseHistory(answer: string): Array<Omit<HistoryEntry, "rasterLi
|
|
|
164
165
|
line,
|
|
165
166
|
);
|
|
166
167
|
if (match === null) {
|
|
167
|
-
if (line.trim() !== "") unknown.push(line.trim());
|
|
168
|
+
if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
|
|
168
169
|
continue;
|
|
169
170
|
}
|
|
170
171
|
entries.push({
|
|
@@ -193,7 +194,7 @@ export function parseHunt(answer: string, limit: number): number[] {
|
|
|
193
194
|
for (const line of answer.split("\n")) {
|
|
194
195
|
const match = /^([0-9a-f]{4})$/i.exec(line.trim());
|
|
195
196
|
if (match === null) {
|
|
196
|
-
if (line.trim() !== "") unknown.push(line.trim());
|
|
197
|
+
if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
|
|
197
198
|
continue;
|
|
198
199
|
}
|
|
199
200
|
if (matches.length < limit) matches.push(Number.parseInt(match[1]!, 16));
|
|
@@ -236,14 +237,15 @@ export function parseProfile(answer: string): ProfileEntry[] {
|
|
|
236
237
|
const unknown: string[] = [];
|
|
237
238
|
for (const line of answer.split("\n")) {
|
|
238
239
|
// Counts can carry the host locale's digit grouping ("1,123,200") and the percent a decimal comma.
|
|
239
|
-
|
|
240
|
+
// A profile of zero cycles, as after a window move restarts it, prints its percents as "-nan".
|
|
241
|
+
const row = new RegExp(`^\\s*(${COUNT})\\s+(?:[\\d.,]+|-?nan)%\\s+(${COUNT})\\s+([\\d.,]+|-?nan)%\\s+(\\S+)\\s*$`, "i").exec(line);
|
|
240
242
|
if (row === null) {
|
|
241
|
-
if (line.trim() !== "" && !/^Total\s+%\s+Self\s+%$/.test(line.trim()) && !/^[-\s]+$/.test(line)) unknown.push(line.trim());
|
|
243
|
+
if (line.trim() !== "" && !CONDITION_LINE.test(line.trim()) && !/^Total\s+%\s+Self\s+%$/.test(line.trim()) && !/^[-\s]+$/.test(line)) unknown.push(line.trim());
|
|
242
244
|
continue;
|
|
243
245
|
}
|
|
244
246
|
// A row named ROOT or by a label has no routine address.
|
|
245
247
|
if (!/^[0-9a-f]{4}$/i.test(row[4]!)) continue;
|
|
246
|
-
entries.push({ address: Number.parseInt(row[4]!, 16), totalCycles: digits(row[1]!), selfCycles: digits(row[2]!), percent: Number(row[3]!.replace(",", ".")) });
|
|
248
|
+
entries.push({ address: Number.parseInt(row[4]!, 16), totalCycles: digits(row[1]!), selfCycles: digits(row[2]!), percent: /nan/i.test(row[3]!) ? 0 : Number(row[3]!.replace(",", ".")) });
|
|
247
249
|
}
|
|
248
250
|
// An answer in another form must not read as "nothing ran".
|
|
249
251
|
if (entries.length === 0 && unknown.length > 0) throw unknownForm("profile", unknown);
|
|
@@ -284,7 +286,7 @@ export function parseMemmap(answer: string, maxRanges: number): { ranges: Memmap
|
|
|
284
286
|
for (const line of answer.split("\n")) {
|
|
285
287
|
const match = /^([0-9a-f]{4}): (\S{3}) (\S{3}) (\S{3})/i.exec(line);
|
|
286
288
|
if (match === null) {
|
|
287
|
-
if (line.trim() !== "" && !/^addr:\s+IO\s+ROM\s+RAM$/.test(line.trim())) unknown.push(line.trim());
|
|
289
|
+
if (line.trim() !== "" && !CONDITION_LINE.test(line.trim()) && !/^addr:\s+IO\s+ROM\s+RAM$/.test(line.trim())) unknown.push(line.trim());
|
|
288
290
|
continue;
|
|
289
291
|
}
|
|
290
292
|
const address = Number.parseInt(match[1]!, 16);
|
|
@@ -308,12 +310,19 @@ export function parseMemmap(answer: string, maxRanges: number): { ranges: Memmap
|
|
|
308
310
|
*/
|
|
309
311
|
const STOP_LINE = /^(?:#\d+ \((?:Stop on|Trace)\s|\.(?:C|\d+):[0-9a-f]{4}\s)/i;
|
|
310
312
|
|
|
311
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* The line VICE prints on the text monitor when the binary monitor sets a
|
|
315
|
+
* checkpoint condition. It can come late, in the answer of the next text
|
|
316
|
+
* command (seen on Windows).
|
|
317
|
+
*/
|
|
318
|
+
const CONDITION_LINE = /^Setting checkpoint \d+ condition to: /;
|
|
319
|
+
|
|
320
|
+
/** The lines of a text command's answer that are not blank, not a stop line, not a condition line and not `known`. */
|
|
312
321
|
function otherLines(answer: string, known?: RegExp): string[] {
|
|
313
322
|
return answer
|
|
314
323
|
.split("\n")
|
|
315
324
|
.map((line) => line.trim())
|
|
316
|
-
.filter((line) => line !== "" && !STOP_LINE.test(line) && known?.test(line) !== true);
|
|
325
|
+
.filter((line) => line !== "" && !STOP_LINE.test(line) && !CONDITION_LINE.test(line) && known?.test(line) !== true);
|
|
317
326
|
}
|
|
318
327
|
|
|
319
328
|
/** Refuses the answer of a text command that prints nothing, or only `known` lines, when it works. */
|
|
@@ -423,6 +432,19 @@ export class ViceAdapter {
|
|
|
423
432
|
return bytes.toString("hex");
|
|
424
433
|
}
|
|
425
434
|
|
|
435
|
+
/**
|
|
436
|
+
* The timer latches and the interrupt mask of the CIA at `base`, from VICE's
|
|
437
|
+
* text-monitor dump: no register read gives them (D63).
|
|
438
|
+
*/
|
|
439
|
+
async ciaHidden(base: 0xdc00 | 0xdd00): Promise<CiaHidden> {
|
|
440
|
+
const answer = await this.#computerText(`io ${hex4(base)}`);
|
|
441
|
+
const mask = /\(mask: ([0-9a-f]{2})\)/i.exec(answer);
|
|
442
|
+
const latchA = /Timer A: [0-9a-f]{4} \(latched ([0-9a-f]{4})\)/i.exec(answer);
|
|
443
|
+
const latchB = /Timer B: [0-9a-f]{4} \(latched ([0-9a-f]{4})\)/i.exec(answer);
|
|
444
|
+
if (mask === null || latchA === null || latchB === null) throw unknownForm("CIA dump", answer.split("\n"));
|
|
445
|
+
return { latchA: Number.parseInt(latchA[1]!, 16), latchB: Number.parseInt(latchB[1]!, 16), interruptMask: Number.parseInt(mask[1]!, 16) & 0x1f };
|
|
446
|
+
}
|
|
447
|
+
|
|
426
448
|
/** Reads C64 I/O registers through the I/O bank, whatever the CPU has banked in, without side effects. */
|
|
427
449
|
async readIo(start: number, size: number): Promise<Uint8Array> {
|
|
428
450
|
const response = await this.#monitor.request(
|
|
@@ -631,6 +653,12 @@ export class ViceAdapter {
|
|
|
631
653
|
await this.#monitor.request(Command.autostart, Buffer.concat([head, name]));
|
|
632
654
|
}
|
|
633
655
|
|
|
656
|
+
/** Detaches every cartridge. VICE powers the cartridge off; the C64 keeps running. */
|
|
657
|
+
async detachCartridge(): Promise<void> {
|
|
658
|
+
// Device 32 is the cartridge port; the monitor reads numbers as hex. VICE prints nothing when it detaches.
|
|
659
|
+
expectQuiet("cartridge detach answer", await this.#text.command("detach 20"));
|
|
660
|
+
}
|
|
661
|
+
|
|
634
662
|
/** Attaches a disk image file from the host file system to drive 8. */
|
|
635
663
|
async attachDisk(file: string): Promise<void> {
|
|
636
664
|
const answer = await this.#text.command(`attach ${quoted(file)} 8`);
|
package/src/host/vice/chips.ts
CHANGED
|
@@ -57,8 +57,15 @@ function bcd(value: number): number {
|
|
|
57
57
|
return (value >> 4) * 10 + (value & 0x0f);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** The parts of a CIA that no register read gives: the timer latches and the interrupt mask. */
|
|
61
|
+
export interface CiaHidden {
|
|
62
|
+
latchA: number;
|
|
63
|
+
latchB: number;
|
|
64
|
+
interruptMask: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
/** `registers` holds $dc00-$dc0f or $dd00-$dd0f. TOD hours become 0-23. */
|
|
61
|
-
export function decodeCia(id: 1 | 2, registers: Uint8Array): CiaState {
|
|
68
|
+
export function decodeCia(id: 1 | 2, registers: Uint8Array, hidden: CiaHidden): CiaState {
|
|
62
69
|
const word = (low: number) => registers[low]! | (registers[low + 1]! << 8);
|
|
63
70
|
const hourRegister = registers[0x0b]!;
|
|
64
71
|
const pm = (hourRegister & 0x80) !== 0;
|
|
@@ -70,9 +77,12 @@ export function decodeCia(id: 1 | 2, registers: Uint8Array): CiaState {
|
|
|
70
77
|
ddrB: registers[0x03]!,
|
|
71
78
|
timerA: word(0x04),
|
|
72
79
|
timerB: word(0x06),
|
|
80
|
+
latchA: hidden.latchA,
|
|
81
|
+
latchB: hidden.latchB,
|
|
73
82
|
controlA: registers[0x0e]!,
|
|
74
83
|
controlB: registers[0x0f]!,
|
|
75
84
|
interruptStatus: registers[0x0d]!,
|
|
85
|
+
interruptMask: hidden.interruptMask,
|
|
76
86
|
tod: {
|
|
77
87
|
hours: (bcd(hourRegister & 0x1f) % 12) + (pm ? 12 : 0),
|
|
78
88
|
minutes: bcd(registers[0x0a]! & 0x7f),
|
package/src/host/vice/process.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// VICE launch, readiness and termination. The launch contract of x64sc:
|
|
2
2
|
// - `-default` must come before `-binarymonitor`, or the monitor never binds;
|
|
3
|
+
// - `-config` must come first: VICE's trunk (after 3.10) drops every argument
|
|
4
|
+
// before it, so the monitors would never start;
|
|
3
5
|
// - the monitor may accept a connection during startup and then drop it, so
|
|
4
6
|
// readiness is a connect plus a successful ping, retried until a deadline;
|
|
5
7
|
// - any monitor command stops the machine, so readiness ends with `exit`;
|
|
@@ -76,6 +78,9 @@ export function viceArguments(options: {
|
|
|
76
78
|
const headless = options.mode === "headless" ? ["-console", "-sounddev", "dummy"] : [];
|
|
77
79
|
return [
|
|
78
80
|
options.binary,
|
|
81
|
+
// VICE's trunk (after 3.10) drops every argument before -config, so it comes first.
|
|
82
|
+
"-config",
|
|
83
|
+
options.configFile,
|
|
79
84
|
"-default",
|
|
80
85
|
"-logfile",
|
|
81
86
|
options.logFile,
|
|
@@ -85,8 +90,6 @@ export function viceArguments(options: {
|
|
|
85
90
|
"-remotemonitor",
|
|
86
91
|
"-remotemonitoraddress",
|
|
87
92
|
`ip4://127.0.0.1:${options.textPort}`,
|
|
88
|
-
"-config",
|
|
89
|
-
options.configFile,
|
|
90
93
|
"-model",
|
|
91
94
|
options.videoStandard === "ntsc" ? "ntsc" : "c64",
|
|
92
95
|
"-drive8type",
|
|
@@ -94,6 +97,8 @@ export function viceArguments(options: {
|
|
|
94
97
|
// Autostart injects a PRG into RAM; the default mode copies it to a disk image first.
|
|
95
98
|
"-autostartprgmode",
|
|
96
99
|
"1",
|
|
100
|
+
// VICE's default waits a random time before an autostart types its LOAD, so two runs of one file differ (R2-12).
|
|
101
|
+
"+autostart-delay-random",
|
|
97
102
|
// The same RAM after every power-up: a hard reset is a power cycle, and VICE's default can flip
|
|
98
103
|
// random bits, so two runs from one start state could differ.
|
|
99
104
|
"-raminitstartvalue",
|
package/src/host/vice/screen.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { crc32, deflateSync } from "node:zlib";
|
|
5
5
|
|
|
6
6
|
import type { VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
+
import { FRAME_SIZE } from "../../protocol/vice.ts";
|
|
7
8
|
|
|
8
9
|
export interface IndexedFrame {
|
|
9
10
|
width: number;
|
|
@@ -44,8 +45,8 @@ export function decodeDisplay(body: Buffer): RawDisplay {
|
|
|
44
45
|
* offsets: PAL 384×272, NTSC 384×247.
|
|
45
46
|
*/
|
|
46
47
|
const VISIBLE: Record<VideoStandard, { dx: number; dy: number; width: number; height: number }> = {
|
|
47
|
-
pal: { dx: -32, dy: -36,
|
|
48
|
-
ntsc: { dx: -32, dy: -23,
|
|
48
|
+
pal: { dx: -32, dy: -36, ...FRAME_SIZE.pal },
|
|
49
|
+
ntsc: { dx: -32, dy: -23, ...FRAME_SIZE.ntsc },
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
/** Crops the visible bordered frame out of VICE's full raster buffer. */
|
package/src/host/vice/session.ts
CHANGED
|
@@ -125,6 +125,20 @@ export interface SessionOptions {
|
|
|
125
125
|
flaws?: readonly string[];
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Where VICE keeps a staged file: an autostarted disk stays in drive 8, a tape
|
|
130
|
+
* in the datasette and a cartridge in the port, until the same place gets
|
|
131
|
+
* another file. Only a program is read once.
|
|
132
|
+
*/
|
|
133
|
+
type StagedKind = "program" | "disk" | "tape" | "cartridge";
|
|
134
|
+
|
|
135
|
+
function stagedKind(type: string): StagedKind {
|
|
136
|
+
if (type === "crt") return "cartridge";
|
|
137
|
+
if (type === "tap" || type === "t64") return "tape";
|
|
138
|
+
if (type === "prg" || type === "p00") return "program";
|
|
139
|
+
return "disk";
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
export class ViceSession implements ViceSessionHandle {
|
|
129
143
|
/** The current VICE. A window move replaces it. */
|
|
130
144
|
#vice: ViceProcess;
|
|
@@ -179,8 +193,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
179
193
|
#timingStart = 0n;
|
|
180
194
|
/** Counts staged media files, for unique names. */
|
|
181
195
|
#staged = 0;
|
|
196
|
+
/** True after an autostart of a cartridge, until an autostart of another file detaches it. */
|
|
197
|
+
#cartridge = false;
|
|
182
198
|
/** The last staged file of each kind that VICE took; VICE can still read it. */
|
|
183
|
-
readonly #stagedFiles = new Map<
|
|
199
|
+
readonly #stagedFiles = new Map<StagedKind, string>();
|
|
184
200
|
/** Session-local ids: one counter for breakpoints and watchpoints, from 1. */
|
|
185
201
|
#nextPointId = 1;
|
|
186
202
|
/**
|
|
@@ -359,9 +375,27 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
359
375
|
case "autostart": {
|
|
360
376
|
const { type, index, run } = params as ViceOperations["autostart"]["params"];
|
|
361
377
|
const file = this.#stage(onlyAttachment(attachments), type);
|
|
378
|
+
// VICE keeps a cartridge through every reset and autostart, as a real C64 does.
|
|
379
|
+
if (type !== "crt" && this.#cartridge) {
|
|
380
|
+
await this.#observe(() => this.#machine.detachCartridge());
|
|
381
|
+
this.#cartridge = false;
|
|
382
|
+
this.#replaceStaged("cartridge", undefined);
|
|
383
|
+
}
|
|
384
|
+
// Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types (R3-05).
|
|
385
|
+
const centred: Array<1 | 2> = [];
|
|
386
|
+
for (const [port, joystick] of this.#joysticks) {
|
|
387
|
+
if (joystick.direction === "center" && !joystick.fire) continue;
|
|
388
|
+
const centre: JoystickState = { port, direction: "center", fire: false };
|
|
389
|
+
await this.#observe(() => this.#machine.setJoystick(centre));
|
|
390
|
+
this.#joysticks.set(port, centre);
|
|
391
|
+
centred.push(port);
|
|
392
|
+
}
|
|
362
393
|
await this.#runningCommand(() => this.#machine.autostart(file, index, run));
|
|
363
|
-
|
|
364
|
-
|
|
394
|
+
if (type === "crt") this.#cartridge = true;
|
|
395
|
+
this.#replaceStaged(stagedKind(type), file);
|
|
396
|
+
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
397
|
+
this.#timingStart = 0n;
|
|
398
|
+
return { state: "running", ...(type === "tap" && this.#inputFlaw !== undefined ? { emulatorFlaw: this.#inputFlaw } : {}), ...(centred.length === 0 ? {} : { joysticksCentred: centred }) };
|
|
365
399
|
}
|
|
366
400
|
case "diskAttach": {
|
|
367
401
|
const { type } = params as ViceOperations["diskAttach"]["params"];
|
|
@@ -473,7 +507,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
473
507
|
async #status(): Promise<MachineStatus> {
|
|
474
508
|
const warp = await this.#observe(() => this.#machine.warp());
|
|
475
509
|
// Read after #observe: it resumes a running machine, unless a breakpoint or a JAM stopped it meanwhile.
|
|
476
|
-
const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window" };
|
|
510
|
+
const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window", cartridge: this.#cartridge };
|
|
477
511
|
if (status.state === "stopped") status.pc = await this.#pc();
|
|
478
512
|
return status;
|
|
479
513
|
}
|
|
@@ -500,7 +534,11 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
500
534
|
}
|
|
501
535
|
if (params.action === "advance-frames") {
|
|
502
536
|
if (params.count === undefined) throw new WireFailure("invalid-input", "advance-frames needs count, the number of frames");
|
|
503
|
-
const
|
|
537
|
+
const raster = RASTER[this.#videoStandard];
|
|
538
|
+
if (params.anchor !== undefined && (params.anchor.line >= raster.lines || params.anchor.cycle >= raster.cycles)) {
|
|
539
|
+
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}.`);
|
|
540
|
+
}
|
|
541
|
+
const outcome = await this.#runFrames(params.count, undefined, params.anchor);
|
|
504
542
|
return { state: "stopped", pc: outcome.pc, advancedFrames: outcome.frames };
|
|
505
543
|
}
|
|
506
544
|
await this.#stop();
|
|
@@ -531,6 +569,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
531
569
|
|
|
532
570
|
async #reset(params: { mode: "soft" | "hard"; run: boolean }): Promise<{ state: RunState; pc?: number }> {
|
|
533
571
|
await this.#stop();
|
|
572
|
+
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
573
|
+
this.#timingStart = 0n;
|
|
534
574
|
if (params.run) {
|
|
535
575
|
await this.#runningCommand(() => this.#machine.reset(params.mode));
|
|
536
576
|
return { state: "running" };
|
|
@@ -560,10 +600,11 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
560
600
|
* checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
|
|
561
601
|
* The machine is stopped afterwards.
|
|
562
602
|
*/
|
|
563
|
-
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
|
|
603
|
+
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
|
|
564
604
|
await this.#stop();
|
|
565
605
|
const lines = RASTER[this.#videoStandard].lines;
|
|
566
|
-
|
|
606
|
+
// A frame ends where the raster passes the anchor; without one, where this run starts.
|
|
607
|
+
const start = anchor ?? (await this.#machine.rasterPosition());
|
|
567
608
|
const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
|
|
568
609
|
const ending = rasterWindowExpression(start.line, start.cycle, lines);
|
|
569
610
|
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
@@ -817,14 +858,21 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
817
858
|
}
|
|
818
859
|
|
|
819
860
|
/**
|
|
820
|
-
* Records the staged file that VICE took for `kind`,
|
|
821
|
-
* it replaces: VICE does not read that one again.
|
|
822
|
-
* take stays until the session closes, because a
|
|
861
|
+
* Records the staged file that VICE took for `kind`, or that it no longer
|
|
862
|
+
* has, and removes the file it replaces: VICE does not read that one again.
|
|
863
|
+
* A file that VICE did not take stays until the session closes, because a
|
|
864
|
+
* held VICE can still read it. So does a file that Windows does not let go.
|
|
823
865
|
*/
|
|
824
|
-
#replaceStaged(kind:
|
|
866
|
+
#replaceStaged(kind: StagedKind, file: string | undefined): void {
|
|
825
867
|
const previous = this.#stagedFiles.get(kind);
|
|
826
|
-
this.#stagedFiles.
|
|
827
|
-
|
|
868
|
+
if (file === undefined) this.#stagedFiles.delete(kind);
|
|
869
|
+
else this.#stagedFiles.set(kind, file);
|
|
870
|
+
if (previous === undefined || previous === file) return;
|
|
871
|
+
try {
|
|
872
|
+
rmSync(previous, { force: true });
|
|
873
|
+
} catch {
|
|
874
|
+
// The session directory goes when the session closes.
|
|
875
|
+
}
|
|
828
876
|
}
|
|
829
877
|
|
|
830
878
|
/** Loads a PRG without reset or start; finishes stopped. */
|
|
@@ -885,7 +933,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
885
933
|
async #cias(which: CiaSelection): Promise<CiaState[]> {
|
|
886
934
|
const ids: Array<1 | 2> = which === "both" ? [1, 2] : [Number(which) as 1 | 2];
|
|
887
935
|
const chips = [];
|
|
888
|
-
for (const id of ids)
|
|
936
|
+
for (const id of ids) {
|
|
937
|
+
const base = id === 1 ? 0xdc00 : 0xdd00;
|
|
938
|
+
chips.push(decodeCia(id, await this.#machine.readIo(base, 0x10), await this.#machine.ciaHidden(base)));
|
|
939
|
+
}
|
|
889
940
|
return chips;
|
|
890
941
|
}
|
|
891
942
|
|
|
@@ -1074,10 +1125,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1074
1125
|
if (baseline === undefined) throw new WireFailure("not-found", `There is no baseline named ${params.baseline}.`);
|
|
1075
1126
|
const current = await this.#observe(() => this.#machine.captureFrame(this.#videoStandard));
|
|
1076
1127
|
const difference = compareFrames(baseline.frame, current.frame, params.mask);
|
|
1077
|
-
|
|
1128
|
+
if (difference.comparedPixels === 0) throw new WireFailure("invalid-input", "The mask covers the whole screen: nothing to compare.");
|
|
1129
|
+
const mismatchRatio = difference.mismatchingPixels / difference.comparedPixels;
|
|
1078
1130
|
const result: ScreenComparison = {
|
|
1079
1131
|
match: mismatchRatio <= params.maxMismatchRatio,
|
|
1080
1132
|
mismatchingPixels: difference.mismatchingPixels,
|
|
1133
|
+
comparedPixels: difference.comparedPixels,
|
|
1081
1134
|
mismatchRatio,
|
|
1082
1135
|
};
|
|
1083
1136
|
if (difference.bounds !== undefined) result.bounds = difference.bounds;
|
|
@@ -36,10 +36,20 @@ export const RUN_LIMIT_FRAMES = 300;
|
|
|
36
36
|
*/
|
|
37
37
|
export async function runProgram(client: ViceSessionClient, path: string, prg: Uint8Array): Promise<"not-loaded" | "not-started" | undefined> {
|
|
38
38
|
if (!(await autostartLoaded(client, path, prg))) return "not-loaded";
|
|
39
|
+
return (await typeRun(client)) ? undefined : "not-started";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Types RUN at the READY prompt and stops where BASIC starts the first line,
|
|
44
|
+
* before its SYS. Every tool that starts a loaded program this way reaches
|
|
45
|
+
* its entry at the same raster line and timer values (D62). False when BASIC
|
|
46
|
+
* runs no line within RUN_LIMIT_FRAMES.
|
|
47
|
+
*/
|
|
48
|
+
export async function typeRun(client: ViceSessionClient): Promise<boolean> {
|
|
39
49
|
await client.keyboard(textToPetscii("RUN\n"));
|
|
40
50
|
const started = await client.runUntil({
|
|
41
51
|
target: { kind: "memory", address: CURLIN_HIGH, operator: "ne", value: 0xff, space: "c64", view: "ram" },
|
|
42
52
|
timeoutFrames: RUN_LIMIT_FRAMES,
|
|
43
53
|
});
|
|
44
|
-
return started.reached
|
|
54
|
+
return started.reached;
|
|
45
55
|
}
|
|
@@ -4,7 +4,7 @@ import { connect as connectTcp, type Socket } from "node:net";
|
|
|
4
4
|
|
|
5
5
|
import { encodeFrame, MessageReader } from "../protocol/framing.ts";
|
|
6
6
|
import { elapsedMs, trace } from "../trace.ts";
|
|
7
|
-
import { DEFAULT_HOST_PORT, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, HOST_PROTOCOL_ID, HOST_PROTOCOL_VERSION, parseHostMessage, ProtocolError, WireFailure, withStopAndAsk, type
|
|
7
|
+
import { DEFAULT_HOST_PORT, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, HOST_PROTOCOL_ID, HOST_PROTOCOL_VERSION, parseHostMessage, ProtocolError, WireFailure, withStopAndAsk, type Hello, type HostMessage, type Ping, type Request, type Role, type VideoStandard } from "../protocol/messages.ts";
|
|
8
8
|
import { PACKAGE } from "../package.ts";
|
|
9
9
|
|
|
10
10
|
export interface Endpoint {
|
|
@@ -37,19 +37,19 @@ export function hostEndpoints(env: NodeJS.ProcessEnv = process.env): Endpoint[]
|
|
|
37
37
|
return ["127.0.0.1", "host.docker.internal", "host.containers.internal"].map((host) => ({ host, port: DEFAULT_HOST_PORT }));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/** A host that does not run or cannot be reached is machine-unavailable for every role (D37). */
|
|
41
|
+
function unreachable(): WireFailure {
|
|
42
42
|
return new WireFailure(
|
|
43
|
-
|
|
43
|
+
"machine-unavailable",
|
|
44
44
|
withStopAndAsk(
|
|
45
45
|
`The c64-re-tools host runtime is not running or cannot be reached. Start it on the host (npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host) and try again.`,
|
|
46
46
|
),
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function stopped(
|
|
50
|
+
function stopped(): WireFailure {
|
|
51
51
|
return new WireFailure(
|
|
52
|
-
|
|
52
|
+
"machine-unavailable",
|
|
53
53
|
"The connection to the c64-re-tools host runtime stopped before the host runtime was ready.",
|
|
54
54
|
);
|
|
55
55
|
}
|
|
@@ -146,9 +146,9 @@ export class HostConnection {
|
|
|
146
146
|
}
|
|
147
147
|
if (signal?.aborted) {
|
|
148
148
|
socket?.destroy();
|
|
149
|
-
throw stopped(
|
|
149
|
+
throw stopped();
|
|
150
150
|
}
|
|
151
|
-
if (socket === undefined) throw unreachable(
|
|
151
|
+
if (socket === undefined) throw unreachable();
|
|
152
152
|
|
|
153
153
|
const connection = new HostConnection(socket);
|
|
154
154
|
const hello: Hello = { type: "hello", protocol: HOST_PROTOCOL_ID, version: HOST_PROTOCOL_VERSION, role: options.role };
|
|
@@ -172,7 +172,7 @@ export class HostConnection {
|
|
|
172
172
|
signal?.removeEventListener("abort", stop);
|
|
173
173
|
if (signal?.aborted) {
|
|
174
174
|
await connection.close();
|
|
175
|
-
throw stopped(
|
|
175
|
+
throw stopped();
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
if (!(answer instanceof Error) && answer.type === "ready") {
|
|
@@ -190,7 +190,7 @@ export class HostConnection {
|
|
|
190
190
|
);
|
|
191
191
|
}
|
|
192
192
|
throw new WireFailure(
|
|
193
|
-
|
|
193
|
+
"machine-unavailable",
|
|
194
194
|
"The c64-re-tools host runtime did not finish the handshake. Check that it is running and try again.",
|
|
195
195
|
);
|
|
196
196
|
}
|
|
@@ -9,23 +9,32 @@ import type { ViceSessionClient } from "./vice-session.ts";
|
|
|
9
9
|
/** A full coverage file: offset n is address n (D40). */
|
|
10
10
|
export const FULL_COVERAGE = 0x10000;
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
export function readCoverageFiles(paths: string[], fail: (message: string) => never): Uint8Array | undefined {
|
|
17
|
-
if (paths.length === 0) return undefined;
|
|
18
|
-
const joined = new Uint8Array(FULL_COVERAGE);
|
|
19
|
-
for (const path of paths) {
|
|
12
|
+
/** Reads full coverage files from the project, one map per file. `fail` refuses a file of another size. */
|
|
13
|
+
export function readCoverageMaps(paths: string[], fail: (message: string) => never): Uint8Array[] {
|
|
14
|
+
return paths.map((path) => {
|
|
20
15
|
const { bytes } = readProjectFile(path);
|
|
21
16
|
if (bytes.length !== FULL_COVERAGE) {
|
|
22
17
|
fail(`--coverage ${path} has ${bytes.length} bytes; a full coverage file has 65536, one for each address. Write it with c64_memmap action read and out, without start and end.`);
|
|
23
18
|
}
|
|
24
|
-
|
|
25
|
-
}
|
|
19
|
+
return bytes;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Joins coverage maps: an address has every flag that any run gave it. */
|
|
24
|
+
export function joinCoverage(maps: readonly Uint8Array[]): Uint8Array {
|
|
25
|
+
const joined = new Uint8Array(FULL_COVERAGE);
|
|
26
|
+
for (const map of maps) for (let address = 0; address < FULL_COVERAGE; address++) joined[address]! |= map[address]!;
|
|
26
27
|
return joined;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Reads full coverage files from the project and joins them: an address has
|
|
32
|
+
* every flag that any run gave it. `fail` refuses a file of another size.
|
|
33
|
+
*/
|
|
34
|
+
export function readCoverageFiles(paths: string[], fail: (message: string) => never): Uint8Array | undefined {
|
|
35
|
+
return paths.length === 0 ? undefined : joinCoverage(readCoverageMaps(paths, fail));
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
/** The coverage bytes of start..end, read from the session in parts. */
|
|
30
39
|
export async function readCoverage(session: Pick<ViceSessionClient, "memmap">, start = 0x0000, end = 0xffff): Promise<Uint8Array> {
|
|
31
40
|
const map = new Uint8Array(end - start + 1);
|
package/src/host-client/tools.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// ACME, DXA and Ghidra themselves through src/native.
|
|
5
5
|
|
|
6
6
|
import { ProtocolError, WireFailure } from "../protocol/messages.ts";
|
|
7
|
-
import { DISK_IMAGE_TYPES, validateToolResult, type C1541Result, type DiskAction, type DiskImageType, type PetcatResult, type ToolOperation, type ToolOperations, type ToolStatus } from "../protocol/tools.ts";
|
|
7
|
+
import { DISK_IMAGE_TYPES, validateToolResult, type C1541Result, type DiskAction, type DiskImageType, type PetcatResult, type ToolOperation, type ToolOperations, type HostInfo, type ToolStatus } from "../protocol/tools.ts";
|
|
8
8
|
import { HostConnection } from "./connect.ts";
|
|
9
9
|
import { readProjectFile } from "./transfer.ts";
|
|
10
10
|
|
|
@@ -43,7 +43,7 @@ export async function callTool<O extends ToolOperation>(
|
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
void connection.closed.then(() =>
|
|
46
|
-
reject(new WireFailure("
|
|
46
|
+
reject(new WireFailure("machine-unavailable", "The connection to the c64-re-tools host runtime was lost during the request. Check that the Host Runtime (c64-re-tools-host) is running.")),
|
|
47
47
|
);
|
|
48
48
|
connection.send({ type: "request", id: 1, op, params }, attachments);
|
|
49
49
|
});
|
|
@@ -78,12 +78,12 @@ export async function decodeBasic(request: { program: string }, options: ToolCal
|
|
|
78
78
|
return (await callTool("petcat.decode", {}, [file.bytes], options)).result;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** host.status: each native tool on the host, found and run once. */
|
|
82
|
-
export async function hostTools(options: ToolCallOptions = {}): Promise<ToolStatus[]> {
|
|
83
|
-
return (await callTool("host.status", {}, [], options)).result
|
|
81
|
+
/** host.status: the Host Runtime, and each native tool on the host, found and run once. */
|
|
82
|
+
export async function hostTools(options: ToolCallOptions = {}): Promise<{ host: HostInfo; tools: ToolStatus[] }> {
|
|
83
|
+
return (await callTool("host.status", {}, [], options)).result;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/** Skill scripts see failures and wire limits through the host-client, never the private protocol module. */
|
|
87
87
|
export { WireFailure } from "../protocol/messages.ts";
|
|
88
88
|
export { DISK_ACTIONS } from "../protocol/tools.ts";
|
|
89
|
-
export { JOYSTICK_DIRECTIONS, MAX_BASELINES, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES } from "../protocol/vice.ts";
|
|
89
|
+
export { JOYSTICK_DIRECTIONS, MAX_BASELINES, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES, maskCoversFrame } from "../protocol/vice.ts";
|
|
@@ -134,7 +134,7 @@ export class ViceSessionClient {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/** Autostarts a project program or image, as VICE's autostart does. */
|
|
137
|
-
async autostart(params: { path: string; index: number; run: boolean }): Promise<{ state: RunState }> {
|
|
137
|
+
async autostart(params: { path: string; index: number; run: boolean }): Promise<{ state: RunState; emulatorFlaw?: string }> {
|
|
138
138
|
const file = readProjectFile(params.path);
|
|
139
139
|
return this.#request("autostart", { type: file.type, index: params.index, run: params.run }, [file.bytes]);
|
|
140
140
|
}
|
package/src/knowledge/schema.ts
CHANGED
|
@@ -45,6 +45,13 @@ export type SuppressionEntity = (typeof SUPPRESSION_ENTITIES)[number];
|
|
|
45
45
|
|
|
46
46
|
/** A module file of the generated source (D48): a plain name that ends in .asm, no directory. */
|
|
47
47
|
export const MODULE_FILE = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,59}\.asm$/;
|
|
48
|
+
/** Files that c64-source writes itself: no module can have these names. */
|
|
49
|
+
export const GENERATED_FILES = ["main.asm", "loader.asm", "constants.asm"] as const;
|
|
50
|
+
|
|
51
|
+
/** True for a generated file name in any letter case: the file systems of macOS and Windows ignore case. */
|
|
52
|
+
export function isGeneratedFile(file: string): boolean {
|
|
53
|
+
return (GENERATED_FILES as readonly string[]).includes(file.toLowerCase());
|
|
54
|
+
}
|
|
48
55
|
|
|
49
56
|
/**
|
|
50
57
|
* The address space of a knowledge row (D53): the C64, the 1541 drive, or a
|
package/src/knowledge/write.ts
CHANGED
|
@@ -41,6 +41,8 @@ import {
|
|
|
41
41
|
MAX_ELEMENT,
|
|
42
42
|
MODULE_FILE,
|
|
43
43
|
DEFAULT_SPACE,
|
|
44
|
+
GENERATED_FILES,
|
|
45
|
+
isGeneratedFile,
|
|
44
46
|
SPACE,
|
|
45
47
|
NO_ELEMENT_TYPES,
|
|
46
48
|
REFERENCE_KINDS,
|
|
@@ -431,6 +433,9 @@ function checkModule(request: ModuleRequest): void {
|
|
|
431
433
|
checkSpace(request);
|
|
432
434
|
checkAddress(request.start, "start");
|
|
433
435
|
if (!MODULE_FILE.test(request.file)) throw new KnowledgeError("invalid-input", "A module file is a name of letters, digits, underscores or hyphens (at most 60) that ends in .asm, without a directory.");
|
|
436
|
+
if (isGeneratedFile(request.file)) {
|
|
437
|
+
throw new KnowledgeError("invalid-input", `c64-source writes ${GENERATED_FILES.join(", ")} itself, in any letter case, so a module cannot have the file ${request.file}. Give the module another file.`);
|
|
438
|
+
}
|
|
434
439
|
if (request.title !== undefined && (request.title.trim().length === 0 || request.title.length > MAX_COMMENT)) throw new KnowledgeError("invalid-input", `A module title has 1 to ${MAX_COMMENT} characters.`);
|
|
435
440
|
}
|
|
436
441
|
|
package/src/mcp/tools/debug.ts
CHANGED
|
@@ -41,7 +41,8 @@ export const c64Breakpoint = defineTool({
|
|
|
41
41
|
description:
|
|
42
42
|
"Manage breakpoints. A breakpoint stops the CPU when it is about to execute the instruction at an address. " +
|
|
43
43
|
"Action add sets one at address in space c64 or drive8. With a condition, it stops only when the condition is true. " +
|
|
44
|
-
"add returns the new id.
|
|
44
|
+
"add returns the new id. Breakpoints and watchpoints take their ids from one counter, so the ids of one kind can have gaps. " +
|
|
45
|
+
"Actions remove, enable and disable take that id. remove returns removed true and the id. Action list gives all breakpoints. Each breakpoint shows its condition, when it has one. " +
|
|
45
46
|
"Breakpoints stay through a reset.",
|
|
46
47
|
inputSchema: z
|
|
47
48
|
.object({
|
|
@@ -84,7 +85,8 @@ export const c64Watchpoint = defineTool({
|
|
|
84
85
|
"Manage watchpoints. A watchpoint stops the CPU when an instruction reads or writes memory in a range. " +
|
|
85
86
|
`Action add sets one at address for size bytes (1 to ${MAX_WATCH_SIZE}, default 1), with access read, write or read-write. ` +
|
|
86
87
|
"With a condition, it stops only when the condition is true. For a write, a memory condition sees the memory after the write. " +
|
|
87
|
-
"add returns the new id.
|
|
88
|
+
"add returns the new id. Breakpoints and watchpoints take their ids from one counter, so the ids of one kind can have gaps. " +
|
|
89
|
+
"Actions remove, enable and disable take that id. remove returns removed true and the id. Action list gives all watchpoints. Each watchpoint shows its condition, when it has one.",
|
|
88
90
|
inputSchema: z
|
|
89
91
|
.object({
|
|
90
92
|
action: z.enum(CHECKPOINT_ACTIONS),
|
|
@@ -129,7 +131,7 @@ export const c64CpuHistory = defineTool({
|
|
|
129
131
|
description:
|
|
130
132
|
"Get the last instructions that the CPU executed, oldest first. " +
|
|
131
133
|
`limit is the number of instructions (1 to ${MAX_HISTORY}, default 50). ` +
|
|
132
|
-
"Each entry gives the address, the bytes, the instruction and the registers a, x, y and sp. " +
|
|
134
|
+
"Each entry gives the address, the bytes, the instruction and the registers a, x, y and sp before the instruction executed. " +
|
|
133
135
|
"In space c64, it also gives the raster line and cycle when the instruction started. " +
|
|
134
136
|
"With out, it writes the entries as text lines to that project file, and the result gives the file in place of the entries. " +
|
|
135
137
|
"This does not change the run state of the machine.",
|
|
@@ -191,7 +193,8 @@ export const c64Timing = defineTool({
|
|
|
191
193
|
title: "Cycle stopwatch",
|
|
192
194
|
description:
|
|
193
195
|
"A stopwatch that counts C64 CPU cycles. Action start sets it to zero. Action read gives the cycles since the last start, " +
|
|
194
|
-
"or since the session started, as a decimal string. A
|
|
196
|
+
"or since the session started, as a decimal string. A reset and c64_autostart start it again from the reset. " +
|
|
197
|
+
"A PAL frame has 19656 cycles and an NTSC frame 17095.",
|
|
195
198
|
inputSchema: z.object({ action: z.enum(TIMING_ACTIONS) }).strict(),
|
|
196
199
|
outputSchema: z.object({ started: z.boolean().optional(), cycles: z.string().regex(/^\d+$/).optional() }),
|
|
197
200
|
readOnly: false,
|
|
@@ -204,7 +207,8 @@ export const c64Profile = defineTool({
|
|
|
204
207
|
name: "c64_profile",
|
|
205
208
|
title: "Cycle profile",
|
|
206
209
|
description:
|
|
207
|
-
"Get the routines where the C64 CPU spent the most cycles since the session started. " +
|
|
210
|
+
"Get the routines where the C64 CPU spent the most cycles since the session started or since the last c64_window. " +
|
|
211
|
+
"After c64_window or a snapshot restore, the emulator can give the cycles to a routine that ran before the restore, for example $fd50. Do not trust the routine addresses then. " +
|
|
208
212
|
`limit is the maximum number of routines (1 to ${MAX_PROFILE}, default 20). ` +
|
|
209
213
|
"Each entry gives the routine address, its total cycles with the routines it calls, and its own cycles. " +
|
|
210
214
|
"percent is the part of all cycles that the routine used itself. " +
|