@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.
Files changed (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -1,9 +1,15 @@
1
1
  // host.status: VICE and the tools that come with it, each found the way its
2
2
  // operations find it and run once with a version option. A tool from a
3
3
  // VICE older than MIN_VICE is reported as not working, as its operations refuse it.
4
+ // VICE itself is reported with the version that the host read from the
5
+ // running emulator at start: the Homebrew x64sc of VICE 3.10 on macOS stops
6
+ // on -version ("argv[0] is NULL") although the emulator runs.
4
7
 
5
- import type { ToolStatus } from "../../protocol/tools.ts";
6
- import { C1541, findTool, isOlderThanMinimum, MIN_VICE, PETCAT, VICE, viceVersionOf, type ToolSpec } from "../../native/discover.ts";
8
+ import { arch, platform, release } from "node:os";
9
+
10
+ import { PACKAGE } from "../../package.ts";
11
+ import type { HostInfo, ToolStatus } from "../../protocol/tools.ts";
12
+ import { C1541, findTool, isOlderThanMinimum, MIN_VICE, PETCAT, VICE, viceFlaws, viceVersionOf, type ToolSpec, type ViceVersion } from "../../native/discover.ts";
7
13
  import type { ToolContext } from "../../native/run.ts";
8
14
  import { probeTools } from "../../native/status.ts";
9
15
  import { findVice } from "../vice/process.ts";
@@ -16,15 +22,26 @@ function requireMinimum(status: ToolStatus, tool: ToolSpec): ToolStatus {
16
22
  return { ...status, runs: false, problem: `${found}, and c64-re-tools needs VICE ${MIN_VICE.major}.${MIN_VICE.minor} or later. ${tool.remedy}` };
17
23
  }
18
24
 
19
- export async function hostStatus(context: ToolContext): Promise<{ result: { tools: ToolStatus[] } }> {
25
+ /** The version text of a VICE version, as x64sc -version prints it. */
26
+ const versionText = (version: ViceVersion) => `x64sc (VICE ${version.major}.${version.minor}${version.revision === undefined ? "" : ` SVN r${version.revision}`})`;
27
+
28
+ /** `started`: the version that the host read from the emulator at start; without it, VICE is probed with -version too. */
29
+ export async function hostStatus(context: ToolContext, started?: ViceVersion): Promise<{ result: { host: HostInfo; tools: ToolStatus[] } }> {
20
30
  const specs = [VICE, C1541, PETCAT];
21
- const tools = await probeTools(
31
+ const probed = await probeTools(
22
32
  [
23
- { name: VICE.name, find: findVice, args: ["-version"], pattern: /^x64sc \(VICE \d/ },
33
+ ...(started === undefined ? [{ name: VICE.name, find: findVice, args: ["-version"], pattern: /^x64sc \(VICE \d/ }] : []),
24
34
  { name: C1541.name, find: (env) => findTool(C1541, env), args: ["-version"], pattern: /^c1541 \(VICE \d/ },
25
35
  { name: PETCAT.name, find: (env) => findTool(PETCAT, env), args: ["-version"], pattern: /^petcat \(VICE \d/ },
26
36
  ],
27
37
  context,
28
38
  );
29
- return { result: { tools: tools.map((status, index) => requireMinimum(status, specs[index]!)) } };
39
+ const tools = started === undefined ? probed : [{ name: VICE.name, found: true, path: findVice(context.env ?? process.env), version: versionText(started), runs: true }, ...probed];
40
+ const checked = tools.map((status, index) => requireMinimum(status, specs[index]!));
41
+ // VICE runs with its known flaws; status names them (D56).
42
+ const vice = checked[0]!;
43
+ const version = vice.runs ? viceVersionOf(vice.version ?? "") : undefined;
44
+ const flaws = version === undefined ? [] : viceFlaws(version);
45
+ if (flaws.length > 0) checked[0] = { ...vice, flaws };
46
+ return { result: { host: { version: PACKAGE.version, platform: platform(), release: release(), arch: arch() }, tools: checked } };
30
47
  }
@@ -3,9 +3,11 @@
3
3
 
4
4
  import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
5
5
  import { type Comparison, type Condition, type BacktraceFrame, type HistoryEntry, type Instruction, type MemmapRange, type ProfileEntry, type JoystickState, type MemoryView, type RegisterValues, type Registers, type Space } from "../../protocol/vice.ts";
6
- import { formatC64Address, formatC64Byte } from "../../c64.ts";
7
- import { decodeDisplay, decodePalette, visibleFrame, type IndexedFrame } from "./screen.ts";
6
+ import { COVERAGE, formatC64Address, formatC64Byte } from "../../c64.ts";
7
+ import type { IndexedFrame } from "../../graphics.ts";
8
+ import { decodeDisplay, decodePalette, visibleFrame } from "./screen.ts";
8
9
  import type { TextMonitor } from "./text-monitor.ts";
10
+ import type { CiaHidden } from "./chips.ts";
9
11
  import {
10
12
  BinaryMonitor,
11
13
  Command,
@@ -135,7 +137,7 @@ export function parseDisassembly(answer: string, start: number): Instruction[] {
135
137
  for (const line of answer.split("\n")) {
136
138
  const match = /^\.(?:C|\d+):([0-9a-f]{4})\s+((?:[0-9a-f]{2} )*[0-9a-f]{2})\s+(.*?)\s*$/i.exec(line);
137
139
  if (match === null) {
138
- if (line.trim() !== "") unknown.push(line.trim());
140
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
139
141
  continue;
140
142
  }
141
143
  const address = Number.parseInt(match[1]!, 16);
@@ -164,7 +166,7 @@ export function parseHistory(answer: string): Array<Omit<HistoryEntry, "rasterLi
164
166
  line,
165
167
  );
166
168
  if (match === null) {
167
- if (line.trim() !== "") unknown.push(line.trim());
169
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
168
170
  continue;
169
171
  }
170
172
  entries.push({
@@ -193,7 +195,7 @@ export function parseHunt(answer: string, limit: number): number[] {
193
195
  for (const line of answer.split("\n")) {
194
196
  const match = /^([0-9a-f]{4})$/i.exec(line.trim());
195
197
  if (match === null) {
196
- if (line.trim() !== "") unknown.push(line.trim());
198
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
197
199
  continue;
198
200
  }
199
201
  if (matches.length < limit) matches.push(Number.parseInt(match[1]!, 16));
@@ -236,14 +238,15 @@ export function parseProfile(answer: string): ProfileEntry[] {
236
238
  const unknown: string[] = [];
237
239
  for (const line of answer.split("\n")) {
238
240
  // Counts can carry the host locale's digit grouping ("1,123,200") and the percent a decimal comma.
239
- const row = new RegExp(`^\\s*(${COUNT})\\s+[\\d.,]+%\\s+(${COUNT})\\s+([\\d.,]+)%\\s+(\\S+)\\s*$`).exec(line);
241
+ // A profile of zero cycles, as after a window move restarts it, prints its percents as "-nan".
242
+ const row = new RegExp(`^\\s*(${COUNT})\\s+(?:[\\d.,]+|-?nan)%\\s+(${COUNT})\\s+([\\d.,]+|-?nan)%\\s+(\\S+)\\s*$`, "i").exec(line);
240
243
  if (row === null) {
241
- if (line.trim() !== "" && !/^Total\s+%\s+Self\s+%$/.test(line.trim()) && !/^[-\s]+$/.test(line)) unknown.push(line.trim());
244
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim()) && !/^Total\s+%\s+Self\s+%$/.test(line.trim()) && !/^[-\s]+$/.test(line)) unknown.push(line.trim());
242
245
  continue;
243
246
  }
244
247
  // A row named ROOT or by a label has no routine address.
245
248
  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(",", ".")) });
249
+ 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
250
  }
248
251
  // An answer in another form must not read as "nothing ran".
249
252
  if (entries.length === 0 && unknown.length > 0) throw unknownForm("profile", unknown);
@@ -258,33 +261,48 @@ function unknownForm(what: string, lines: string[]): WireFailure {
258
261
  return new WireFailure("operation-failed", `VICE printed the ${what} in a form that c64-re-tools does not read: ${JSON.stringify(lines.slice(0, 3))}`);
259
262
  }
260
263
 
264
+ /** The coverage byte (D40) of one memmap row's I/O, ROM and RAM columns, each "rwx" with "-" for no access. */
265
+ export function memmapFlags(io: string, rom: string, ram: string): number {
266
+ let flags = 0;
267
+ if (ram[0] === "r") flags |= COVERAGE.ramRead;
268
+ if (ram[1] === "w") flags |= COVERAGE.ramWrite;
269
+ if (ram[2] === "x") flags |= COVERAGE.ramExecute;
270
+ if (rom[0] === "r") flags |= COVERAGE.romRead;
271
+ if (rom[2] === "x") flags |= COVERAGE.romExecute;
272
+ if (io[0] === "r") flags |= COVERAGE.ioRead;
273
+ if (io[1] === "w") flags |= COVERAGE.ioWrite;
274
+ if (io[2] === "x") flags |= COVERAGE.ioExecute;
275
+ return flags;
276
+ }
277
+
261
278
  /**
262
279
  * Parses memmap rows such as "00c2: --- --- rw- (uninitialized read)": the I/O,
263
280
  * ROM and RAM access flags of one address. Adjacent addresses with the same
264
- * combined flags merge into one range; at most `maxRanges` come back.
281
+ * flags merge into one range; at most `maxRanges` come back. When more ranges
282
+ * follow, `next` is the first touched address that is not reported.
265
283
  */
266
- export function parseMemmap(answer: string, maxRanges: number): MemmapRange[] {
284
+ export function parseMemmap(answer: string, maxRanges: number): { ranges: MemmapRange[]; next?: number } {
267
285
  const ranges: MemmapRange[] = [];
268
286
  const unknown: string[] = [];
269
287
  for (const line of answer.split("\n")) {
270
288
  const match = /^([0-9a-f]{4}): (\S{3}) (\S{3}) (\S{3})/i.exec(line);
271
289
  if (match === null) {
272
- if (line.trim() !== "" && !/^addr:\s+IO\s+ROM\s+RAM$/.test(line.trim())) unknown.push(line.trim());
290
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim()) && !/^addr:\s+IO\s+ROM\s+RAM$/.test(line.trim())) unknown.push(line.trim());
273
291
  continue;
274
292
  }
275
293
  const address = Number.parseInt(match[1]!, 16);
276
- const flags = match[2]! + match[3]! + match[4]!;
277
- const access = { execute: flags.includes("x"), read: flags.includes("r"), write: flags.includes("w") };
294
+ const flags = memmapFlags(match[2]!, match[3]!, match[4]!);
295
+ if (flags === 0) continue;
278
296
  const last = ranges.at(-1);
279
- if (last !== undefined && last.end === address - 1 && last.execute === access.execute && last.read === access.read && last.write === access.write) {
297
+ if (last !== undefined && last.end === address - 1 && last.flags === flags) {
280
298
  last.end = address;
281
299
  continue;
282
300
  }
283
- if (ranges.length >= maxRanges) break;
284
- ranges.push({ start: address, end: address, ...access });
301
+ if (ranges.length >= maxRanges) return { ranges, next: address };
302
+ ranges.push({ start: address, end: address, flags });
285
303
  }
286
304
  if (ranges.length === 0 && unknown.length > 0) throw unknownForm("memory map", unknown);
287
- return ranges;
305
+ return { ranges };
288
306
  }
289
307
 
290
308
  /**
@@ -293,12 +311,19 @@ export function parseMemmap(answer: string, maxRanges: number): MemmapRange[] {
293
311
  */
294
312
  const STOP_LINE = /^(?:#\d+ \((?:Stop on|Trace)\s|\.(?:C|\d+):[0-9a-f]{4}\s)/i;
295
313
 
296
- /** The lines of a text command's answer that are not blank, not a stop line and not `known`. */
314
+ /**
315
+ * The line VICE prints on the text monitor when the binary monitor sets a
316
+ * checkpoint condition. It can come late, in the answer of the next text
317
+ * command (seen on Windows).
318
+ */
319
+ const CONDITION_LINE = /^Setting checkpoint \d+ condition to: /;
320
+
321
+ /** The lines of a text command's answer that are not blank, not a stop line, not a condition line and not `known`. */
297
322
  function otherLines(answer: string, known?: RegExp): string[] {
298
323
  return answer
299
324
  .split("\n")
300
325
  .map((line) => line.trim())
301
- .filter((line) => line !== "" && !STOP_LINE.test(line) && known?.test(line) !== true);
326
+ .filter((line) => line !== "" && !STOP_LINE.test(line) && !CONDITION_LINE.test(line) && known?.test(line) !== true);
302
327
  }
303
328
 
304
329
  /** Refuses the answer of a text command that prints nothing, or only `known` lines, when it works. */
@@ -408,6 +433,19 @@ export class ViceAdapter {
408
433
  return bytes.toString("hex");
409
434
  }
410
435
 
436
+ /**
437
+ * The timer latches and the interrupt mask of the CIA at `base`, from VICE's
438
+ * text-monitor dump: no register read gives them (D63).
439
+ */
440
+ async ciaHidden(base: 0xdc00 | 0xdd00): Promise<CiaHidden> {
441
+ const answer = await this.#computerText(`io ${hex4(base)}`);
442
+ const mask = /\(mask: ([0-9a-f]{2})\)/i.exec(answer);
443
+ const latchA = /Timer A: [0-9a-f]{4} \(latched ([0-9a-f]{4})\)/i.exec(answer);
444
+ const latchB = /Timer B: [0-9a-f]{4} \(latched ([0-9a-f]{4})\)/i.exec(answer);
445
+ if (mask === null || latchA === null || latchB === null) throw unknownForm("CIA dump", answer.split("\n"));
446
+ return { latchA: Number.parseInt(latchA[1]!, 16), latchB: Number.parseInt(latchB[1]!, 16), interruptMask: Number.parseInt(mask[1]!, 16) & 0x1f };
447
+ }
448
+
411
449
  /** Reads C64 I/O registers through the I/O bank, whatever the CPU has banked in, without side effects. */
412
450
  async readIo(start: number, size: number): Promise<Uint8Array> {
413
451
  const response = await this.#monitor.request(
@@ -616,6 +654,12 @@ export class ViceAdapter {
616
654
  await this.#monitor.request(Command.autostart, Buffer.concat([head, name]));
617
655
  }
618
656
 
657
+ /** Detaches every cartridge. VICE powers the cartridge off; the C64 keeps running. */
658
+ async detachCartridge(): Promise<void> {
659
+ // Device 32 is the cartridge port; the monitor reads numbers as hex. VICE prints nothing when it detaches.
660
+ expectQuiet("cartridge detach answer", await this.#text.command("detach 20"));
661
+ }
662
+
619
663
  /** Attaches a disk image file from the host file system to drive 8. */
620
664
  async attachDisk(file: string): Promise<void> {
621
665
  const answer = await this.#text.command(`attach ${quoted(file)} 8`);
@@ -739,7 +783,7 @@ export class ViceAdapter {
739
783
  }
740
784
 
741
785
  /** How the C64 CPU accessed [start, end] since the memmap was last cleared, as merged ranges. */
742
- async memmap(start: number, end: number, maxRanges: number): Promise<MemmapRange[]> {
786
+ async memmap(start: number, end: number, maxRanges: number): Promise<{ ranges: MemmapRange[]; next?: number }> {
743
787
  // The first argument is the access mask ("ioRWXrwx"); ff shows every kind of access.
744
788
  return parseMemmap(await this.#computerText(`mmsh ff ${hex4(start)} ${hex4(end)}`, 60_000), maxRanges);
745
789
  }
@@ -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),
@@ -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`;
@@ -9,7 +11,8 @@
9
11
  // operations only it offers; both act on the same machine;
10
12
  // - autostart of a PRG needs `-autostartprgmode 1` (inject into RAM);
11
13
  // - the joyport command reaches the C64 only through the "Joyport I/O
12
- // simulation" device (37), whose lines start all pressed until set;
14
+ // simulation" device (37). In VICE 3.10 and older it holds bits 5-7 of
15
+ // both CIA 1 ports low (VICE bug #2202, a known flaw named by D56);
13
16
  // - with the default JAM action a jammed CPU hangs silently; -jamaction 2
14
17
  // enters the monitor instead;
15
18
  // - the monitor's profiler prints numbers in the host locale, so VICE runs
@@ -29,7 +32,7 @@ import { setTimeout as sleep } from "node:timers/promises";
29
32
 
30
33
  import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
31
34
  import { elapsedMs, trace } from "../../trace.ts";
32
- import { findTool, isOlderThanMinimum, MIN_VICE, VICE } from "../../native/discover.ts";
35
+ import { findTool, isOlderThanMinimum, MIN_VICE, VICE, type ViceVersion } from "../../native/discover.ts";
33
36
  import type { ExitStatus, ProcessSupervisor, SupervisedProcess } from "../../native/processes.ts";
34
37
  import { BinaryMonitor, Command, decodeMemory, memoryGetBody } from "./binary-monitor.ts";
35
38
  import { TextMonitor } from "./text-monitor.ts";
@@ -75,6 +78,9 @@ export function viceArguments(options: {
75
78
  const headless = options.mode === "headless" ? ["-console", "-sounddev", "dummy"] : [];
76
79
  return [
77
80
  options.binary,
81
+ // VICE's trunk (after 3.10) drops every argument before -config, so it comes first.
82
+ "-config",
83
+ options.configFile,
78
84
  "-default",
79
85
  "-logfile",
80
86
  options.logFile,
@@ -84,8 +90,6 @@ export function viceArguments(options: {
84
90
  "-remotemonitor",
85
91
  "-remotemonitoraddress",
86
92
  `ip4://127.0.0.1:${options.textPort}`,
87
- "-config",
88
- options.configFile,
89
93
  "-model",
90
94
  options.videoStandard === "ntsc" ? "ntsc" : "c64",
91
95
  "-drive8type",
@@ -93,6 +97,8 @@ export function viceArguments(options: {
93
97
  // Autostart injects a PRG into RAM; the default mode copies it to a disk image first.
94
98
  "-autostartprgmode",
95
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",
96
102
  // The same RAM after every power-up: a hard reset is a power cycle, and VICE's default can flip
97
103
  // random bits, so two runs from one start state could differ.
98
104
  "-raminitstartvalue",
@@ -200,9 +206,10 @@ export function startErrorLines(output: string[]): string[] {
200
206
  /**
201
207
  * Starts VICE once, as a session does, and stops it again. Finding
202
208
  * x64sc is not enough: without its ROM files VICE exits at once. Throws
203
- * WireFailure installation-incomplete with VICE's own error lines.
209
+ * WireFailure installation-incomplete with VICE's own error lines. Returns
210
+ * the VICE version.
204
211
  */
205
- export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandard" | "log">): Promise<void> {
212
+ export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandard" | "log">): Promise<ViceVersion> {
206
213
  const binary = findVice(options.env ?? process.env);
207
214
  const output: string[] = [];
208
215
  let vice: ViceProcess;
@@ -231,13 +238,16 @@ export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandar
231
238
  } finally {
232
239
  await vice.stop();
233
240
  }
234
- const version = { major: info[1] ?? 0, minor: info[2] ?? 0 };
241
+ // The SVN revision is 0 in a release build.
242
+ const revision = info.length >= 10 ? info.readUInt32LE(6) : 0;
243
+ const version = { major: info[1] ?? 0, minor: info[2] ?? 0, ...(revision === 0 ? {} : { revision }) };
235
244
  if (isOlderThanMinimum(version)) {
236
245
  throw new WireFailure(
237
246
  "installation-incomplete",
238
247
  `VICE (${binary}) is version ${version.major}.${version.minor}, and c64-re-tools needs VICE ${MIN_VICE.major}.${MIN_VICE.minor} or later. ${VICE.remedy}`,
239
248
  );
240
249
  }
250
+ return version;
241
251
  }
242
252
 
243
253
  async function launchOnce(options: LaunchOptions): Promise<ViceProcess> {
@@ -302,6 +312,7 @@ async function launchOnce(options: LaunchOptions): Promise<ViceProcess> {
302
312
  clearInterval(slow);
303
313
  }
304
314
  if (failure !== undefined) throw failure;
315
+ log(`VICE stopped (pid ${child.pid}, in ${Date.now() - started} ms)`);
305
316
  })();
306
317
  return stopping;
307
318
  };
@@ -320,7 +331,7 @@ async function launchOnce(options: LaunchOptions): Promise<ViceProcess> {
320
331
  if (error instanceof WireFailure) throw error;
321
332
  throw new WireFailure("machine-unavailable", "The emulator could not be started on the host.");
322
333
  }
323
- log(`VICE started (pid ${child.pid}, monitor ports ${port} and ${textPort})`);
334
+ log(`VICE started (${mode}, pid ${child.pid}, monitor ports ${port} and ${textPort})`);
324
335
  trace().event("vice.ready", { pid: child.pid, ms: elapsedMs(launched) });
325
336
  return { pid: child.pid, monitor, text, exited: child.exited, outputTail, stop };
326
337
  }
@@ -1,16 +1,9 @@
1
1
  // Canonical emulator-frame capture: VICE's own rendered VIC-II frame, never a
2
2
  // desktop screenshot; the frame comparison and difference image for baselines.
3
3
 
4
- import { crc32, deflateSync } from "node:zlib";
5
-
4
+ import type { IndexedFrame } from "../../graphics.ts";
6
5
  import type { VideoStandard } from "../../protocol/messages.ts";
7
-
8
- export interface IndexedFrame {
9
- width: number;
10
- height: number;
11
- /** One palette index per pixel, row by row. */
12
- pixels: Uint8Array;
13
- }
6
+ import { FRAME_SIZE } from "../../protocol/vice.ts";
14
7
 
15
8
  export interface RawDisplay {
16
9
  /** Full buffer size, blanking included. */
@@ -44,8 +37,8 @@ export function decodeDisplay(body: Buffer): RawDisplay {
44
37
  * offsets: PAL 384×272, NTSC 384×247.
45
38
  */
46
39
  const VISIBLE: Record<VideoStandard, { dx: number; dy: number; width: number; height: number }> = {
47
- pal: { dx: -32, dy: -36, width: 384, height: 272 },
48
- ntsc: { dx: -32, dy: -23, width: 384, height: 247 },
40
+ pal: { dx: -32, dy: -36, ...FRAME_SIZE.pal },
41
+ ntsc: { dx: -32, dy: -23, ...FRAME_SIZE.ntsc },
49
42
  };
50
43
 
51
44
  /** Crops the visible bordered frame out of VICE's full raster buffer. */
@@ -82,39 +75,6 @@ export function decodePalette(body: Buffer): Array<[number, number, number]> {
82
75
  return colours;
83
76
  }
84
77
 
85
- function chunk(type: string, data: Uint8Array): Buffer {
86
- const length = Buffer.alloc(4);
87
- length.writeUInt32BE(data.length, 0);
88
- const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
89
- const crc = Buffer.alloc(4);
90
- crc.writeUInt32BE(crc32(body), 0);
91
- return Buffer.concat([length, body, crc]);
92
- }
93
-
94
- /** Encodes an indexed frame as an 8-bit palette PNG. */
95
- export function encodePng(frame: IndexedFrame, palette: ReadonlyArray<readonly [number, number, number]>): Buffer {
96
- for (const index of frame.pixels) {
97
- if (index >= palette.length) throw new Error(`pixel colour ${index} is not in the ${palette.length}-colour palette`);
98
- }
99
- const header = Buffer.alloc(13);
100
- header.writeUInt32BE(frame.width, 0);
101
- header.writeUInt32BE(frame.height, 4);
102
- header[8] = 8; // bit depth
103
- header[9] = 3; // indexed colour
104
- const rows = Buffer.alloc((frame.width + 1) * frame.height);
105
- for (let row = 0; row < frame.height; row++) {
106
- // Filter byte 0 (none), then the row.
107
- rows.set(frame.pixels.subarray(row * frame.width, (row + 1) * frame.width), row * (frame.width + 1) + 1);
108
- }
109
- return Buffer.concat([
110
- Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
111
- chunk("IHDR", header),
112
- chunk("PLTE", Buffer.from(palette.flat())),
113
- chunk("IDAT", deflateSync(rows)),
114
- chunk("IEND", new Uint8Array(0)),
115
- ]);
116
- }
117
-
118
78
  export interface FrameDifference {
119
79
  mismatchingPixels: number;
120
80
  /** Pixels compared: the frame minus the masked ones. */