@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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +1 -1
  3. package/skills/c64-basic/SKILL.md +2 -2
  4. package/skills/c64-basic/scripts/basic.ts +1 -1
  5. package/skills/c64-disk/SKILL.md +3 -3
  6. package/skills/c64-disk/scripts/disk.ts +2 -1
  7. package/skills/c64-emulator/SKILL.md +4 -1
  8. package/skills/c64-knowledge/SKILL.md +2 -2
  9. package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
  10. package/skills/c64-provenance/SKILL.md +10 -5
  11. package/skills/c64-provenance/scripts/compare.ts +35 -6
  12. package/skills/c64-provenance/scripts/provenance.ts +35 -15
  13. package/skills/c64-provenance/scripts/separate.ts +12 -4
  14. package/skills/c64-reverse-engineering/SKILL.md +2 -2
  15. package/skills/c64-source/SKILL.md +18 -11
  16. package/skills/c64-source/scripts/generate.ts +55 -10
  17. package/skills/c64-source/scripts/loader.ts +253 -6
  18. package/skills/c64-source/scripts/source.ts +46 -11
  19. package/skills/c64-static-analysis/SKILL.md +4 -4
  20. package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
  21. package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
  22. package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
  23. package/skills/c64-testing/SKILL.md +1 -1
  24. package/skills/c64-testing/references/scenario-format.md +1 -1
  25. package/skills/c64-testing/scripts/scenario.ts +9 -7
  26. package/skills/c64-unpacker/SKILL.md +11 -9
  27. package/skills/c64-unpacker/scripts/loads.ts +16 -4
  28. package/skills/c64-unpacker/scripts/machine.ts +13 -1
  29. package/skills/c64-unpacker/scripts/stages.ts +66 -3
  30. package/skills/c64-unpacker/scripts/unpack.ts +135 -42
  31. package/src/c64.ts +19 -5
  32. package/src/cli/status.ts +26 -3
  33. package/src/host/tools/c1541.ts +34 -5
  34. package/src/host/tools/petcat.ts +5 -2
  35. package/src/host/tools/status.ts +6 -3
  36. package/src/host/vice/adapter.ts +37 -9
  37. package/src/host/vice/chips.ts +11 -1
  38. package/src/host/vice/process.ts +7 -2
  39. package/src/host/vice/screen.ts +3 -2
  40. package/src/host/vice/session.ts +68 -15
  41. package/src/host-client/autostart.ts +11 -1
  42. package/src/host-client/connect.ts +10 -10
  43. package/src/host-client/coverage.ts +19 -10
  44. package/src/host-client/tools.ts +6 -6
  45. package/src/host-client/vice-session.ts +1 -1
  46. package/src/knowledge/schema.ts +7 -0
  47. package/src/knowledge/write.ts +5 -0
  48. package/src/mcp/tools/debug.ts +9 -5
  49. package/src/mcp/tools/machine.ts +4 -1
  50. package/src/mcp/tools/media.ts +7 -1
  51. package/src/mcp/tools/video.ts +7 -2
  52. package/src/native/discover.ts +2 -1
  53. package/src/platform.ts +3 -2
  54. package/src/protocol/tools.ts +19 -4
  55. package/src/protocol/vice.ts +56 -5
@@ -9,6 +9,7 @@ import { dirname } from "node:path";
9
9
  import { parseArgs } from "node:util";
10
10
 
11
11
  import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
12
+ import { joinCoverage } from "#src/host-client/coverage.ts";
12
13
  import { WireFailure } from "#src/host-client/tools.ts";
13
14
  import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
14
15
  import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
@@ -21,7 +22,7 @@ import { currentRevision } from "#src/knowledge/read.ts";
21
22
  import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
22
23
  import { resolveProjectPath } from "#src/project.ts";
23
24
  import { runScript, UsageError } from "#src/script.ts";
24
- import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageFiles } from "./coverage.ts";
25
+ import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageMaps } from "./coverage.ts";
25
26
  import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.ts";
26
27
  import { findIdioms } from "./idioms.ts";
27
28
  import { instructionList } from "./instructions.ts";
