@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Pointer candidates (D65): code that builds an address from immediates. The
|
|
2
|
+
// byte of each immediate is the low or high byte of the address, so a rebuild
|
|
3
|
+
// that moves the address needs an operand expression there. They are only
|
|
4
|
+
// reported, never written: the LLM records the expressions with c64-knowledge.
|
|
5
|
+
//
|
|
6
|
+
// The scan reads short straight runs of instructions: LDA/LDX/LDY #imm and
|
|
7
|
+
// STA/STX/STY to zero page or absolute, plus SEI, CLI, CLC, SEC and NOP. Any
|
|
8
|
+
// other instruction ends the run, so no register value is guessed across it.
|
|
9
|
+
|
|
10
|
+
import { instructionLength } from "#src/c64.ts";
|
|
11
|
+
import type { ImageByte } from "./coverage.ts";
|
|
12
|
+
|
|
13
|
+
const LOADS: Record<number, "a" | "x" | "y"> = { 0xa9: "a", 0xa2: "x", 0xa0: "y" };
|
|
14
|
+
const STORES: Record<number, "a" | "x" | "y"> = { 0x85: "a", 0x8d: "a", 0x86: "x", 0x8e: "x", 0x84: "y", 0x8c: "y" };
|
|
15
|
+
const NEUTRAL = new Set([0x78, 0x58, 0x18, 0x38, 0xea]);
|
|
16
|
+
/** The instructions of one run: enough for two loads, two stores and a few neutral ones. */
|
|
17
|
+
const MAX_RUN = 8;
|
|
18
|
+
/** The vectors that the KERNAL and the CPU read: IRQ, BRK, NMI, and the hardware vectors. */
|
|
19
|
+
const VECTORS = new Set([0x0314, 0x0316, 0x0318, 0xfffa, 0xfffc, 0xfffe]);
|
|
20
|
+
/** (zp),y: ORA AND EOR ADC STA LDA CMP SBC. */
|
|
21
|
+
const INDIRECT_Y = new Set([0x11, 0x31, 0x51, 0x71, 0x91, 0xb1, 0xd1, 0xf1]);
|
|
22
|
+
/** The sprite pointers of the default screen at $0400 in VIC bank 0. */
|
|
23
|
+
const SPRITE_POINTERS = { start: 0x07f8, end: 0x07ff };
|
|
24
|
+
|
|
25
|
+
export type PointerCandidate =
|
|
26
|
+
| {
|
|
27
|
+
kind: "vector" | "zero-page pointer";
|
|
28
|
+
/** The vector or zero-page pair that gets the address. */
|
|
29
|
+
to: number;
|
|
30
|
+
/** The address that the two immediates make. */
|
|
31
|
+
target: number;
|
|
32
|
+
/** The instructions with the low and the high byte as immediate. */
|
|
33
|
+
low: number;
|
|
34
|
+
high: number;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
kind: "sprite pointer";
|
|
38
|
+
to: number;
|
|
39
|
+
/** value × 64: the sprite data, in VIC bank 0. */
|
|
40
|
+
target: number;
|
|
41
|
+
/** The instruction with the pointer as immediate. */
|
|
42
|
+
at: number;
|
|
43
|
+
value: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
interface Store {
|
|
47
|
+
/** The address of the load of the immediate. */
|
|
48
|
+
load: number;
|
|
49
|
+
value: number;
|
|
50
|
+
to: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The stores of immediates in the straight run that starts at `start`. */
|
|
54
|
+
function storesFrom(byteAt: ImageByte, start: number, range: { start: number; end: number }): Store[] {
|
|
55
|
+
const last: Partial<Record<"a" | "x" | "y", { load: number; value: number }>> = {};
|
|
56
|
+
const stores: Store[] = [];
|
|
57
|
+
let at = start;
|
|
58
|
+
for (let count = 0; count < MAX_RUN; count++) {
|
|
59
|
+
const opcode = byteAt(at);
|
|
60
|
+
if (opcode === undefined) break;
|
|
61
|
+
const length = instructionLength(opcode);
|
|
62
|
+
if (at + length - 1 > range.end) break;
|
|
63
|
+
const operand = length === 2 ? byteAt(at + 1)! : length === 3 ? byteAt(at + 1)! | (byteAt(at + 2)! << 8) : 0;
|
|
64
|
+
if (LOADS[opcode] !== undefined) last[LOADS[opcode]] = { load: at, value: operand };
|
|
65
|
+
else if (STORES[opcode] !== undefined) {
|
|
66
|
+
const loaded = last[STORES[opcode]];
|
|
67
|
+
if (loaded === undefined) break;
|
|
68
|
+
stores.push({ ...loaded, to: operand });
|
|
69
|
+
} else if (!NEUTRAL.has(opcode)) break;
|
|
70
|
+
at += length;
|
|
71
|
+
}
|
|
72
|
+
return stores;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Finds the pointer candidates in the image: an immediate pair stored to a
|
|
77
|
+
* vector or to a zero-page pair that (zp),y uses, where the pair makes an
|
|
78
|
+
* address in the image or a platform name; and an immediate stored to a sprite
|
|
79
|
+
* pointer of the default screen whose sprite data is in a sprite region.
|
|
80
|
+
*/
|
|
81
|
+
export function findPointers(
|
|
82
|
+
byteAt: ImageByte,
|
|
83
|
+
range: { start: number; end: number },
|
|
84
|
+
known: { platform: (address: number) => boolean; sprites: ReadonlyArray<{ start: number; end: number }> },
|
|
85
|
+
): PointerCandidate[] {
|
|
86
|
+
const indirect = new Set<number>();
|
|
87
|
+
for (let at = range.start; at < range.end; at++) if (INDIRECT_Y.has(byteAt(at)!)) indirect.add(byteAt(at + 1)!);
|
|
88
|
+
const inImage = (address: number) => address >= range.start && address <= range.end && byteAt(address) !== undefined;
|
|
89
|
+
const found = new Map<string, PointerCandidate>();
|
|
90
|
+
for (let at = range.start; at <= range.end; at++) {
|
|
91
|
+
if (LOADS[byteAt(at)!] === undefined) continue;
|
|
92
|
+
const stores = storesFrom(byteAt, at, range);
|
|
93
|
+
for (const low of stores) {
|
|
94
|
+
const high = stores.find((store) => store.to === low.to + 1);
|
|
95
|
+
const target = high === undefined ? undefined : low.value | (high.value << 8);
|
|
96
|
+
const kind = VECTORS.has(low.to) ? "vector" : low.to < 0xff && indirect.has(low.to) ? "zero-page pointer" : undefined;
|
|
97
|
+
if (high !== undefined && target !== undefined && kind !== undefined && (inImage(target) || known.platform(target))) {
|
|
98
|
+
found.set(`${low.load}|${high.load}`, { kind, to: low.to, target, low: low.load, high: high.load });
|
|
99
|
+
}
|
|
100
|
+
const sprite = low.value * 64;
|
|
101
|
+
if (low.to >= SPRITE_POINTERS.start && low.to <= SPRITE_POINTERS.end && known.sprites.some((region) => sprite >= region.start && sprite <= region.end)) {
|
|
102
|
+
found.set(`${low.load}`, { kind: "sprite pointer", to: low.to, target: sprite, at: low.load, value: low.value });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const first = (candidate: PointerCandidate) => (candidate.kind === "sprite pointer" ? candidate.at : Math.min(candidate.low, candidate.high));
|
|
107
|
+
return [...found.values()].sort((a, b) => first(a) - first(b));
|
|
108
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: c64-testing
|
|
3
|
-
description: Use this skill to test if a rebuilt or changed C64 program behaves as the original. It runs one scenario on the original and then on the rebuild in an emulator. It compares memory, registers, chip state and the screen with explicit rules, and gives PASS, FAIL or INCONCLUSIVE. It also makes a checklist for a human playtest. Do not use it to find the cause of a difference.
|
|
3
|
+
description: Use this skill to test if a rebuilt or changed C64 program behaves as the original. It runs one scenario on the original and then on the rebuild in an emulator. It compares memory, registers, chip state and the screen with explicit rules, and gives PASS, FAIL or INCONCLUSIVE. It also makes a checklist for a human playtest. Use its run command to explore a program with many steps, for example to visit each room or level. A run keeps screens, coverage maps and snapshots as files. Do not use it to find the cause of a difference.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# c64-testing
|
|
@@ -22,24 +22,73 @@ A PASS proves only the behavior that the scenario does and observes. It does not
|
|
|
22
22
|
The script starts one emulator through the host runtime. It runs the scenario on the original and then on the rebuild. Before each side, it does a hard reset of the emulator. The CPU then stops at the reset vector.
|
|
23
23
|
|
|
24
24
|
```
|
|
25
|
-
node <skill>/scripts/test.ts tests/moves-left.json
|
|
25
|
+
node <skill>/scripts/test.ts tests/moves-left.json --out .c64-re-tools/local/results/moves-left
|
|
26
26
|
node <skill>/scripts/checklist.ts tests/moves-left.json --item 'FIRE starts the game.'
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
The test script prints one JSON object. The exit status is 0 for PASS, 1 for FAIL, 3 for INCONCLUSIVE and 2 for a bad call.
|
|
29
|
+
The test script prints one JSON object. The exit status is 0 for PASS, 1 for FAIL, 3 for INCONCLUSIVE and 2 for a bad call. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json` with each byte that differs. `files` in the result names them. A PASS writes no files. To keep the screens of a PASS, use a run (below).
|
|
30
30
|
|
|
31
31
|
The checklist script prints a Markdown checklist for a human tester.
|
|
32
32
|
|
|
33
|
+
### Runs
|
|
34
|
+
|
|
35
|
+
A run does steps on one program, with no comparison, and keeps the results as files. Use it for many steps. One run replaces many emulator calls, for example a visit to each room of a game.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
node <skill>/scripts/run.ts .c64-re-tools/local/runs/rooms/run.json --out .c64-re-tools/local/runs/rooms
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The run file gives where to start (a program, or a snapshot file), the steps, and optional symbols. To go on from the live emulator, save its state with `c64_snapshot` action `save` and `out`, and start the run from that file. Besides the steps of a scenario, a run has these steps:
|
|
42
|
+
|
|
43
|
+
| Step | Effect |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" }` | Restore a snapshot file. |
|
|
46
|
+
| `{ "save": "room3" }` | Write the state now to `room3.vsf` in the output directory. |
|
|
47
|
+
| `{ "screen": "room3" }` | Write the screen to `room3.png`. |
|
|
48
|
+
| `{ "coverage": "room3" }` | Write the coverage map since the start or the last coverage step to `room3.bin`, then clear the map. Add the map to the knowledge with `coverage` of the c64-knowledge skill. |
|
|
49
|
+
| `{ "observe": "room3", "memory": ["$c100"], "registers": true }` | Record memory, registers, `vicii` or `sprites` in `results.json`. |
|
|
50
|
+
| `{ "trace": "d011", "points": [{ "kind": "write", "at": "$d011" }], "frames": 100 }` | Record each hit of the points for 100 frames in `d011.json`: the instruction, its registers and the raster line and cycle. A point is `exec`, `read` or `write`, at an address or a name, with an optional `end`. `maxHits` limits the hits. |
|
|
51
|
+
| `{ "observe": "all3", "memory": "all" }` | Write all 64 KiB of RAM to `all3.bin`, also the RAM under the ROMs and the I/O area. |
|
|
52
|
+
|
|
53
|
+
A memory item can have `"view": "ram"`. Then it reads the RAM under the ROMs and the I/O area, for example code at `$e000` under the KERNAL. The default view `cpu` reads as the CPU sees memory. An item larger than 4096 bytes goes into its own `.bin` file, and `results.json` gives the file name.
|
|
54
|
+
|
|
55
|
+
See [references/scenario-format.md](references/scenario-format.md), section Runs. The script prints one JSON object. The exit status is 0 when all steps ran, 1 when a step did not reach its target, and 2 for a bad call. A long run can need more time than one shell call. Then run it in the background.
|
|
56
|
+
|
|
57
|
+
### Differences
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
node <skill>/scripts/diff.ts .c64-re-tools/local/runs/rooms/before.vsf .c64-re-tools/local/runs/rooms/after.vsf
|
|
61
|
+
node <skill>/scripts/diff.ts .c64-re-tools/local/runs/rooms/results.json:room0 .c64-re-tools/local/runs/rooms/results.json:room3
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`diff.ts` tells what changed between two states of one kind. Use it in place of your own compare script.
|
|
65
|
+
|
|
66
|
+
- Two snapshot files: the script restores each one in its own emulator. It gives the changed ranges of the RAM and of the colour RAM, and the changed VIC-II, CIA, SID and CPU registers.
|
|
67
|
+
- Two memory files of a run: the changed ranges. For a file of less than 64 KiB, `--at` gives the address of its first byte.
|
|
68
|
+
- Two observations of runs, also of one run: the changed memory items and registers.
|
|
69
|
+
|
|
70
|
+
`--gap N` joins ranges across N equal bytes. `--out` writes all ranges to a project file.
|
|
71
|
+
|
|
72
|
+
### Project folder
|
|
73
|
+
|
|
74
|
+
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
75
|
+
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
76
|
+
- Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
|
|
77
|
+
- Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
|
|
78
|
+
- Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
|
|
79
|
+
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
80
|
+
|
|
33
81
|
## Workflow
|
|
34
82
|
|
|
35
83
|
1. Write the scenario file. Start the steps with a frame count, for example `{ "frames": 150 }`. Each side starts at the reset vector, and the C64 needs these frames to start.
|
|
36
84
|
2. Use symbol names, for example `player_x`, not fixed addresses. Each side finds the name in its own symbols. Use a `$` address only when both programs use the same address.
|
|
37
85
|
3. Make each checkpoint a condition, for example `runUntil` at a routine. Do not wait for a time.
|
|
38
|
-
4. Save the c64-assembler output of the rebuild to the symbol file
|
|
86
|
+
4. Save the c64-assembler output of the rebuild to the symbol file, for example `build/game.json` in the project folder. Give `--all-symbols`, so that the file also has the constants that the source does not use. From the folder above the project, write `assemble.ts --project <folder> ... > <folder>/build/game.json`.
|
|
39
87
|
5. Run the test script.
|
|
40
88
|
6. Read the result:
|
|
41
89
|
- `PASS`: all observations are equal, or the differences are in the allowed tolerance.
|
|
42
90
|
- `FAIL`: `differences` lists each difference with the checkpoint, the item, the original value and the rebuild value.
|
|
91
|
+
- Each value has one form. The pc and each address are `$xxxx`. Memory is lowercase hex. Other register values and chip values are decimal numbers.
|
|
43
92
|
- `INCONCLUSIVE`: `reason` tells why the script could not compare. This is not a failure of the program.
|
|
44
93
|
7. For a FAIL, examine the first difference with the c64-emulator skill.
|
|
45
94
|
|
|
@@ -66,8 +115,12 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
|
|
|
66
115
|
- 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.
|
|
67
116
|
- INCONCLUSIVE, "has no symbol": one side has no symbol with that name. Add the name to the knowledge or to the rebuild source.
|
|
68
117
|
- INCONCLUSIVE, "no emulator" or "could not run": the host runtime or the emulator failed. Tell the user the message.
|
|
118
|
+
- `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.
|
|
69
119
|
- A FAIL with only a `screen` difference: add `vicii` or `sprites` to the observation to find which chip state is different.
|
|
70
120
|
- 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.
|
|
121
|
+
- 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. It writes the entry state of the capture in the knowledge.
|
|
122
|
+
- Frame counts drift by one frame between the sides: the sides got to the same address at different raster positions. Do not sync on an IRQ handler. Sync on a value that the IRQ changes, with `runUntil` `memory`, or on an address and a raster line together.
|
|
123
|
+
- A byte that cannot be the same on both sides, for example a read-only collision register: use `exclude` with the reason. Do not use `exclude` to hide a real difference.
|
|
71
124
|
|
|
72
125
|
Do not add a mask or a tolerance only to make a test pass. A tolerance must have a reason in the behavior of the program.
|
|
73
126
|
|
|
@@ -23,7 +23,7 @@ A scenario is one JSON object in a project file.
|
|
|
23
23
|
|
|
24
24
|
`original` and `rebuild` each have:
|
|
25
25
|
|
|
26
|
-
- `program`: a PRG file, relative to the project
|
|
26
|
+
- `program`: a PRG file, relative to the project folder.
|
|
27
27
|
- `symbols`: where the names come from:
|
|
28
28
|
- `knowledge`: the project knowledge.
|
|
29
29
|
- A JSON file: the output of the c64-assembler skill, or an object of names and `$` addresses.
|
|
@@ -38,21 +38,27 @@ Before the first step of each side, the script does a hard reset. The CPU stops
|
|
|
38
38
|
| Step | Effect |
|
|
39
39
|
|---|---|
|
|
40
40
|
| `{ "reset": "hard" }` | Reset the C64 (`hard` or `soft`). The CPU stops at the reset vector. |
|
|
41
|
-
| `{ "frames": 150 }` | Run for an exact number of frames, from 1 to
|
|
41
|
+
| `{ "frames": 150 }` | Run for an exact number of frames, from 1 to 30000. A breakpoint or a watchpoint stops it before that. Use it after a reset to start the C64. |
|
|
42
42
|
| `{ "load": true }` | Load the program of the side into memory. The CPU stays stopped. |
|
|
43
|
-
| `{ "autostart": true }` | Load and
|
|
43
|
+
| `{ "autostart": true }` | Load the program of the side, type `RUN` and wait until the program runs. The program must have a BASIC start line. The keys of a later `type` step go to the program. |
|
|
44
44
|
| `{ "write": { "at": "seed", "bytes": [1, 2] } }` | Write bytes into memory: 1 to 4096 numbers from 0 to 255. |
|
|
45
45
|
| `{ "registers": { "pc": "start", "a": 0 } }` | Set registers: `pc`, `a`, `x`, `y` or `sp`. A value is a number or a name. |
|
|
46
46
|
| `{ "joystick": { "port": 2, "direction": "left", "fire": true } }` | Set a joystick. The joystick stays in this state until the next joystick step. |
|
|
47
|
-
| `{ "type": "RUN\n" }` | Type text on the keyboard, at most 1024 keys. |
|
|
47
|
+
| `{ "type": "RUN\n" }` | Type text on the keyboard, at most 1024 keys. The keys go into the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. A step cannot press one specific key of the keyboard matrix. |
|
|
48
48
|
| `{ "runUntil": { "at": "main_loop" }, "timeoutFrames": 600 }` | Run until the CPU gets to an address. |
|
|
49
49
|
| `{ "runUntil": { "memory": { "at": "game_state", "equals": 2 } }, "timeoutFrames": 600 }` | Run until a byte in memory has a value. |
|
|
50
|
+
| `{ "runUntil": { "raster": 250 }, "timeoutFrames": 5 }` | Run until the raster beam gets to a line (0 to 311 for PAL, 0 to 262 for NTSC). |
|
|
51
|
+
| `{ "runUntil": { "at": "irq_tick", "raster": 250 }, "timeoutFrames": 600 }` | Run until the CPU gets to an address while the beam is on that line. |
|
|
52
|
+
| `{ "snapshot": "snapshots/start.vsf" }` | Restore a snapshot file. Usually with `side`: the snapshot of one program is not a state of the other. |
|
|
53
|
+
| `{ "normalize": true }` | Write pages 0 to 3 and `$00`/`$01` from the entry state in the knowledge. The entry state comes from a capture of the c64-unpacker skill, which `image` of the c64-knowledge skill put in. Then both sides have the same zero page, stack and KERNAL work area. |
|
|
50
54
|
| `{ "observe": "moved", ... }` | Record a checkpoint. |
|
|
51
55
|
|
|
52
56
|
The `port` of a joystick is 1 or 2. The `direction` is `center`, `up`, `down`, `left`, `right`, `up-left`, `up-right`, `down-left` or `down-right`.
|
|
53
57
|
|
|
54
58
|
`timeoutFrames` is from 1 to 30000. A `runUntil` that does not get to its target in `timeoutFrames` frames makes the result INCONCLUSIVE.
|
|
55
59
|
|
|
60
|
+
Each step except `observe` can have `"side": "original"` or `"side": "rebuild"`. Then only that side does the step. Use it when only one side needs a step, for example a key for the intro of the original. An `observe` step has no `side`, because it compares both sides.
|
|
61
|
+
|
|
56
62
|
An address is `$` and four hex digits, or a symbol name. Each side finds a name in its own symbols.
|
|
57
63
|
|
|
58
64
|
## Observations
|
|
@@ -61,16 +67,54 @@ An `observe` step has a checkpoint name. A scenario can have each name only one
|
|
|
61
67
|
|
|
62
68
|
| Key | What the script compares |
|
|
63
69
|
|---|---|
|
|
64
|
-
| `memory` | A list of names, `$` addresses, or objects
|
|
70
|
+
| `memory` | A list of names, `$` addresses, or objects. An object has `at`, `size` (1 to 65536, default 1), `view` (`cpu` or `ram`, default `cpu`), `tolerance` (0 to 255, default 0) and `exclude`. The view `ram` reads the RAM under the ROMs and the I/O area. |
|
|
65
71
|
| `registers` | A list of register names: `pc`, `a`, `x`, `y`, `sp`, `flags`. |
|
|
66
72
|
| `screen` | The screen of the rebuild against the screen of the original. `maxMismatchRatio` (default 0) is the largest permitted part of different pixels. `mask` is a list of rectangles (`x`, `y`, `width`, `height`) that the comparison does not include. |
|
|
67
73
|
| `vicii` | The VIC-II state: mode, screen address, scroll and colors. |
|
|
68
74
|
| `sprites` | A list of sprite numbers from 0 to 7. The script compares their position, color and flags. |
|
|
69
75
|
|
|
70
|
-
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
|
+
|
|
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.
|
|
71
79
|
|
|
72
80
|
A `pc` value is an address. Compare `pc` only when both programs have the code at the same address.
|
|
73
81
|
|
|
82
|
+
## Runs
|
|
83
|
+
|
|
84
|
+
A run file (for `run.ts`) has:
|
|
85
|
+
|
|
86
|
+
- `name`: a short name.
|
|
87
|
+
- `start`: `{ "program": "game.prg" }` to autostart a program with a BASIC start line, `{ "program": "game.prg", "load": true }` to load it with the CPU stopped (set `pc` in a step), or `{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" }`.
|
|
88
|
+
- `symbols`: optional, `knowledge`, `none` (default), or a JSON file as for a subject.
|
|
89
|
+
- `videoStandard`: optional, `pal` (default) or `ntsc`.
|
|
90
|
+
- `steps`: the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of a scenario, and the run steps `snapshot`, `save`, `screen`, `coverage`, `trace` and `observe`.
|
|
91
|
+
|
|
92
|
+
A `trace` step has a name, `points` (1 to 8), `frames` (1 to 3000) and an optional `maxHits` (at most 20000). A point has `kind` (`exec`, `read` or `write`), `at` and an optional `end`. The step writes `<name>.json` with each hit. `results.json` gives the file, the count of hits, the frames and why the trace ended. A trace that a breakpoint, a watchpoint or a JAM ends also ends the run.
|
|
93
|
+
|
|
94
|
+
In a run, a memory item has `at`, `size` (1 to 65536) and `view`. An item larger than 4096 bytes goes into the file `<observe>-<at>.bin`. `"memory": "all"` writes all 64 KiB of RAM to `<observe>.bin`.
|
|
95
|
+
|
|
96
|
+
A name in `save`, `screen`, `coverage` and `observe` has 1 to 64 letters, digits, dots, underscores or hyphens. The files go into the `--out` directory, with `results.json`. It has the observations, the files and the steps done. If a `runUntil` step did not reach its target, it also tells why the run stopped.
|
|
97
|
+
|
|
98
|
+
Example: visit two rooms from the same start.
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"name": "rooms",
|
|
103
|
+
"start": { "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" },
|
|
104
|
+
"steps": [
|
|
105
|
+
{ "write": { "at": "$0350", "bytes": [1] } },
|
|
106
|
+
{ "frames": 50 },
|
|
107
|
+
{ "screen": "room1" },
|
|
108
|
+
{ "coverage": "room1" },
|
|
109
|
+
{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" },
|
|
110
|
+
{ "write": { "at": "$0350", "bytes": [2] } },
|
|
111
|
+
{ "frames": 50 },
|
|
112
|
+
{ "screen": "room2" },
|
|
113
|
+
{ "coverage": "room2" }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
74
118
|
## Checks
|
|
75
119
|
|
|
76
120
|
The script checks the scenario before it starts the emulator. A step with an unknown key or a wrong value stops the script with `invalid-input`. The message names the step.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
|
|
7
7
|
import { runScript, UsageError } from "#src/script.ts";
|
|
8
|
-
import { readScenario, ScenarioError, type Scenario } from "./scenario.ts";
|
|
8
|
+
import { readScenario, ScenarioError, type Scenario } from "./lib/scenario.ts";
|
|
9
9
|
|
|
10
10
|
const USAGE = `checklist.ts <scenario.json> [<scenario.json> ...] [--item <text> ...]
|
|
11
11
|
|
|
@@ -44,8 +44,8 @@ function run(argv: string[]): string {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
await runScript(
|
|
47
|
-
() => {
|
|
48
|
-
process.stdout.write(run(
|
|
47
|
+
(argv) => {
|
|
48
|
+
process.stdout.write(run(argv));
|
|
49
49
|
},
|
|
50
50
|
{ usage: USAGE },
|
|
51
51
|
);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The c64-testing diff script (D77): what changed between two machine states.
|
|
2
|
+
// Two snapshot files are restored one after the other in an own emulator,
|
|
3
|
+
// which reads RAM, colour RAM and the chip and CPU registers with the readers
|
|
4
|
+
// that the runs use: VICE reads its own snapshot format. Two memory files of a
|
|
5
|
+
// run, or two observations of runs (also two of one run), compare as they are.
|
|
6
|
+
// Exit status: 0, or 2 for a bad call.
|
|
7
|
+
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
|
|
10
|
+
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
11
|
+
import { WireFailure } from "#src/host-client/tools.ts";
|
|
12
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
13
|
+
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
14
|
+
import { resolveProjectPath } from "#src/project.ts";
|
|
15
|
+
import { runScript, UsageError } from "#src/script.ts";
|
|
16
|
+
import { diffStates, type ChangedRange, type MachineState } from "./lib/diff.ts";
|
|
17
|
+
|
|
18
|
+
const USAGE = `diff.ts <a> <b> [--gap N] [--max-listed N] [--ntsc] [--at <address>] [--out <file>]
|
|
19
|
+
|
|
20
|
+
<a> and <b> are two of one kind, relative to the project folder:
|
|
21
|
+
snapshot.vsf the emulator restores each one and reads RAM, colour
|
|
22
|
+
RAM, the VIC-II, CIA, SID and CPU registers
|
|
23
|
+
memory.bin a memory file of a run (observe with memory "all" or
|
|
24
|
+
a size above 4096); --at gives the address of its
|
|
25
|
+
first byte when it is shorter than 64 KiB
|
|
26
|
+
results.json:<name> the observation <name> of a run; both can be in
|
|
27
|
+
one results.json
|
|
28
|
+
|
|
29
|
+
--gap N join changed ranges across at most N equal bytes (default 0)
|
|
30
|
+
--max-listed N list at most N ranges of each part (default 64)
|
|
31
|
+
--ntsc the snapshots are of an NTSC machine (default PAL)
|
|
32
|
+
--out <file> also write all ranges to a project file
|
|
33
|
+
|
|
34
|
+
The result is one JSON object. Addresses are $ and four hex digits.`;
|
|
35
|
+
|
|
36
|
+
type Kind = "snapshot" | "memory" | "observation";
|
|
37
|
+
|
|
38
|
+
function kindOf(path: string): Kind {
|
|
39
|
+
if (/\.vsf$/i.test(path)) return "snapshot";
|
|
40
|
+
if (/\.json:[^:]+$/.test(path)) return "observation";
|
|
41
|
+
if (/\.bin$/i.test(path)) return "memory";
|
|
42
|
+
throw new UsageError(`${path} is no .vsf, .bin or results.json:<observation>`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The state of one observation of a results.json: its memory items and its registers. */
|
|
46
|
+
function observation(path: string): MachineState {
|
|
47
|
+
const [file, name] = [path.slice(0, path.lastIndexOf(":")), path.slice(path.lastIndexOf(":") + 1)];
|
|
48
|
+
let results: { observations?: Record<string, { memory?: Record<string, unknown>; registers?: Record<string, unknown> }> };
|
|
49
|
+
try {
|
|
50
|
+
results = JSON.parse(readProjectFile(file).bytes.toString("utf8")) as typeof results;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof WireFailure) throw error;
|
|
53
|
+
throw new UsageError(`${file} is not JSON: ${(error as Error).message}`);
|
|
54
|
+
}
|
|
55
|
+
const seen = results.observations?.[name];
|
|
56
|
+
if (seen === undefined) throw new UsageError(`${file} has no observation ${name}; it has ${Object.keys(results.observations ?? {}).join(", ") || "none"}`);
|
|
57
|
+
const observed: Record<string, string> = {};
|
|
58
|
+
for (const [at, value] of Object.entries(seen.memory ?? {})) {
|
|
59
|
+
// A memory item in a file of its own: compare its bytes.
|
|
60
|
+
if (typeof value === "string") observed[at] = value;
|
|
61
|
+
else if (typeof value === "object" && value !== null && typeof (value as { file?: unknown }).file === "string") {
|
|
62
|
+
const folder = file.includes("/") ? file.slice(0, file.lastIndexOf("/") + 1) : "";
|
|
63
|
+
observed[at] = readProjectFile(`${folder}${(value as { file: string }).file}`).bytes.toString("hex");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const values: Record<string, number> = {};
|
|
67
|
+
for (const [register, value] of Object.entries(seen.registers ?? {})) {
|
|
68
|
+
if (typeof value === "number") values[`cpu.${register}`] = value;
|
|
69
|
+
else if (typeof value === "string" && value.startsWith("$")) values[`cpu.${register}`] = parseC64Address(value);
|
|
70
|
+
}
|
|
71
|
+
return { observed, values };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The state of a snapshot: restored in the session, then read with the readers of the runs. */
|
|
75
|
+
async function snapshotState(client: ViceSessionClient, path: string): Promise<MachineState> {
|
|
76
|
+
await client.snapshotRestore(readProjectFile(path).bytes);
|
|
77
|
+
const memory = await readMemoryInParts(client, 0x0000, 0x10000, "ram");
|
|
78
|
+
const colorRam = Buffer.from((await client.ioRead(0xd800, 0x400)).data, "hex").map((value) => value & 0x0f);
|
|
79
|
+
const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
|
|
80
|
+
const values: Record<string, number> = {};
|
|
81
|
+
vic.forEach((value, index) => (values[`vic.${formatC64Address(0xd000 + index)}`] = value));
|
|
82
|
+
for (const chip of (await client.cia("both")).chips) {
|
|
83
|
+
for (const field of ["portA", "portB", "ddrA", "ddrB", "timerA", "timerB", "latchA", "latchB", "controlA", "controlB", "interruptMask"] as const) values[`cia${chip.id}.${field}`] = chip[field];
|
|
84
|
+
}
|
|
85
|
+
const sid = await client.sid();
|
|
86
|
+
for (const voice of sid.voices) for (const [field, value] of Object.entries(voice)) if (field !== "index" && typeof value === "number") values[`sid.voice${voice.index}.${field}`] = value;
|
|
87
|
+
const registers = await client.registersGet("c64");
|
|
88
|
+
for (const [field, value] of Object.entries(registers)) if (typeof value === "number") values[`cpu.${field}`] = value;
|
|
89
|
+
return { memory: { base: 0, bytes: memory }, colorRam, values };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const show = (range: ChangedRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end), ...(range.a === undefined ? {} : { a: range.a, b: range.b }) });
|
|
93
|
+
|
|
94
|
+
async function run(argv: string[]): Promise<Record<string, unknown>> {
|
|
95
|
+
const { values, positionals } = parseArgs({
|
|
96
|
+
args: argv,
|
|
97
|
+
strict: true,
|
|
98
|
+
allowPositionals: true,
|
|
99
|
+
options: { gap: { type: "string" }, "max-listed": { type: "string" }, ntsc: { type: "boolean" }, at: { type: "string" }, out: { type: "string" }, help: { type: "boolean" } },
|
|
100
|
+
});
|
|
101
|
+
if (values.help) throw new UsageError("");
|
|
102
|
+
if (positionals.length !== 2) throw new UsageError("give two states to compare");
|
|
103
|
+
const [first, second] = positionals as [string, string];
|
|
104
|
+
const kind = kindOf(first);
|
|
105
|
+
if (kindOf(second) !== kind) throw new UsageError("give two states of one kind: two snapshots, two memory files or two observations");
|
|
106
|
+
const number = (value: string | undefined, fallback: number, what: string) => {
|
|
107
|
+
if (value === undefined) return fallback;
|
|
108
|
+
if (!/^\d+$/.test(value)) throw new UsageError(`${what} must be a whole number`);
|
|
109
|
+
return Number(value);
|
|
110
|
+
};
|
|
111
|
+
const gap = number(values.gap, 0, "--gap");
|
|
112
|
+
const max = number(values["max-listed"], 64, "--max-listed");
|
|
113
|
+
if (values.out !== undefined) {
|
|
114
|
+
try {
|
|
115
|
+
resolveProjectPath(values.out);
|
|
116
|
+
} catch {
|
|
117
|
+
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let a: MachineState;
|
|
121
|
+
let b: MachineState;
|
|
122
|
+
if (kind === "observation") {
|
|
123
|
+
a = observation(first);
|
|
124
|
+
b = observation(second);
|
|
125
|
+
} else if (kind === "memory") {
|
|
126
|
+
let base = 0;
|
|
127
|
+
if (values.at !== undefined) {
|
|
128
|
+
try {
|
|
129
|
+
base = parseC64Address(values.at);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new UsageError(`--at must be $ followed by four hex digits, not ${values.at}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const read = (path: string) => readProjectFile(path).bytes;
|
|
135
|
+
const [left, right] = [read(first), read(second)];
|
|
136
|
+
if (left.length !== right.length) throw new UsageError(`the memory files have ${left.length} and ${right.length} bytes; give two of one size`);
|
|
137
|
+
if (base + left.length > 0x10000) throw new UsageError("the memory file goes past $ffff from --at");
|
|
138
|
+
a = { memory: { base, bytes: left } };
|
|
139
|
+
b = { memory: { base, bytes: right } };
|
|
140
|
+
} else {
|
|
141
|
+
const client = await ViceSessionClient.open({ videoStandard: values.ntsc ? "ntsc" : "pal" });
|
|
142
|
+
try {
|
|
143
|
+
a = await snapshotState(client, first);
|
|
144
|
+
b = await snapshotState(client, second);
|
|
145
|
+
} finally {
|
|
146
|
+
await client.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const diff = diffStates(a, b, gap);
|
|
150
|
+
const part = (item: { changedBytes: number; ranges: ChangedRange[] } | undefined) =>
|
|
151
|
+
item === undefined ? undefined : { changedBytes: item.changedBytes, ranges: item.ranges.length, listed: item.ranges.slice(0, max).map(show) };
|
|
152
|
+
if (values.out !== undefined) {
|
|
153
|
+
const all = { a: first, b: second, ...(diff.memory === undefined ? {} : { memory: diff.memory.ranges.map(show) }), ...(diff.colorRam === undefined ? {} : { colorRam: diff.colorRam.ranges.map(show) }), values: diff.values ?? [], observed: diff.observed ?? [] };
|
|
154
|
+
writeProjectFile(values.out, Buffer.from(`${JSON.stringify(all, null, 2)}\n`), { what: "--out" });
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
a: first,
|
|
158
|
+
b: second,
|
|
159
|
+
...(diff.memory === undefined ? {} : { memory: part(diff.memory) }),
|
|
160
|
+
...(diff.colorRam === undefined ? {} : { colorRam: part(diff.colorRam) }),
|
|
161
|
+
...(diff.values === undefined ? {} : { registers: diff.values.slice(0, max) }),
|
|
162
|
+
...(diff.observed === undefined ? {} : { observed: diff.observed.slice(0, max) }),
|
|
163
|
+
...(values.out === undefined ? {} : { out: values.out }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await runScript(run, { usage: USAGE });
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The differences between two machine states (D77): changed ranges of RAM and
|
|
2
|
+
// colour RAM, and changed chip registers and CPU registers. The states come
|
|
3
|
+
// from snapshots that VICE restores, from memory files, or from the
|
|
4
|
+
// observations of runs; this file only compares.
|
|
5
|
+
|
|
6
|
+
export interface MachineState {
|
|
7
|
+
/** Bytes from `base`: all 64 KiB of RAM, or the bytes of one memory file. */
|
|
8
|
+
memory?: { base: number; bytes: Uint8Array };
|
|
9
|
+
/** Colour RAM $D800-$DBFF, the low 4 bits of each byte. */
|
|
10
|
+
colorRam?: Uint8Array;
|
|
11
|
+
/** Named single values: VIC-II, CIA and SID registers and the CPU registers, as "vic.$d011", "cia1.timerA", "cpu.pc". */
|
|
12
|
+
values?: Record<string, number>;
|
|
13
|
+
/** The memory items of an observation of a run: the address or name, and its bytes as hex. */
|
|
14
|
+
observed?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ChangedRange {
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
/** The bytes of both states, as hex, when the range has at most 16 bytes. */
|
|
21
|
+
a?: string;
|
|
22
|
+
b?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StateDiff {
|
|
26
|
+
memory?: { changedBytes: number; ranges: ChangedRange[] };
|
|
27
|
+
colorRam?: { changedBytes: number; ranges: ChangedRange[] };
|
|
28
|
+
values?: Array<{ name: string; a: number; b: number }>;
|
|
29
|
+
observed?: Array<{ at: string; a: string; b: string }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Changed ranges of two byte arrays of one length, joined across gaps of at most `gap` equal bytes. */
|
|
33
|
+
export function changedRanges(a: Uint8Array, b: Uint8Array, base: number, gap = 0): { changedBytes: number; ranges: ChangedRange[] } {
|
|
34
|
+
const ranges: ChangedRange[] = [];
|
|
35
|
+
let changedBytes = 0;
|
|
36
|
+
for (let offset = 0; offset < a.length; offset++) {
|
|
37
|
+
if (a[offset] === b[offset]) continue;
|
|
38
|
+
changedBytes++;
|
|
39
|
+
const last = ranges.at(-1);
|
|
40
|
+
if (last !== undefined && base + offset - last.end - 1 <= gap) last.end = base + offset;
|
|
41
|
+
else ranges.push({ start: base + offset, end: base + offset });
|
|
42
|
+
}
|
|
43
|
+
for (const range of ranges) {
|
|
44
|
+
if (range.end - range.start + 1 > 16) continue;
|
|
45
|
+
range.a = Buffer.from(a.subarray(range.start - base, range.end - base + 1)).toString("hex");
|
|
46
|
+
range.b = Buffer.from(b.subarray(range.start - base, range.end - base + 1)).toString("hex");
|
|
47
|
+
}
|
|
48
|
+
return { changedBytes, ranges };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What differs between two states, for each part that both have. */
|
|
52
|
+
export function diffStates(a: MachineState, b: MachineState, gap = 0): StateDiff {
|
|
53
|
+
const result: StateDiff = {};
|
|
54
|
+
if (a.memory !== undefined && b.memory !== undefined) {
|
|
55
|
+
if (a.memory.base !== b.memory.base || a.memory.bytes.length !== b.memory.bytes.length) throw new Error("the two memory parts do not cover the same addresses");
|
|
56
|
+
result.memory = changedRanges(a.memory.bytes, b.memory.bytes, a.memory.base, gap);
|
|
57
|
+
}
|
|
58
|
+
if (a.colorRam !== undefined && b.colorRam !== undefined) result.colorRam = changedRanges(a.colorRam, b.colorRam, 0xd800, gap);
|
|
59
|
+
if (a.values !== undefined && b.values !== undefined) {
|
|
60
|
+
result.values = Object.keys(a.values)
|
|
61
|
+
.filter((name) => b.values![name] !== undefined && a.values![name] !== b.values![name])
|
|
62
|
+
.map((name) => ({ name, a: a.values![name]!, b: b.values![name]! }));
|
|
63
|
+
}
|
|
64
|
+
if (a.observed !== undefined && b.observed !== undefined) {
|
|
65
|
+
result.observed = Object.keys(a.observed)
|
|
66
|
+
.filter((at) => b.observed![at] !== undefined && a.observed![at] !== b.observed![at])
|
|
67
|
+
.map((at) => ({ at, a: a.observed![at]!, b: b.observed![at]! }));
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|