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