@@ -38,8 +39,9 @@ const USAGE = `analyze.ts <image> [options]
38
39
  mode, operand, target, executed) to this project file as JSON
39
40
  --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): its
40
41
  executed code is seeded, and analyzer data over it is reported
41
- --max-listed <n> list at most n labels or functions (default 50, 0 for none);
42
- the counts are always given, and knowledge has all of them
42
+ --max-listed <n> list at most n labels, functions or candidates (default 50,
43
+ 0 for none); the counts are always given, and knowledge has
44
+ all labels and functions
43
45
  --space <space> the knowledge space of the image: c64 (the default), drive8
44
46
  for 1541 drive code, or bank0 to bank255 for a cartridge bank
45
47
 
@@ -158,7 +160,8 @@ async function run(argv: string[]): Promise<unknown> {
158
160
  if (space !== DEFAULT_SPACE && values.coverage !== undefined) throw new UsageError("--coverage works only in space c64");
159
161
  const imageKind = values.flat64k ? "flat64k" : "prg";
160
162
  const bytes = readProjectFile(image).bytes;
161
- const coverage = readCoverageFiles(values.coverage ?? []);
163
+ const runs = readCoverageMaps(values.coverage ?? []);
164
+ const coverage = runs.length === 0 ? undefined : joinCoverage(runs);
162
165
  const byteAt = imageReader(bytes, imageKind);
163
166
  // With coverage, a code finding over an executed instruction start replaces the data of the other analyzer (D58), and the files are the evidence.
164
167
  const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
@@ -170,7 +173,7 @@ async function run(argv: string[]): Promise<unknown> {
170
173
  } catch (error) {
171
174
  throw new UsageError(`${image}: ${(error as Error).message}`);
172
175
  }
173
- const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, coverage);
176
+ const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, runs);
174
177
 
175
178
  const reader = openForRead();
176
179
  const revision = currentRevision(reader);
@@ -192,7 +195,11 @@ async function run(argv: string[]): Promise<unknown> {
192
195
  revision: imported.revision,
193
196
  changes: { regions: imported.regions, references: imported.references },
194
197
  conflicts: imported.conflicts.map((conflict) => showConflict(conflict, "heuristic")),
195
- candidates: { dop: idioms.candidates.dop.map(formatC64Address), selfModified: idioms.candidates.selfModified.map(formatC64Address) },
198
+ // Candidates are not knowledge: a larger --max-listed gives more of them.
199
+ candidates: {
200
+ dop: listed(idioms.candidates.dop, max, formatC64Address, "a larger --max-listed"),
201
+ selfModified: listed(idioms.candidates.selfModified, max, formatC64Address, "a larger --max-listed"),
202
+ },
196
203
  },
197
204
  };
198
205
  };
@@ -5,7 +5,7 @@
5
5
  import type { DatabaseSync } from "node:sqlite";
6
6
 
7
7
  import { COVERAGE, instructionLength } from "#src/c64.ts";
8
- import { FULL_COVERAGE, readCoverageFiles as readFiles } from "#src/host-client/coverage.ts";
8
+ import { FULL_COVERAGE, readCoverageMaps as readMaps } from "#src/host-client/coverage.ts";
9
9
  import { listRegions, type RegionRow } from "#src/knowledge/read.ts";
10
10
  import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
11
11
  import { UsageError } from "#src/script.ts";
@@ -15,9 +15,9 @@ const FULL_MAP = FULL_COVERAGE;
15
15
  const FLOW_ENDS = new Set([0x00, 0x40, 0x4c, 0x60, 0x6c]);
16
16
  const ANALYZER_ORIGINS = new Set(["dxa", "ghidra", "heuristic"]);
17
17
 
18
- /** Reads coverage files and joins them: an address has every flag that any run gave it. */
19
- export function readCoverageFiles(paths: string[]): Uint8Array | undefined {
20
- return readFiles(paths, (message) => {
18
+ /** Reads coverage files, one map per file (a run). */
19
+ export function readCoverageMaps(paths: string[]): Uint8Array[] {
20
+ return readMaps(paths, (message) => {
21
21
  throw new UsageError(message);
22
22
  });
23
23
  }
@@ -1,10 +1,13 @@
1
1
  // 6502 idioms that hide code from a disassembler (D44): RTS dispatch tables,
2
2
  // JMP tables and BIT skips are found by their exact instruction patterns and
3
3
  // become heuristic findings and entry points; DOP padding and self-modified
4
- // opcodes are only reported. A table's length is not known statically:
5
- // entries count while each target is a plausible instruction in the program.
4
+ // opcodes are only reported. A self-modified opcode is one that one run wrote
5
+ // and executed, in either order, since a coverage file holds no order (R3-04):
6
+ // a copier's writes in one run and their execution in another do not count (F-16). A table's length is not known statically: entries
7
+ // count while each target is a plausible instruction in the program.
6
8
 
7
9
  import { COVERAGE } from "#src/c64.ts";
10
+ import { joinCoverage } from "#src/host-client/coverage.ts";
8
11
  import type { NormalizedFindings } from "#src/knowledge/import.ts";
9
12
  import type { ImageByte } from "./coverage.ts";
10
13
 
@@ -22,7 +25,7 @@ export interface Idioms {
22
25
  /** Table ranges for the same run: they are data. */
23
26
  dataRanges: Array<{ start: number; end: number }>;
24
27
  counts: { rtsTables: number; jumpTables: number; bitSkips: number };
25
- /** Seen in the coverage, not written: the LLM decides. */
28
+ /** Seen in the coverage, not written: the LLM decides. All of them: the result lists the first ones. */
26
29
  candidates: { dop: number[]; selfModified: number[] };
27
30
  }
28
31
 
@@ -38,7 +41,9 @@ const word = (byteAt: ImageByte, address: number) => {
38
41
  return low === undefined || high === undefined ? undefined : low | (high << 8);
39
42
  };
40
43
 
41
- export function findIdioms(byteAt: ImageByte, range: { start: number; end: number }, coverage?: Uint8Array): Idioms {
44
+ /** `runs` are the coverage maps, one for each file. */
45
+ export function findIdioms(byteAt: ImageByte, range: { start: number; end: number }, runs: readonly Uint8Array[] = []): Idioms {
46
+ const coverage = runs.length === 0 ? undefined : joinCoverage(runs);
42
47
  const regions: NormalizedFindings["regions"] = [];
43
48
  const references: NormalizedFindings["references"] = [];
44
49
  const entryPoints = new Set<number>();
@@ -113,14 +118,14 @@ export function findIdioms(byteAt: ImageByte, range: { start: number; end: numbe
113
118
  }
114
119
  }
115
120
 
116
- // Candidates from the coverage: DOP padding that ran, and opcodes that the program wrote and then ran.
121
+ // Candidates from the coverage: DOP padding that ran, and opcodes that one run wrote and executed.
117
122
  const candidates = { dop: [] as number[], selfModified: [] as number[] };
118
123
  if (coverage !== undefined) {
124
+ const both = COVERAGE.ramWrite | COVERAGE.ramExecute;
119
125
  for (let address = range.start; address <= range.end; address++) {
120
- const flags = coverage[address]!;
121
- if ((flags & COVERAGE.ramExecute) === 0) continue;
122
- if (DOPS.has(byteAt(address)!) && candidates.dop.length < MAX_TABLE) candidates.dop.push(address);
123
- if ((flags & COVERAGE.ramWrite) !== 0 && candidates.selfModified.length < MAX_TABLE) candidates.selfModified.push(address);
126
+ if ((coverage[address]! & COVERAGE.ramExecute) === 0) continue;
127
+ if (DOPS.has(byteAt(address)!)) candidates.dop.push(address);
128
+ if (runs.some((run) => (run[address]! & both) === both)) candidates.selfModified.push(address);
124
129
  }
125
130
  }
126
131
 
@@ -86,7 +86,7 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
86
86
  - INCONCLUSIVE, "did not reach the checkpoint": one side did not reach the checkpoint in the frame limit. Find the reason with the c64-emulator skill. Do not report a FAIL.
87
87
  - INCONCLUSIVE, "has no symbol": one side has no symbol with that name. Add the name to the knowledge or to the rebuild source.
88
88
  - INCONCLUSIVE, "no emulator" or "could not run": the host runtime or the emulator failed. Tell the user the message.
89
- - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
89
+ - `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
90
90
  - A FAIL with only a `screen` difference: add `vicii` or `sprites` to the observation to find which chip state is different.
