@henols/c64-re-tools 2.0.0-rc.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -116
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- 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 +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -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 +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- 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 +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- 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} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -5
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +25 -5
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- 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 +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +8 -2
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/package.ts +13 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
|
@@ -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,7 +38,7 @@ 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
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. |
|
|
@@ -50,7 +50,7 @@ Before the first step of each side, the script does a hard reset. The CPU stops
|
|
|
50
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
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
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":
|
|
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. |
|
|
54
54
|
| `{ "observe": "moved", ... }` | Record a checkpoint. |
|
|
55
55
|
|
|
56
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`.
|
|
@@ -67,7 +67,7 @@ An `observe` step has a checkpoint name. A scenario can have each name only one
|
|
|
67
67
|
|
|
68
68
|
| Key | What the script compares |
|
|
69
69
|
|---|---|
|
|
70
|
-
| `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. |
|
|
71
71
|
| `registers` | A list of register names: `pc`, `a`, `x`, `y`, `sp`, `flags`. |
|
|
72
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. |
|
|
73
73
|
| `vicii` | The VIC-II state: mode, screen address, scroll and colors. |
|
|
@@ -84,10 +84,14 @@ A `pc` value is an address. Compare `pc` only when both programs have the code a
|
|
|
84
84
|
A run file (for `run.ts`) has:
|
|
85
85
|
|
|
86
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": "snapshots/room0.vsf" }`.
|
|
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
88
|
- `symbols`: optional, `knowledge`, `none` (default), or a JSON file as for a subject.
|
|
89
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` and `observe`.
|
|
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`.
|
|
91
95
|
|
|
92
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.
|
|
93
97
|
|
|
@@ -96,13 +100,13 @@ Example: visit two rooms from the same start.
|
|
|
96
100
|
```json
|
|
97
101
|
{
|
|
98
102
|
"name": "rooms",
|
|
99
|
-
"start": { "snapshot": "snapshots/room0.vsf" },
|
|
103
|
+
"start": { "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" },
|
|
100
104
|
"steps": [
|
|
101
105
|
{ "write": { "at": "$0350", "bytes": [1] } },
|
|
102
106
|
{ "frames": 50 },
|
|
103
107
|
{ "screen": "room1" },
|
|
104
108
|
{ "coverage": "room1" },
|
|
105
|
-
{ "snapshot": "snapshots/room0.vsf" },
|
|
109
|
+
{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" },
|
|
106
110
|
{ "write": { "at": "$0350", "bytes": [2] } },
|
|
107
111
|
{ "frames": 50 },
|
|
108
112
|
{ "screen": "room2" },
|
|
@@ -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: 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: 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
|
+
}
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
// Observations name semantic symbols that resolve per side, comparisons are
|
|
4
4
|
// explicit, and the result is PASS, FAIL or INCONCLUSIVE.
|
|
5
5
|
|
|
6
|
-
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
7
|
-
import { MAX_BASELINES, MAX_MASK_RECTANGLES,
|
|
6
|
+
import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
7
|
+
import { MAX_BASELINES, MAX_MASK_RECTANGLES, 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";
|
|
11
|
-
import type
|
|
11
|
+
import { readMemoryInParts, type ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
12
12
|
import { openForRead } from "#src/knowledge/database.ts";
|
|
13
|
-
import { listSymbols } from "#src/knowledge/read.ts";
|
|
13
|
+
import { listAnalyzerNames, listSymbols } from "#src/knowledge/read.ts";
|
|
14
|
+
import { programImage } from "#src/knowledge/program.ts";
|
|
14
15
|
|
|
15
16
|
export class ScenarioError extends Error {}
|
|
16
17
|
|
|
@@ -22,11 +23,13 @@ export type { Location };
|
|
|
22
23
|
|
|
23
24
|
export interface MemoryObservation {
|
|
24
25
|
at: Location;
|
|
25
|
-
/** Bytes from `at`; 1 when not given. */
|
|
26
|
+
/** Bytes from `at`; 1 when not given, at most 64 KiB. */
|
|
26
27
|
size?: number;
|
|
28
|
+
/** cpu (the default) reads as the CPU sees memory; ram reads the RAM under the ROMs and the I/O area. */
|
|
29
|
+
view?: "cpu" | "ram";
|
|
27
30
|
/** The largest allowed difference per byte; 0 when not given. */
|
|
28
31
|
tolerance?: number;
|
|
29
|
-
/** Bytes in the range that the comparison leaves out, each with the reason
|
|
32
|
+
/** Bytes in the range that the comparison leaves out, each with the reason. */
|
|
30
33
|
exclude?: Array<{ at: Location; reason: string }>;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -42,16 +45,16 @@ type StepBody =
|
|
|
42
45
|
| { load: true }
|
|
43
46
|
| { autostart: true }
|
|
44
47
|
| { snapshot: string }
|
|
45
|
-
| { normalize:
|
|
48
|
+
| { normalize: true }
|
|
46
49
|
| ActionStep
|
|
47
50
|
| { observe: string; memory?: Array<Location | MemoryObservation>; registers?: ObservedRegister[]; screen?: ScreenPolicy; vicii?: boolean; sprites?: number[] };
|
|
48
51
|
|
|
49
|
-
/** A step; with side, only that side does it
|
|
52
|
+
/** A step; with side, only that side does it. An observe step has no side: it compares both. */
|
|
50
53
|
export type Step = StepBody & { side?: Side };
|
|
51
54
|
|
|
52
55
|
export interface Subject {
|
|
53
56
|
program: string;
|
|
54
|
-
/** "knowledge" for knowledge
|
|
57
|
+
/** "knowledge" for the project knowledge, a project JSON file (c64-assembler output or a name → "$xxxx" map), or "none". */
|
|
55
58
|
symbols: string;
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -79,7 +82,7 @@ export interface ScenarioResult {
|
|
|
79
82
|
reason?: string;
|
|
80
83
|
checkpoints: string[];
|
|
81
84
|
differences: Difference[];
|
|
82
|
-
/** The bytes that the comparison left out, with their reasons
|
|
85
|
+
/** The bytes that the comparison left out, with their reasons. */
|
|
83
86
|
excluded?: Array<{ checkpoint: string; at: Location; reason: string }>;
|
|
84
87
|
/** The files that a FAIL wrote with --out. */
|
|
85
88
|
files?: string[];
|
|
@@ -155,7 +158,7 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
|
|
|
155
158
|
if (typeof step.snapshot !== "string" || step.snapshot === "") fail(`${at}: snapshot needs a .vsf file in the project`);
|
|
156
159
|
return;
|
|
157
160
|
case "normalize":
|
|
158
|
-
if (
|
|
161
|
+
if (step.normalize !== true) fail(`${at}: normalize is true: it writes the entry state of the knowledge`);
|
|
159
162
|
return;
|
|
160
163
|
case "write":
|
|
161
164
|
case "registers":
|
|
@@ -178,7 +181,8 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
178
181
|
checkLocation(item, `${at}: memory`, fail);
|
|
179
182
|
continue;
|
|
180
183
|
}
|
|
181
|
-
if (!isObject(item) || Object.keys(item).some((key) => !["at", "size", "tolerance", "exclude"].includes(key))) return fail(`${at}: a memory object has at, size, tolerance and exclude, and nothing else`);
|
|
184
|
+
if (!isObject(item) || Object.keys(item).some((key) => !["at", "size", "view", "tolerance", "exclude"].includes(key))) return fail(`${at}: a memory object has at, size, view, tolerance and exclude, and nothing else`);
|
|
185
|
+
if (item.view !== undefined && item.view !== "cpu" && item.view !== "ram") fail(`${at}: memory.view must be cpu or ram`);
|
|
182
186
|
if (item.exclude !== undefined) {
|
|
183
187
|
if (!Array.isArray(item.exclude) || item.exclude.length === 0) fail(`${at}: memory.exclude must be a list of objects with at and reason`);
|
|
184
188
|
for (const excluded of item.exclude as unknown[]) {
|
|
@@ -188,7 +192,7 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
checkLocation(item.at, `${at}: memory.at`, fail);
|
|
191
|
-
if (item.size !== undefined && !isInteger(item.size, 1,
|
|
195
|
+
if (item.size !== undefined && !isInteger(item.size, 1, 0x10000)) fail(`${at}: memory.size must be a number from 1 to 65536`);
|
|
192
196
|
if (item.tolerance !== undefined && !isInteger(item.tolerance, 0, 0xff)) fail(`${at}: memory.tolerance must be a number from 0 to 255`);
|
|
193
197
|
}
|
|
194
198
|
}
|
|
@@ -216,7 +220,7 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
216
220
|
isInteger(rectangle.height, 1, 4096);
|
|
217
221
|
if (!ok) fail(`${at}: a screen.mask rectangle needs whole numbers x and y from 0, and width and height from 1`);
|
|
218
222
|
}
|
|
219
|
-
// The emulator refuses such a compare too, but only after the original side ran
|
|
223
|
+
// The emulator refuses such a compare too, but only after the original side ran.
|
|
220
224
|
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`);
|
|
221
225
|
}
|
|
222
226
|
}
|
|
@@ -236,6 +240,10 @@ export function loadSymbols(subject: Subject): Map<string, number> {
|
|
|
236
240
|
if (subject.symbols === "knowledge") {
|
|
237
241
|
const db = openForRead();
|
|
238
242
|
for (const symbol of listSymbols(db)) symbols.set(symbol.name, symbol.address);
|
|
243
|
+
// An analyzer name gives an address too, when no symbol has the name and only one analyzer gives it.
|
|
244
|
+
const second = new Map<string, number[]>();
|
|
245
|
+
for (const row of listAnalyzerNames(db)) second.set(row.name, [...(second.get(row.name) ?? []), row.address]);
|
|
246
|
+
for (const [name, addresses] of second) if (!symbols.has(name) && new Set(addresses).size === 1) symbols.set(name, addresses[0]!);
|
|
239
247
|
db?.close();
|
|
240
248
|
return symbols;
|
|
241
249
|
}
|
|
@@ -248,11 +256,12 @@ export function loadSymbols(subject: Subject): Map<string, number> {
|
|
|
248
256
|
// c64-assembler output, or a plain name → "$xxxx" map. A constant in the
|
|
249
257
|
// assembler output counts too: RAM variables such as "counter = $c100" lie
|
|
250
258
|
// outside the program, so the assembler reports them as constants.
|
|
251
|
-
const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?:
|
|
259
|
+
const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?: string }> }).symbols;
|
|
252
260
|
const entries: Array<[string, unknown]> = Array.isArray(assembled)
|
|
253
261
|
? assembled.flatMap((symbol): Array<[string, unknown]> => {
|
|
254
262
|
if (symbol.address !== undefined) return [[symbol.name, symbol.address]];
|
|
255
|
-
|
|
263
|
+
// A constant above $ffff has more digits and is no address.
|
|
264
|
+
if (typeof symbol.value === "string" && /^\$[0-9a-f]{4}$/.test(symbol.value)) return [[symbol.name, symbol.value]];
|
|
256
265
|
return [];
|
|
257
266
|
})
|
|
258
267
|
: Object.entries(value as Record<string, unknown>);
|
|
@@ -288,24 +297,25 @@ interface Observation {
|
|
|
288
297
|
screen?: { match: boolean; mismatchingPixels: number; comparedPixels: number; mismatchRatio: number; bounds?: unknown };
|
|
289
298
|
vicii?: unknown;
|
|
290
299
|
sprites?: unknown;
|
|
291
|
-
/** PNG images for the files of a FAIL
|
|
300
|
+
/** PNG images for the files of a FAIL: the side's screen, and on the rebuild the difference image. */
|
|
292
301
|
png?: string;
|
|
293
302
|
diffPng?: string;
|
|
294
303
|
}
|
|
295
304
|
|
|
296
|
-
/** Writes pages 0-3 and $00/$01 from
|
|
297
|
-
async function normalize(client: ViceSessionClient
|
|
298
|
-
|
|
305
|
+
/** Writes pages 0-3 and $00/$01 from the entry state of the knowledge: both sides then start from the same RAM. */
|
|
306
|
+
async function normalize(client: ViceSessionClient): Promise<void> {
|
|
307
|
+
const db = openForRead();
|
|
308
|
+
let state: { pages?: Record<string, string>; cpuPort?: Record<string, number> } | undefined;
|
|
299
309
|
try {
|
|
300
|
-
state =
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
throw new ScenarioError(`${path} is not an entry state file: ${(error as Error).message}`);
|
|
310
|
+
state = programImage(db)?.state as typeof state;
|
|
311
|
+
} finally {
|
|
312
|
+
db?.close();
|
|
304
313
|
}
|
|
314
|
+
if (state === undefined) throw new ScenarioError("normalize needs the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json> of the c64-knowledge skill");
|
|
305
315
|
const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => state.pages?.[page]);
|
|
306
316
|
const port = [state.cpuPort?.$00, state.cpuPort?.$01];
|
|
307
317
|
if (pages.some((page) => typeof page !== "string" || !/^[0-9a-f]{512}$/.test(page)) || port.some((value) => !isInteger(value, 0, 0xff))) {
|
|
308
|
-
throw new ScenarioError(
|
|
318
|
+
throw new ScenarioError("the entry state of the knowledge is not the entry state of a c64-unpacker capture: it needs pages $0000 to $0300 and cpuPort $00 and $01");
|
|
309
319
|
}
|
|
310
320
|
if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
|
|
311
321
|
// Pages 0 to 3 in RAM from $0002, then the CPU port: the port bits switch what the CPU sees.
|
|
@@ -348,7 +358,7 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
348
358
|
const spec: MemoryObservation = typeof item === "string" ? { at: item } : item;
|
|
349
359
|
const address = at(spec.at);
|
|
350
360
|
const size = spec.size ?? 1;
|
|
351
|
-
const read = await client
|
|
361
|
+
const read = { data: (await readMemoryInParts(client, address, size, spec.view ?? "cpu")).toString("hex") };
|
|
352
362
|
const excluded = (spec.exclude ?? []).map((exclude) => {
|
|
353
363
|
const offset = at(exclude.at) - address;
|
|
354
364
|
if (offset < 0 || offset >= size) throw new ScenarioError(`the exclude ${exclude.at} lies outside the observed ${spec.at} (${size} bytes)`);
|
|
@@ -358,14 +368,14 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
358
368
|
}
|
|
359
369
|
if (step.registers !== undefined) {
|
|
360
370
|
const registers = await client.registersGet("c64");
|
|
361
|
-
for (const name of step.registers) observation.registers[name] = registers[name];
|
|
371
|
+
for (const name of step.registers) observation.registers[name] = name === "pc" ? formatC64Address(registers.pc) : registers[name];
|
|
362
372
|
}
|
|
363
373
|
if (step.vicii) {
|
|
364
374
|
// The raster line changes from one moment to the next; the comparison leaves it out.
|
|
365
375
|
const { rasterLine: _rasterLine, ...state } = await client.vicii();
|
|
366
|
-
observation.vicii = state;
|
|
376
|
+
observation.vicii = formatVicii(state);
|
|
367
377
|
}
|
|
368
|
-
if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites;
|
|
378
|
+
if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
|
|
369
379
|
if (step.screen !== undefined) {
|
|
370
380
|
const baseline = baselineName(checkpoint, step.observe);
|
|
371
381
|
if (side === "original") {
|
|
@@ -392,7 +402,7 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
392
402
|
} else if ("snapshot" in step) {
|
|
393
403
|
await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
|
|
394
404
|
} else if ("normalize" in step) {
|
|
395
|
-
await normalize(client
|
|
405
|
+
await normalize(client);
|
|
396
406
|
} else {
|
|
397
407
|
const { side: _side, ...action } = step;
|
|
398
408
|
const notReached = await runActionStep(client, action as ActionStep, at);
|
|
@@ -413,7 +423,7 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
413
423
|
const other = right.memory[index]!;
|
|
414
424
|
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
415
425
|
if (item.bytes.some((byte, at) => !skipped.has(at) && Math.abs(byte - other.bytes[at]!) > item.tolerance)) {
|
|
416
|
-
const show = (bytes: number[]) =>
|
|
426
|
+
const show = (bytes: number[]) => Buffer.from(bytes).toString("hex");
|
|
417
427
|
differences.push({ checkpoint, what: `memory ${item.label}`, original: show(item.bytes), rebuild: show(other.bytes) });
|
|
418
428
|
}
|
|
419
429
|
});
|
|
@@ -430,7 +440,7 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
430
440
|
}
|
|
431
441
|
|
|
432
442
|
/**
|
|
433
|
-
* Writes the files of a FAIL into `out
|
|
443
|
+
* Writes the files of a FAIL into `out`: for a screen difference both
|
|
434
444
|
* screens and the difference image, and memory.json with each differing byte.
|
|
435
445
|
*/
|
|
436
446
|
function writeFailFiles(out: string, original: Map<string, Observation>, rebuild: Map<string, Observation>, differences: Difference[]): string[] {
|
|
@@ -439,7 +449,8 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
|
|
|
439
449
|
writeProjectFile(`${out}/${name}`, bytes, { what: "--out" });
|
|
440
450
|
files.push(name);
|
|
441
451
|
};
|
|
442
|
-
const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original:
|
|
452
|
+
const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original: string; rebuild: string }> }> = [];
|
|
453
|
+
const hex = (byte: number) => byte.toString(16).padStart(2, "0");
|
|
443
454
|
for (const difference of differences) {
|
|
444
455
|
const left = original.get(difference.checkpoint)!;
|
|
445
456
|
const right = rebuild.get(difference.checkpoint)!;
|
|
@@ -454,7 +465,7 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
|
|
|
454
465
|
const item = left.memory[index]!;
|
|
455
466
|
const other = right.memory[index]!;
|
|
456
467
|
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
457
|
-
const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: byte, rebuild: other.bytes[offset]! }] : []));
|
|
468
|
+
const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: hex(byte), rebuild: hex(other.bytes[offset]!) }] : []));
|
|
458
469
|
memory.push({ checkpoint: difference.checkpoint, what: difference.what, bytes });
|
|
459
470
|
}
|
|
460
471
|
}
|