91
91
  - A program that uses random numbers: set the random seed with a `write` step on both sides. If that is not possible, observe only values that do not depend on the random numbers.
92
92
  - The two sides start differently, for example the original after a cracked intro. Give the steps that only one side needs a `side`. Then make the memory the same with a `normalize` step from the entry state of the c64-unpacker skill.
@@ -73,7 +73,7 @@ An `observe` step has a checkpoint name. A scenario can have each name only one
73
73
  | `vicii` | The VIC-II state: mode, screen address, scroll and colors. |
74
74
  | `sprites` | A list of sprite numbers from 0 to 7. The script compares their position, color and flags. |
75
75
 
76
- A `tolerance` is the largest permitted difference for each byte. A `mask` has at most 64 rectangles. A scenario can observe the screen at most at 64 checkpoints.
76
+ A `tolerance` is the largest permitted difference for each byte. A `mask` has at most 64 rectangles. A `mask` must not cover all of the screen: PAL is 384×272 pixels and NTSC is 384×247 pixels. A scenario can observe the screen at most at 64 checkpoints.
77
77
 
78
78
  `exclude` is a list of bytes in the observed range that the comparison does not include. Each has `at` and a `reason`, for example `{ "at": "$d01e", "reason": "latched sprite collision, not writable" }`. The result lists each excluded byte with its reason.
79
79
 
@@ -4,7 +4,7 @@
4
4
  // explicit, and the result is PASS, FAIL or INCONCLUSIVE.
5
5
 
6
6
  import { formatC64Address, parseC64Address } from "#src/c64.ts";
7
- import { MAX_BASELINES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
7
+ import { MAX_BASELINES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
8
8
  import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
9
9
  import { ACTION_STEP_KEYS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type Location } from "#src/host-client/steps.ts";
10
10
  import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
@@ -129,7 +129,7 @@ export function readScenario(path: string): Scenario {
129
129
  const allowed = [...(STEP_KEYS[kind] ?? [kind]), ...(kind === "observe" ? [] : ["side"])];
130
130
  for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
131
131
  if (step.side !== undefined && !(SIDES as unknown[]).includes(step.side)) fail(`${at}: side must be original or rebuild`);
132
- checkStep(kind, step, at, fail);
132
+ checkStep(kind, step, at, fail, scenario.videoStandard ?? "pal");
133
133
  if (kind === "observe") {
134
134
  if (labels.has(step.observe as string)) fail(`${at} needs a new checkpoint name`);
135
135
  labels.add(step.observe as string);
@@ -142,7 +142,7 @@ export function readScenario(path: string): Scenario {
142
142
  }
143
143
 
144
144
  /** Checks the value of one step against its kind. */
145
- function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
145
+ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
146
146
  switch (kind) {
147
147
  case "reset":
148
148
  if (step.reset !== "hard" && step.reset !== "soft") fail(`${at}: reset must be hard or soft`);
@@ -165,11 +165,11 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
165
165
  case "runUntil":
166
166
  return checkActionStep(kind, step, at, fail);
167
167
  default:
168
- checkObserve(step, at, fail);
168
+ checkObserve(step, at, fail, videoStandard);
169
169
  }
170
170
  }
171
171
 
172
- function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
172
+ function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
173
173
  if (typeof step.observe !== "string" || step.observe === "") fail(`${at} needs a new checkpoint name`);
174
174
  if (step.memory !== undefined) {
175
175
  if (!Array.isArray(step.memory) || step.memory.length === 0) fail(`${at}: memory must be a list of names, $ addresses or objects with at`);
@@ -216,6 +216,8 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
216
216
  isInteger(rectangle.height, 1, 4096);
217
217
  if (!ok) fail(`${at}: a screen.mask rectangle needs whole numbers x and y from 0, and width and height from 1`);
218
218
  }
219
+ // The emulator refuses such a compare too, but only after the original side ran (b2/03).
220
+ if (maskCoversFrame(screen.mask as Array<{ x: number; y: number; width: number; height: number }>, videoStandard)) fail(`${at}: screen.mask covers the whole screen, so nothing is compared`);
219
221
  }
220
222
  }
221
223
  if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
@@ -283,7 +285,7 @@ export function resolve(location: Location, symbols: Map<string, number>, side:
283
285
  interface Observation {
284
286
  memory: Array<{ label: string; address: number; bytes: number[]; tolerance: number; excluded: Array<{ offset: number; at: Location; reason: string }> }>;
285
287
  registers: Partial<Record<ObservedRegister, unknown>>;
286
- screen?: { match: boolean; mismatchingPixels: number; mismatchRatio: number; bounds?: unknown };
288
+ screen?: { match: boolean; mismatchingPixels: number; comparedPixels: number; mismatchRatio: number; bounds?: unknown };
287
289
  vicii?: unknown;
288
290
  sprites?: unknown;
289
291
  /** PNG images for the files of a FAIL (D46): the side's screen, and on the rebuild the difference image. */
@@ -371,7 +373,7 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
371
373
  if (files) observation.png = shot.png;
372
374
  } else {
373
375
  const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff: files });
374
- observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
376
+ observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, comparedPixels: compared.comparedPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
375
377
  if (files) {
376
378
  observation.png = (await client.screenCapture()).png;
377
379
  if (compared.diffPng !== undefined) observation.diffPng = compared.diffPng;
@@ -26,17 +26,17 @@ node <skill>/scripts/unpack.ts stages original/game.prg --steps input.json
26
26
  node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out parts --drive
27
27
  ```
28
28
 
29
- `inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). In `stages`, a `frames` step counts as run time, and a `runUntil` step is not possible.
29
+ `inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). In `stages` and `capture`, a `frames` step and a `runUntil` step with only `at` count as run time. `stages` refuses a `runUntil` step with `raster` or `memory`.
30
30
 
31
31
  | Option | Meaning |
32
32
  |---|---|
33
- | `--until '$c000'` | The address where the unpacked program starts. |
33
+ | `--until '$c000'` | The address where the unpacked program starts. The capture stops there after the steps. To capture a later arrival at the address, add a `runUntil` step to an address after the first arrival. |
34
34
  | `--out unpacked/game.prg` | Where to write the result, relative to the project directory. |
35
35
  | `--range '$c000' '$cfff'` | Write this range as a PRG. Without it, the script writes all 64 KiB of RAM as a flat file. |
36
36
  | `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
37
37
  | `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
38
38
  | `--entry '$c000'` | `trace`, `stages` and `capture`: start the program at this address. Without it, the script types RUN. A file that is not a BASIC program at `$0801` needs it. |
39
- | `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. |
39
+ | `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. The scripts start the program as a c64-testing scenario does. Thus the same steps in a scenario get the program to the entry at the same time. |
40
40
  | `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
41
41
  | `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
42
42
  | `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
@@ -64,18 +64,19 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
64
64
  - `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
65
65
  - `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
66
66
  2. Run `trace`. It gives the evidence from the run. Read `packing`:
67
- - `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the written code that runs at the end of the trace. When the trace ends in ROM code, for example in an interrupt, it is the written code that runs next. Its `start` is usually the entry point of the unpacked program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
67
+ - `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the written code that runs at the end of the trace. When the trace ends in ROM code, for example in an interrupt, it is the written code that runs next. Its `start` is code that runs after the unpacking. It is not always the entry point of the program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
68
68
  - `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
69
69
  - `unlikely`: the program executed no code that it wrote. If the program waits for a key or a joystick, it can move code after that input. Then use the c64-emulator skill.
70
70
  3. Find the address where the unpacked program starts:
71
- - Run `stages`. The `entry` of the last stage is usually the start of the program.
71
+ - Run `stages`. The `entry` of the last stage is usually the start of the program. A routine that a stage writes and calls is part of that stage when it returns to the stage.
72
72
  - Or use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
73
73
  - Or use the c64-emulator skill: set a breakpoint after the unpack loop, or look at the last JMP of the unpacker.
74
+ - Disassemble the code at the address with the c64-emulator skill. Make sure that it is the start of the program. The start of a program often sets the stack pointer, the interrupts and the vectors.
74
75
  4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
75
76
  - `image.bin`: all 64 KiB of RAM at the entry.
76
- - `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. Blocks with the code of an earlier stage are not segments. `writtenBy` tells which stage wrote the segment.
77
+ - `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
77
78
  - One PRG for each segment.
78
- - `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
79
+ - `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
79
80
  5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it.
80
81
  6. Give `image.bin` to the c64-static-analysis skill with its `--flat64k` option, or give it the segment PRGs.
81
82
  7. Use `--range` only when you know the area of the program. Then the capture writes that one segment.
@@ -113,7 +114,7 @@ Do not record the unpacker in the project knowledge, unless the user asks to exa
113
114
 
114
115
  The packing evidence, the stages, or the unpacked program with its image, segments and entry state in a directory of the project.
115
116
 
116
- For `loads`: `captures`, with the name, the PC, the segments and the drive file of each capture.
117
+ For `loads`: `captures`, with the name, the PC, the segments and the drive file of each capture. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
117
118
 
118
119
  The exit status is 0 for a result. The exit status is 1 for `traced: false`, for `captured: false`, for `loaded: false` and for an error. The exit status is 2 for a wrong argument.
119
120
 
@@ -122,10 +123,11 @@ The exit status is 0 for a result. The exit status is 1 for `traced: false`, for
122
123
  - `traced: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
123
124
  - `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames. If it stopped in a key wait, give `--steps` with a key.
124
125
  - A step did not reach its target: the reason names the step. Change the step or give it more frames.
126
+ - `loaded: false` and no capture has a segment: the program did not load a part, or it did not start. `keyWait: true` on a capture tells that the C64 waited at READY. or at an INPUT statement. For a tape image, read `emulatorFlaw`. A known VICE flaw can stop a tape load. Tell the user to install the latest VICE.
125
127
  - Do not name a packer from weak evidence. The script never names one.
126
128
  - Some programs unpack in more than one stage. Run `stages` to find each stage. Capture at the entry of the last stage. Make sure that it is the application and not a second unpacker.
127
129
  - `not-found` or `invalid-input`: a path or an argument is wrong. Read the message.
128
- - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
130
+ - `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
129
131
 
130
132
  ## Handoffs
131
133
 
@@ -4,8 +4,8 @@
4
4
  // image with its entry state, and with --drive the drive RAM as a PRG for
5
5
  // knowledge space drive8. No tool guesses where a load ends: the steps decide.
6
6
 
7
- import { COVERAGE, formatC64Address, parseC64Address, textToPetscii } from "#src/c64.ts";
8
- import { autostartLoaded, LOAD_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
7
+ import { COVERAGE, formatC64Address, parseC64Address } from "#src/c64.ts";
8
+ import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
9
9
  import { readCoverage } from "#src/host-client/coverage.ts";
10
10
  import { ACTION_STEP_KEYS, ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
11
11
  import { WireFailure } from "#src/host-client/tools.ts";
@@ -24,6 +24,8 @@ const DRIVE_RAM = 0x0800;
24
24
  const FIRST_SEGMENT_ADDRESS = 0x0400;
25
25
  /** The KERNAL LOAD entry in the jump table. */
26
26
  const KERNAL_LOAD = 0xffd5;
27
+ /** The KERNAL loop that waits for a key at READY. or at an INPUT statement. */
28
+ const KERNAL_KEY_WAIT = { start: 0xe5cd, end: 0xe5d5 };
27
29
 
28
30
  /** Reads and checks the steps file of loads: action steps with $ addresses, and capture steps with new names. */
29
31
  export function readLoadSteps(path: string): LoadStep[] {
@@ -100,6 +102,8 @@ export async function loads(program: string, options: { out: string; steps: Load
100
102
  let loadedProgram: Buffer | undefined;
101
103
  /** False while the map holds the writes of the KERNAL RAM test. */
102
104
  let writesKnown = true;
105
+ /** A known flaw of the host's VICE version that a tape load meets (D56). */
106
+ let emulatorFlaw: string | undefined;
103
107
  if (program.toLowerCase().endsWith(".prg")) {
104
108
  const { bytes } = readProjectFile(program);
105
109
  if (!(await autostartLoaded(client, program, bytes))) return { output: { loaded: false, reason: `The program was not in memory after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
@@ -109,12 +113,13 @@ export async function loads(program: string, options: { out: string; steps: Load
109
113
  previous.fill(0, load, Math.min(0x10000, load + bytes.length - 2));
110
114
  loadedProgram = bytes;
111
115
  await client.memmap({ action: "clear" });
112
- await client.keyboard(textToPetscii("RUN\n"));
116
+ // RUN as c64-testing types it: the steps start at the same time as a scenario's (D62).
117
+ if (!(await typeRun(client))) return { output: { loaded: false, reason: `After RUN, BASIC ran no program line in ${RUN_LIMIT_FRAMES} frames.` }, failed: true };
113
118
  } else {
114
119
  // A disk, tape or cartridge: VICE loads and starts it. The RAM test of the KERNAL puts back what it writes.
115
120
  previous = await readRam(client, 0x0000, 0x10000);
116
121
  await client.memmap({ action: "clear" });
117
- await client.autostart({ path: program, index: 0, run: true });
122
+ ({ emulatorFlaw } = await client.autostart({ path: program, index: 0, run: true }));
118
123
  writesKnown = false;
119
124
  if (!program.toLowerCase().endsWith(".crt")) {
120
125
  // The map starts again where the KERNAL LOAD starts, after the RAM test: then it holds only what the program wrote.
@@ -178,10 +183,12 @@ export async function loads(program: string, options: { out: string; steps: Load
178
183
  }),
179
184
  );
180
185
  write("entry_state.json", json(entryState(registers, state, vectors)));
186
+ const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
181
187
  captures.push({
182
188
  capture: step.capture,
183
189
  out: directory,
184
190
  pc: formatC64Address(registers.pc),
191
+ ...(keyWait ? { keyWait: true } : {}),
185
192
  segments: segments.map(showRange),
186
193
  ...(small === 0 ? {} : { smallChanges: small }),
187
194
  ...(drive === undefined ? {} : { drive: drive.file }),
@@ -190,6 +197,11 @@ export async function loads(program: string, options: { out: string; steps: Load
190
197
  await client.memmap({ action: "clear" });
191
198
  writesKnown = true;
192
199
  }
200
+ if (captures.length > 0 && captures.every((capture) => (capture.segments as unknown[]).length === 0)) {
201
+ const waits = captures.some((capture) => capture.keyWait === true) ? " The C64 waited for a key at READY. or at an INPUT statement, so the load ended or did not start." : "";
202
+ const reason = `No capture has a segment: the program changed no block of RAM from ${formatC64Address(FIRST_SEGMENT_ADDRESS)} on.${waits}`;
203
+ return { output: { loaded: false, reason, captures, ...(emulatorFlaw === undefined ? {} : { emulatorFlaw }) }, failed: true };
204
+ }
193
205
  return { output: { loaded: true, captures }, failed: false };
194
206
  } finally {
195
207
  await client.close();
@@ -14,10 +14,19 @@ export const IO_AREAS = { vic: [0xd000, 0x2f], sid: [0xd400, 0x1d], cia1: [0xdc0
14
14
  /** Frames from a hard reset to the READY prompt, with margin. */
15
15
  export const READY_FRAMES = 200;
16
16
 
17
+ /** What no register read of a CIA gives (D63). */
18
+ export interface CiaHidden {
19
+ latchA: number;
20
+ latchB: number;
21
+ interruptMask: number;
22
+ }
23
+
17
24
  export interface MachineState {
18
25
  port: Buffer;
19
26
  pages: Buffer;
20
27
  io: Record<keyof typeof IO_AREAS, Buffer>;
28
+ /** The timer latches and the interrupt mask of each CIA. */
29
+ cia: { cia1: CiaHidden; cia2: CiaHidden };
21
30
  romVectors: Buffer;
22
31
  }
23
32
 
@@ -36,8 +45,9 @@ export async function machineState(client: ViceSessionClient): Promise<MachineSt
36
45
  const pages = await readRam(client, 0x0000, 0x400);
37
46
  const io = {} as Record<keyof typeof IO_AREAS, Buffer>;
38
47
  for (const [name, [address, size]] of Object.entries(IO_AREAS)) io[name as keyof typeof IO_AREAS] = Buffer.from((await client.ioRead(address, size)).data, "hex");
48
+ const [cia1, cia2] = (await client.cia("both")).chips.map(({ latchA, latchB, interruptMask }) => ({ latchA, latchB, interruptMask }));
39
49
  const romVectors = Buffer.from((await client.memoryRead({ address: 0xfffa, size: 6, space: "c64", view: "cpu" })).data, "hex");
40
- return { port, pages, io, romVectors };
50
+ return { port, pages, io, cia: { cia1: cia1!, cia2: cia2! }, romVectors };
41
51
  }
42
52
 
43
53
  const word = (bytes: Buffer, offset: number) => formatC64Address(bytes[offset]! | (bytes[offset + 1]! << 8));
@@ -60,6 +70,8 @@ export function entryState(registers: Registers, state: MachineState, vectors: R
60
70
  cpuPort: { $00: state.port[0], $01: state.port[1] },
61
71
  pages: Object.fromEntries([0, 1, 2, 3].map((page) => [formatC64Address(page << 8), state.pages.subarray(page << 8, (page + 1) << 8).toString("hex")])),
62
72
  io: Object.fromEntries(Object.entries(IO_AREAS).map(([name, [address]]) => [name, { from: formatC64Address(address), bytes: state.io[name as keyof typeof IO_AREAS].toString("hex") }])),
73
+ // The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them (D63).
74
+ cia: state.cia,
63
75
  vectors,
64
76
  ...(differsFromReset === undefined ? {} : { differsFromReset }),
65
77
  };
@@ -4,7 +4,9 @@
4
4
  // starts only where the CPU executes an opcode byte that the current stage
5
5
  // wrote before the CPU got there. Code that the stage executed before it wrote
6
6
  // it is the stage's own code: it stops the run once, and then a write watch
7
- // over it arms it again.
7
+ // over it arms it again. Written code that the stage calls, and that returns
8
+ // to the stage, is the stage's own code too (D59): a packer hands off by JMP,
9
+ // RTS or RTI and never runs its own code again.
8
10
 
9
11
  import { COVERAGE } from "#src/c64.ts";
10
12
  import { writtenBlocks } from "#src/host-client/coverage.ts";
@@ -26,12 +28,13 @@ export function mergeMaps(into: Uint8Array, from: Uint8Array): void {
26
28
  * code that can start a stage: every executed address except own code that
27
29
  * the stage has not written.
28
30
  */
29
- export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number): Range[] {
31
+ export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number, called: readonly Range[] = []): Range[] {
30
32
  const combined = Uint8Array.from(stage);
31
33
  mergeMaps(combined, chunk);
32
34
  return writtenBlocks(combined, minimum).filter((block) => {
33
35
  for (let address = block.start; address <= block.end; address++) {
34
36
  if ((chunk[address]! & COVERAGE.ramExecute) === 0) continue;
37
+ if (inRanges(called, address)) continue;
35
38
  if (own.has(address) && (combined[address]! & COVERAGE.ramWrite) === 0) continue;
36
39
  return true;
37
40
  }
@@ -44,7 +47,7 @@ export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: Reado
44
47
  * them except own code that is not written yet, and write watches over that
45
48
  * own code.
46
49
  */
47
- export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>, written: (address: number) => boolean): { exec: Range[]; watch: Range[] } {
50
+ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>, written: (address: number) => boolean, called: readonly Range[] = []): { exec: Range[]; watch: Range[] } {
48
51
  const exec: Range[] = [];
49
52
  const watch: Range[] = [];
50
53
  const add = (list: Range[], address: number) => {
@@ -54,6 +57,7 @@ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>,
54
57
  };
55
58
  for (const block of blocks) {
56
59
  for (let address = block.start; address <= block.end; address++) {
60
+ if (inRanges(called, address)) continue;
57
61
  if (own.has(address) && !written(address)) add(watch, address);
58
62
  else add(exec, address);
59
63
  }
@@ -65,3 +69,62 @@ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>,
65
69
  export function inRanges(ranges: readonly Range[], address: number): boolean {
66
70
  return ranges.some((range) => contains(range, address));
67
71
  }
72
+
73
+ /**
74
+ * Where a call returns to the stage (D59): the return address on top of the
75
+ * stack, when a JSR that the stage executed pushed it. `stack` is page 1.
76
+ * Returns the address of the instruction after that JSR.
77
+ */
78
+ export function callReturn(stack: Uint8Array, sp: number, byteAt: (address: number) => number, executed: (address: number) => boolean): number | undefined {
79
+ const pushed = stack[(sp + 1) & 0xff]! | (stack[(sp + 2) & 0xff]! << 8);
80
+ const jsr = (pushed - 2) & 0xffff;
81
+ if (byteAt(jsr) !== 0x20 || !executed(jsr)) return undefined;
82
+ return (pushed + 1) & 0xffff;
83
+ }
84
+
85
+ /**
86
+ * The code that ran in a block: its executed opcode bytes with the two bytes
87
+ * after each, which can be operands, joined and cut at the block's end.
88
+ */
89
+ export function executedIn(map: Uint8Array, block: Range): Range[] {
90
+ const ranges: Range[] = [];
91
+ for (let address = block.start; address <= block.end; address++) {
92
+ if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
93
+ const end = Math.min(block.end, address + 2);
94
+ const last = ranges[ranges.length - 1];
95
+ if (last !== undefined && address <= last.end + 1) last.end = Math.max(last.end, end);
96
+ else ranges.push({ start: address, end });
97
+ }
98
+ return ranges;
99
+ }
100
+
101
+ /**
102
+ * The segments of a capture: the blocks that the stages wrote, without loader
103
+ * leftovers, joined into ranges that do not overlap or touch (F-26). A block
104
+ * is a leftover when it holds the entry of an intermediate stage that started
105
+ * after the block's writer, and no write from that stage on replaced the code
106
+ * at that entry (R2-01). Code that a stage ran before a later stage wrote over
107
+ * it never drops a block, and the block that holds the capture entry is always
108
+ * a segment. The filter comes before the join: a join first would drop the
109
+ * program with a leftover. `writtenBy` is the last stage that wrote into the
110
+ * range.
111
+ */
112
+ export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly Range[] }>, entry: number): Array<Range & { writtenBy: number }> {
113
+ const last = stages.length - 1;
114
+ const replaced = (from: number, address: number) => stages.slice(from).some((stage) => inRanges(stage.wrote, address));
115
+ const leftover = (block: Range, writer: number) =>
116
+ !contains(block, entry) && stages.some((stage, index) => index > writer && index < last && contains(block, stage.entry) && !replaced(index, stage.entry));
117
+ const blocks = stages
118
+ .flatMap((stage, index) => stage.wrote.map((range) => ({ start: range.start, end: range.end, writtenBy: index })))
119
+ .filter((block) => !leftover(block, block.writtenBy))
120
+ .sort((a, b) => a.start - b.start);
121
+ const segments: Array<Range & { writtenBy: number }> = [];
122
+ for (const block of blocks) {
123
+ const previous = segments[segments.length - 1];
124
+ if (previous !== undefined && block.start <= previous.end + 1) {
125
+ previous.end = Math.max(previous.end, block.end);
126
+ previous.writtenBy = Math.max(previous.writtenBy, block.writtenBy);
127
+ } else segments.push({ ...block });
128
+ }
129
+ return segments;
130
+ }