@henols/c64-re-tools 0.1.4
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 +61 -0
- package/bin/cli.mjs +226 -0
- package/package.json +53 -0
- package/skills/acme-build/SKILL.md +224 -0
- package/skills/acme-build/scripts/acme.mjs +263 -0
- package/skills/acme-build/template.a +39 -0
- package/skills/c64-memory-mapping/SKILL.md +199 -0
- package/skills/c64-memory-mapping/memmap.json +8800 -0
- package/skills/c64-memory-mapping/scripts/driver.mjs +553 -0
- package/skills/c64-program-recon/SKILL.md +172 -0
- package/skills/c64-program-recon/references/control-flow.md +174 -0
- package/skills/c64-program-recon/references/graphics.md +73 -0
- package/skills/c64-program-recon/references/observation-hazards.md +118 -0
- package/skills/c64-program-recon/references/reconstruction.md +128 -0
- package/skills/c64-program-recon/references/sound-and-input.md +68 -0
- package/skills/c64-program-recon/references/tool-selection.md +55 -0
- package/skills/c64-program-recon/scripts/derive.mjs +364 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +62 -0
- package/skills/c64-provenance-diff/SKILL.md +257 -0
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +981 -0
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +665 -0
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +383 -0
- package/skills/c64-ram-capture/SKILL.md +306 -0
- package/skills/c64-ram-capture/scripts/compare.mjs +258 -0
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +243 -0
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +243 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +317 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +133 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +81 -0
- package/skills/c64-ram-capture/scripts/releases.mjs +109 -0
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +75 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +575 -0
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +339 -0
- package/skills/c64-ram-capture/templates/capture-record.template.md +59 -0
- package/skills/vice-wedge-triage/SKILL.md +149 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ACME -> C64 assembler driver. Target is fixed: C64, 6510 CPU, cbm output.
|
|
3
|
+
// Scope is assembling only: source in, .prg + symbol files out. Running the
|
|
4
|
+
// result on a C64 belongs to the emulator skill.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join, basename, relative, isAbsolute } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
11
|
+
const HERE = dirname(SELF);
|
|
12
|
+
|
|
13
|
+
// The ACME library (<cbm/c64/vic.a> and friends) lives wherever the package put
|
|
14
|
+
// it. Probe instead of assuming; validated by a file we actually include.
|
|
15
|
+
const LIB_MARKER = join("cbm", "c64", "vic.a");
|
|
16
|
+
function findAcmeLib() {
|
|
17
|
+
const tried = [];
|
|
18
|
+
for (const c of [
|
|
19
|
+
process.env.ACME,
|
|
20
|
+
"/usr/local/share/acme", "/usr/share/acme", "/usr/lib/acme",
|
|
21
|
+
process.env.HOME && join(process.env.HOME, ".acme"),
|
|
22
|
+
].filter(Boolean)) {
|
|
23
|
+
tried.push(c);
|
|
24
|
+
if (existsSync(join(c, LIB_MARKER))) return { path: c, tried };
|
|
25
|
+
}
|
|
26
|
+
return { path: null, tried };
|
|
27
|
+
}
|
|
28
|
+
const ACME_LIB = findAcmeLib();
|
|
29
|
+
|
|
30
|
+
// How to refer to this script in hints, from wherever we were run.
|
|
31
|
+
function selfPath() {
|
|
32
|
+
const r = relative(process.cwd(), SELF);
|
|
33
|
+
return !r || r.startsWith("..") || isAbsolute(r) ? SELF : r;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
|
|
37
|
+
|
|
38
|
+
// ------------------------------------------------------------------- build
|
|
39
|
+
|
|
40
|
+
// ACME's --msvc format: file(line) : Error (Zone <z>): message.
|
|
41
|
+
const MSVC = /^(.*?)\((\d+)\)\s*:\s*(Error|Warning|Serious error)\s*(?:\(([^)]*)\))?\s*:\s*(.*)$/;
|
|
42
|
+
|
|
43
|
+
function parseDiagnostics(text) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const line of text.split("\n")) {
|
|
46
|
+
const m = line.match(MSVC);
|
|
47
|
+
if (m) {
|
|
48
|
+
out.push({
|
|
49
|
+
file: m[1], line: Number(m[2]),
|
|
50
|
+
severity: m[3].toLowerCase().replace(" ", "_"),
|
|
51
|
+
zone: m[4] || null, message: m[5].trim(),
|
|
52
|
+
});
|
|
53
|
+
} else if (line.trim()) {
|
|
54
|
+
out.push({ file: null, line: null, severity: "note", zone: null, message: line.trim() });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The symbol list marks address-typed symbols with a leading "!addr" and
|
|
61
|
+
// never-referenced ones with a trailing "; unused". Both matter downstream.
|
|
62
|
+
function parseSymbols(path) {
|
|
63
|
+
if (!existsSync(path)) return [];
|
|
64
|
+
return readFileSync(path, "utf8").split("\n").flatMap((raw) => {
|
|
65
|
+
const m = raw.match(/^(!addr\s+)?(\S+)\s*=\s*(\S+?)\s*(?:;\s*(.*))?$/);
|
|
66
|
+
if (!m) return [];
|
|
67
|
+
return [{
|
|
68
|
+
name: m[2], value: m[3],
|
|
69
|
+
isAddress: Boolean(m[1]),
|
|
70
|
+
used: !/^unused/.test(m[4] || ""),
|
|
71
|
+
}];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ACME's label file lists every global symbol, constants included, in address
|
|
76
|
+
// form. A debugger reads `viccolor_WHITE = $1` as a name for address $0001 and
|
|
77
|
+
// relabels the 6510 processor port with it. Keep referenced addresses only, so
|
|
78
|
+
// the emitted file is safe to load anywhere.
|
|
79
|
+
function curateLabels(vsPath, symbols) {
|
|
80
|
+
if (!existsSync(vsPath)) return { kept: 0, dropped: 0 };
|
|
81
|
+
const addr = new Set(symbols.filter((s) => s.isAddress && s.used).map((s) => s.name));
|
|
82
|
+
const kept = [];
|
|
83
|
+
let dropped = 0;
|
|
84
|
+
for (const l of readFileSync(vsPath, "utf8").split("\n")) {
|
|
85
|
+
const m = l.match(/^al\s+C:[0-9a-f]+\s+\.(\S+)/i);
|
|
86
|
+
if (!m) continue;
|
|
87
|
+
if (addr.has(m[1])) kept.push(l); else dropped++;
|
|
88
|
+
}
|
|
89
|
+
writeFileSync(vsPath, kept.join("\n") + (kept.length ? "\n" : ""));
|
|
90
|
+
return { kept: kept.length, dropped };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function build(src, opts) {
|
|
94
|
+
if (!existsSync(src)) die(`no such source file: ${src}`);
|
|
95
|
+
// Side files follow the .prg, not the source: two -DVARIANT builds of one
|
|
96
|
+
// source must not overwrite each other's symbol tables.
|
|
97
|
+
const prg = opts.out || join(opts.outDir || dirname(src),
|
|
98
|
+
basename(src).replace(/\.(a|asm|s)$/i, "") + ".prg");
|
|
99
|
+
const outDir = dirname(prg);
|
|
100
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
101
|
+
const stem = prg.replace(/\.prg$/i, "");
|
|
102
|
+
|
|
103
|
+
const args = [
|
|
104
|
+
"--cpu", "6510", // C64: enables the 6510 illegal opcodes
|
|
105
|
+
"-f", opts.format || "cbm", // cbm = 2-byte load address, what LOAD wants
|
|
106
|
+
"-Wtype-mismatch", // catches a missing '#' on an immediate
|
|
107
|
+
"--strict-segments", // overlapping segments are reported as errors
|
|
108
|
+
"--msvc", // machine-parseable diagnostics
|
|
109
|
+
"-v1", // report the address range actually emitted
|
|
110
|
+
"-o", prg,
|
|
111
|
+
"-l", `${stem}.sym`,
|
|
112
|
+
"--vicelabels", `${stem}.vs`,
|
|
113
|
+
];
|
|
114
|
+
if (!opts.noReport) args.push("-r", `${stem}.rep`);
|
|
115
|
+
for (const d of opts.defines) args.push(`-D${d}`);
|
|
116
|
+
for (const i of opts.includes) args.push("-I", i);
|
|
117
|
+
if (opts.setpc) args.push("--setpc", opts.setpc);
|
|
118
|
+
args.push(src);
|
|
119
|
+
|
|
120
|
+
// `<cbm/c64/vic.a>` style includes resolve through the ACME env var, so set
|
|
121
|
+
// it here rather than depending on the shell environment carrying it.
|
|
122
|
+
const env = { ...process.env };
|
|
123
|
+
if (ACME_LIB.path) env.ACME = ACME_LIB.path;
|
|
124
|
+
const r = spawnSync("acme", args, { encoding: "utf8", env });
|
|
125
|
+
if (r.error) {
|
|
126
|
+
die(r.error.code === "ENOENT"
|
|
127
|
+
? "install the ACME cross assembler and put `acme` on PATH"
|
|
128
|
+
: String(r.error));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const diags = parseDiagnostics(((r.stderr || "") + (r.stdout || "")).trim());
|
|
132
|
+
if (diags.some((d) => /ACME.*environment variable/i.test(d.message))) {
|
|
133
|
+
diags.push({
|
|
134
|
+
file: null, line: null, severity: "note", zone: null,
|
|
135
|
+
message: `for <...> includes, set $ACME to the directory holding ${LIB_MARKER} ` +
|
|
136
|
+
`(looked in: ${ACME_LIB.tried.join(", ")})`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const errors = diags.filter((d) => d.severity.endsWith("error"));
|
|
140
|
+
const ok = r.status === 0 && existsSync(prg);
|
|
141
|
+
|
|
142
|
+
let range = null, size = null, symbols = [], labels = null;
|
|
143
|
+
if (ok) {
|
|
144
|
+
symbols = parseSymbols(`${stem}.sym`);
|
|
145
|
+
labels = curateLabels(`${stem}.vs`, symbols);
|
|
146
|
+
const buf = readFileSync(prg);
|
|
147
|
+
size = buf.length;
|
|
148
|
+
if ((opts.format || "cbm") === "cbm" && buf.length >= 2) {
|
|
149
|
+
const load = buf[0] | (buf[1] << 8);
|
|
150
|
+
range = { load, end: load + buf.length - 2, bytes: buf.length - 2 };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { ok, prg, stem, diags, errors, symbols, labels, range, size };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const hex = (n, w = 4) => n.toString(16).padStart(w, "0");
|
|
157
|
+
|
|
158
|
+
function reportBuild(res, { json }) {
|
|
159
|
+
if (json) { console.log(JSON.stringify(res, null, 2)); return; }
|
|
160
|
+
for (const d of res.diags) {
|
|
161
|
+
if (d.file) console.log(`${d.file}:${d.line}: ${d.severity}: ${d.message}`);
|
|
162
|
+
else console.log(` ${d.message}`);
|
|
163
|
+
}
|
|
164
|
+
if (!res.ok) { console.error(`build FAILED (${res.errors.length} error(s))`); return; }
|
|
165
|
+
const r = res.range;
|
|
166
|
+
console.log(
|
|
167
|
+
`built ${res.prg} (${res.size} bytes)` +
|
|
168
|
+
(r ? ` load $${hex(r.load)}-$${hex(r.end)} ${r.bytes} bytes of code` : "")
|
|
169
|
+
);
|
|
170
|
+
const used = res.symbols.filter((s) => s.used).length;
|
|
171
|
+
console.log(`symbols: ${res.stem}.sym (${used} used / ${res.symbols.length} total)`);
|
|
172
|
+
if (res.labels) {
|
|
173
|
+
console.log(`debug labels: ${res.stem}.vs (${res.labels.kept} addresses)`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// -------------------------------------------------------------------- verbs
|
|
178
|
+
|
|
179
|
+
function cmdBuild(argv) {
|
|
180
|
+
const o = parseOpts(argv);
|
|
181
|
+
const res = build(o.src, o);
|
|
182
|
+
reportBuild(res, o);
|
|
183
|
+
process.exit(res.ok ? 0 : 1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function cmdSym(argv) {
|
|
187
|
+
const o = parseOpts(argv);
|
|
188
|
+
const res = build(o.src, { ...o, noReport: true });
|
|
189
|
+
if (!res.ok) { reportBuild(res, o); process.exit(1); }
|
|
190
|
+
const used = res.symbols.filter((s) => s.used).sort((a, b) => a.name.localeCompare(b.name));
|
|
191
|
+
if (o.json) { console.log(JSON.stringify(used, null, 2)); return; }
|
|
192
|
+
for (const s of used) console.log(`${s.isAddress ? "addr " : "const"} ${s.value.padStart(6)} ${s.name}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// A skeleton that is correct on the first try: BASIC stub with a computed SYS
|
|
196
|
+
// target, the C64 symbol libraries, and no !to (the CLI supplies -o).
|
|
197
|
+
function cmdNew(argv) {
|
|
198
|
+
const path = argv[0];
|
|
199
|
+
if (!path) die("usage: new <file.a>");
|
|
200
|
+
if (existsSync(path)) die(`${path} already exists`);
|
|
201
|
+
// template.a lives at the skill root, one level up from scripts/, by
|
|
202
|
+
// decision (D-03): only .mjs modules move into scripts/.
|
|
203
|
+
writeFileSync(path, readFileSync(join(HERE, "..", "template.a"), "utf8"));
|
|
204
|
+
console.log(`wrote ${path}`);
|
|
205
|
+
console.log(`next: node ${selfPath()} build ${path}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// `toacme` ships with ACME and turns object code back into ACME source.
|
|
209
|
+
function cmdDisasm(argv) {
|
|
210
|
+
const src = argv[0];
|
|
211
|
+
if (!src) die("usage: disasm <file.prg> [out.a]");
|
|
212
|
+
const out = argv[1] || src.replace(/\.prg$/i, "") + ".dis.a";
|
|
213
|
+
const r = spawnSync("toacme", ["object", src, out], { encoding: "utf8" });
|
|
214
|
+
if (r.error) die("install the ACME cross assembler and put `toacme` on PATH");
|
|
215
|
+
if (r.status !== 0) die(`toacme: ${(r.stderr || r.stdout).trim()}`);
|
|
216
|
+
const n = readFileSync(out, "utf8").split("\n").filter((l) => /^L[0-9a-f]{4}/.test(l)).length;
|
|
217
|
+
console.log(`${out}: ${n} lines`);
|
|
218
|
+
console.log("Read it as a linear decode: trust the instruction stream, and");
|
|
219
|
+
console.log("treat strings, tables and the BASIC stub as data. To reassemble,");
|
|
220
|
+
console.log("define the out-of-range labels it emits (Ld020, Lffd2, ...) and");
|
|
221
|
+
console.log("indent its illegal-opcode lines to the operand column.");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ------------------------------------------------------------------ options
|
|
225
|
+
|
|
226
|
+
function parseOpts(argv) {
|
|
227
|
+
const o = { defines: [], includes: [], json: false };
|
|
228
|
+
const rest = [];
|
|
229
|
+
for (let i = 0; i < argv.length; i++) {
|
|
230
|
+
const a = argv[i];
|
|
231
|
+
if (a === "--json") o.json = true;
|
|
232
|
+
else if (a === "--no-report") o.noReport = true;
|
|
233
|
+
else if (a === "-o" || a === "--out") o.out = argv[++i];
|
|
234
|
+
else if (a === "--out-dir") o.outDir = argv[++i];
|
|
235
|
+
else if (a === "-f" || a === "--format") o.format = argv[++i];
|
|
236
|
+
else if (a === "--setpc") o.setpc = argv[++i];
|
|
237
|
+
else if (a === "-D") o.defines.push(argv[++i]);
|
|
238
|
+
else if (a.startsWith("-D")) o.defines.push(a.slice(2));
|
|
239
|
+
else if (a === "-I") o.includes.push(argv[++i]);
|
|
240
|
+
else rest.push(a);
|
|
241
|
+
}
|
|
242
|
+
o.src = rest[0];
|
|
243
|
+
if (!o.src) die("no source file given");
|
|
244
|
+
return o;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --------------------------------------------------------------------- main
|
|
248
|
+
|
|
249
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
250
|
+
const VERBS = { new: cmdNew, build: cmdBuild, sym: cmdSym, disasm: cmdDisasm };
|
|
251
|
+
if (!cmd || !VERBS[cmd]) {
|
|
252
|
+
console.log(`usage: node ${selfPath()} <command> [options]
|
|
253
|
+
|
|
254
|
+
new <file.a> scaffold a C64 program (BASIC stub + libs)
|
|
255
|
+
build <file.a> assemble -> .prg .sym .vs .rep
|
|
256
|
+
sym <file.a> list the symbols the program uses
|
|
257
|
+
disasm <file.prg> [out.a] turn object code back into ACME source
|
|
258
|
+
|
|
259
|
+
options: -o FILE --out-dir DIR -f FORMAT --setpc ADDR -DSYM=VAL -I DIR
|
|
260
|
+
--no-report --json`);
|
|
261
|
+
process.exit(cmd ? 1 : 0);
|
|
262
|
+
}
|
|
263
|
+
VERBS[cmd](rest);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
; C64 / 6510 program skeleton.
|
|
2
|
+
; Build: node .claude/skills/acme-build/scripts/acme.mjs build THIS.a
|
|
3
|
+
; No !to here on purpose - the driver passes -o, and having both makes ACME
|
|
4
|
+
; warn "Output file already chosen" and silently ignore the !to.
|
|
5
|
+
|
|
6
|
+
!cpu 6510 ; C64's CPU: legal 6502 + the illegal opcodes
|
|
7
|
+
!source <cbm/c64/vic.a> ; vic_* registers, viccolor_* constants
|
|
8
|
+
!source <cbm/c64/kernal.a> ; k_* KERNAL entry points ($ff81-$fff5)
|
|
9
|
+
!source <cbm/c64/cia1.a> ; cia1_* keyboard / joystick 2
|
|
10
|
+
|
|
11
|
+
* = $0801 ; start of BASIC RAM
|
|
12
|
+
|
|
13
|
+
; --- BASIC stub: "10 SYS <entry>", so LOAD"...",8,1 + RUN reaches the code.
|
|
14
|
+
; The SYS digits are computed from `entry`, so inserting code above never
|
|
15
|
+
; breaks the stub - which is the classic way to lose an afternoon.
|
|
16
|
+
!word .eol, 10 ; link to next line, line number
|
|
17
|
+
!byte $9e ; SYS token
|
|
18
|
+
!byte '0' + entry % 10000 / 1000
|
|
19
|
+
!byte '0' + entry % 1000 / 100
|
|
20
|
+
!byte '0' + entry % 100 / 10
|
|
21
|
+
!byte '0' + entry % 10
|
|
22
|
+
!byte 0 ; end of BASIC line
|
|
23
|
+
.eol !word 0 ; end of BASIC program
|
|
24
|
+
|
|
25
|
+
entry
|
|
26
|
+
lda #viccolor_BLACK
|
|
27
|
+
sta vic_cbg ; $d021 background
|
|
28
|
+
lda #viccolor_GREEN
|
|
29
|
+
sta vic_cborder ; $d020 border
|
|
30
|
+
|
|
31
|
+
ldx #0
|
|
32
|
+
- lda .msg,x
|
|
33
|
+
beq +
|
|
34
|
+
jsr k_chrout ; $ffd2 print one PETSCII char
|
|
35
|
+
inx
|
|
36
|
+
bne -
|
|
37
|
+
+ rts
|
|
38
|
+
|
|
39
|
+
.msg !pet "hello from acme", 13, 0
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-memory-mapping
|
|
3
|
+
description: Look up what any C64 address means and turn raw 6502 disassembly into documented assembly, by resolving every address against the C64 memory map, KERNAL ROM routine list, canonical assembler symbols, and per-bit VIC-II/SID/CIA register tables. Use when asked to annotate or comment assembly, document a disassembly listing, or look up an address like $D020, $EA24 or $FFD2.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# C64 memory mapping & annotated disassembly
|
|
7
|
+
|
|
8
|
+
Look up what a C64 address means, and document a 6502 listing by resolving every
|
|
9
|
+
address it touches. One script does both, offline, anywhere Node ≥18 runs:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
D=.claude/skills/c64-memory-mapping/scripts/driver.mjs # relative to the repo root
|
|
13
|
+
|
|
14
|
+
node $D lookup '$D011' '$FFD2' # what lives at an address
|
|
15
|
+
node $D annotate --file game.asm # document a listing or .asm file
|
|
16
|
+
… | node $D annotate # ... or one piped in on stdin
|
|
17
|
+
node $D memmap # refresh the table from its sources (needs network)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Write an address however the source you copied it from wrote it — hex `$D011`,
|
|
21
|
+
`0xD011`, `D011h` or `D011`, binary `%1101000000010001`, or decimal `53265` all
|
|
22
|
+
reach the same register, in either letter case.
|
|
23
|
+
|
|
24
|
+
## Look up an address
|
|
25
|
+
|
|
26
|
+
`lookup` prints the full published prose for an address, most specific match
|
|
27
|
+
first, each tagged with the source it came from and the memory region it sits in.
|
|
28
|
+
Registers carry their per-bit breakdown, and the wider regions enclosing the
|
|
29
|
+
address follow it:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
=== $D011 ===
|
|
33
|
+
$D011 [$D000-$D3FF, 53248-54271 VIC-II; video display] <sta>
|
|
34
|
+
Screen control register #1. Bits: Bits #0-#2: Vertical raster scroll. Bit #3: Screen height; 0 =
|
|
35
|
+
24 rows; 1 = 25 rows. Bit #4: 0 = Screen off, complete screen is covered by border; 1 = Screen
|
|
36
|
+
on, normal screen contents are visible. Bit #5: 0 = Text mode; 1 = Bitmap mode. Bit #6: 1 =
|
|
37
|
+
Extended background mode on. Bit #7: Read: Current raster line (bit #8). Write: Raster line to
|
|
38
|
+
generate interrupt at (bit #8). Default: $1B, %00011011.
|
|
39
|
+
$D011 [MOS 6566 VIDEO INTERFACE CONTROLLER (VIC)] <io>
|
|
40
|
+
VIC Control Register
|
|
41
|
+
bit 7 Raster Compare: (Bit 8) See 53266
|
|
42
|
+
bit 6 Extended Color Text Mode 1 = Enable
|
|
43
|
+
bit 5 Bit Map Mode. 1 = Enable
|
|
44
|
+
bit 4 Blank Screen to Border Color: O = Blank
|
|
45
|
+
bit 3 Select 24/25 Row Text Display: 1 = 25 Rows
|
|
46
|
+
bit 2-0 Smooth Scroll to Y Dot-Position (0-7)
|
|
47
|
+
$D000-$D02E [C64 memory map (labelled)] <zim>
|
|
48
|
+
6566 Video Interface Chip, VIC II.
|
|
49
|
+
…
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Reach for this whenever a bare address needs a meaning — a register you are about
|
|
53
|
+
to write, a `JSR` target, or a symbol name to give a variable.
|
|
54
|
+
|
|
55
|
+
## Annotate a listing
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node $D annotate --file game.asm --out game.documented.asm
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The input is any text carrying 6502 mnemonics: hand-written source, or a listing
|
|
62
|
+
from whichever disassembler produced it. Lines come back byte-for-byte —
|
|
63
|
+
indentation, labels, directives, blank lines and existing comments intact — with
|
|
64
|
+
a `; $addr (SYMBOL) = description` comment appended:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
* = $C000
|
|
68
|
+
start lda #$00
|
|
69
|
+
sta $d021 ; $D021 = Background color (only bits #0-#3)
|
|
70
|
+
loop: ldx $dc01 ; $DC01 = Port B, keyboard matrix rows and joystick #1
|
|
71
|
+
inc $d020 ; $D020 = Border color (only bits #0-#3)
|
|
72
|
+
jsr $ffd2 ; $FFD2 = Output Vector, chrout
|
|
73
|
+
lda ($fb),y ; $00FB-$00FE (FREKZP, pointer) = Unused (4 bytes)
|
|
74
|
+
sta $0400,x ; $0400-$07E7 (VICSCN, indexed) = Default area of screen memory (1000 bytes)
|
|
75
|
+
bne loop
|
|
76
|
+
jmp ($0314) ; $0314-$0315 (CINV, pointer) = Execution address of interrupt service routine
|
|
77
|
+
rts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A header block listing every referenced address with its full description, symbol
|
|
81
|
+
and region is prepended (elided above): measured on the two examples on this page,
|
|
82
|
+
the eleven-line listing above grows a 23-line header and the nine-line IRQ excerpt
|
|
83
|
+
below grows a 25-line one — a little over 2x the input either way. Pass
|
|
84
|
+
`--no-header` to drop it and get the annotated body alone.
|
|
85
|
+
|
|
86
|
+
Options:
|
|
87
|
+
|
|
88
|
+
- `--out FILE` writes the result to a file instead of stdout.
|
|
89
|
+
- `--max-span N` keeps comments to hits narrower than N bytes, for terser output.
|
|
90
|
+
Default is 4096 bytes: at that width a hit as wide as screen RAM
|
|
91
|
+
(`$0400-$07E7`, 1000 bytes) or the `$C000-$CFFF` block still earns an inline
|
|
92
|
+
comment, while the 8 KB BASIC and KERNAL ROM blocks do not — a branch
|
|
93
|
+
annotated "KERNAL ROM (8192 bytes)" teaches nobody anything. The cap applies
|
|
94
|
+
only to non-flow instructions: flow instructions (`JMP`, `JSR`, branches,
|
|
95
|
+
`RTS`, `RTI`) are held to 2 bytes regardless of `--max-span`, so a jump or
|
|
96
|
+
branch only earns a comment when it targets a specific vector such as
|
|
97
|
+
`$0314`. `--max-span 2` gives register- and variable-level comments only.
|
|
98
|
+
- `--no-header` suppresses the prepended header block, for piping annotated
|
|
99
|
+
output straight into a file across a large listing set.
|
|
100
|
+
- `--file -` reads stdin, identical to a bare `annotate`.
|
|
101
|
+
|
|
102
|
+
Piping works the same way, to annotate straight from another command:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
node $D annotate --max-span 2 < irq.txt
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
$EA31: 20 EA FF JSR $FFEA ; $FFEA = Increment Real-Time Clock
|
|
110
|
+
$EA34: A5 CC LDA $CC ; $00CC (BLNSW) = Cursor visibility switch
|
|
111
|
+
$EA36: D0 29 BNE $EA61
|
|
112
|
+
$EA38: C6 CD DEC $CD ; $00CD (BLNCT) = Delay counter for changing cursor phase
|
|
113
|
+
$EA3C: A9 14 LDA #$14
|
|
114
|
+
$EA40: A4 D3 LDY $D3 ; $00D3 (PNTR) = Current cursor column
|
|
115
|
+
$EA44: AE 87 02 LDX $0287 ; $0287 (GDCOL) = Color of character under cursor
|
|
116
|
+
$EA47: B1 D1 LDA ($D1),Y ; $00D1-$00D2 (PNT, pointer) = Pointer to current line in screen memory
|
|
117
|
+
$EA4F: 20 24 EA JSR $EA24 ; $EA24 = Syncronise Color Pointer
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Reading the annotations
|
|
121
|
+
|
|
122
|
+
Weigh a comment by the region it describes:
|
|
123
|
+
|
|
124
|
+
- **`$D000-$DFFF` is authoritative.** Hardware decides what these mean, so a VIC,
|
|
125
|
+
SID or CIA comment holds for any program. `LDA $DC01 / AND #$10 / BNE` annotates
|
|
126
|
+
as "Port B, keyboard matrix rows and joystick #1", bit 4 is the fire button, and
|
|
127
|
+
that reading is sound because it rests on hardware. KERNAL entry points are
|
|
128
|
+
equally solid wherever ROM is banked in — `$01` bits #0-#2 select it, and the
|
|
129
|
+
vector at `$0314` shows whether the KERNAL IRQ path is in use.
|
|
130
|
+
- **Zero page, `$0200-$07FF` and the BASIC area describe BASIC and KERNAL usage.**
|
|
131
|
+
Read them as a strong hint and confirm against the program's own behaviour
|
|
132
|
+
before adopting the name. A game that banks ROM out keeps its own variables
|
|
133
|
+
there. A real case: at `$08E6` a game's `LDA $49` gets labelled `FORPNT`
|
|
134
|
+
("value of current variable during LET"), when `$49` is really one of that
|
|
135
|
+
game's own variables. Take the address, verify the meaning.
|
|
136
|
+
- **A region-only answer is not an error.** An address that no source names
|
|
137
|
+
specifically prints only the wider regions enclosing it — no error, no
|
|
138
|
+
not-found line. `node $D lookup '$1234'` prints:
|
|
139
|
+
```
|
|
140
|
+
=== $1234 ===
|
|
141
|
+
$0801-$9FFF [$0800-$9FFF, 2048-40959 BASIC area] <sta>
|
|
142
|
+
Default BASIC area (38911 bytes).
|
|
143
|
+
$0800-$9FFF [C64 memory map (labelled)] <zim>
|
|
144
|
+
Normal BASIC Program space.
|
|
145
|
+
```
|
|
146
|
+
This is the dominant case for any game's own code: a region-only answer
|
|
147
|
+
means "the four tables do not name this exact address", not "this address is
|
|
148
|
+
unmapped". It differs from the genuine zero-hit case, where `lookup` prints
|
|
149
|
+
`(not in memory map)` because nothing at all covers the address
|
|
150
|
+
(driver.mjs:485-487). Checked directly against the committed table (a scan of
|
|
151
|
+
`memmap.json`, not a `memmap` rebuild): every address `$0000`-`$FFFF` is
|
|
152
|
+
covered by at least one entry as of this build, so `(not in memory map)` is
|
|
153
|
+
not reachable for any valid address today — the branch exists for a future
|
|
154
|
+
table that loses coverage, not for anything the current one omits.
|
|
155
|
+
|
|
156
|
+
For a game, the fastest route to a real name is to annotate with `--max-span 2`,
|
|
157
|
+
trust the I/O lines immediately, and confirm the rest by watching what the code
|
|
158
|
+
does with them.
|
|
159
|
+
|
|
160
|
+
## Where the data comes from
|
|
161
|
+
|
|
162
|
+
`node $D memmap` rebuilds the table from four published sources, each covering
|
|
163
|
+
what the others leave out:
|
|
164
|
+
|
|
165
|
+
| Source | Contribution |
|
|
166
|
+
|---|---|
|
|
167
|
+
| [sta.c64.org/cbm64mem.html](https://sta.c64.org/cbm64mem.html) | richest prose for zero page, work areas, screen RAM, I/O |
|
|
168
|
+
| [C64.MemoryMap.txt](https://www.zimmers.net/anonftp/pub/cbm/maps/C64.MemoryMap.txt) | canonical assembler symbols — `PNT`, `CINV`, `VICSCN`, `TXTTAB` |
|
|
169
|
+
| [krnromma.htm](http://unusedino.de/ec64/technical/aay/c64/krnromma.htm) | every KERNAL ROM routine by name, so `JSR $EA24` reads as "Syncronise Color Pointer" |
|
|
170
|
+
| [C64io.txt](https://www.zimmers.net/anonftp/pub/cbm/maps/C64io.txt) | VIC/SID/CIA registers broken down per bit |
|
|
171
|
+
|
|
172
|
+
The built table is committed alongside the script, so `lookup` and `annotate` need
|
|
173
|
+
only Node. Rebuild when a source publishes a correction; `lookup`'s `<src>` tags
|
|
174
|
+
show which table any given claim came from.
|
|
175
|
+
|
|
176
|
+
`memmap` overwrites the committed, git-tracked `memmap.json` (driver.mjs:270 —
|
|
177
|
+
235,925 bytes as of 2026-08-04, confirm with `wc -c`) **in place, with no backup
|
|
178
|
+
and no diff.** One of the four sources (`http://unusedino.de/…`, driver.mjs:33)
|
|
179
|
+
is fetched over plain HTTP with no TLS, and the only guard against a bad rebuild
|
|
180
|
+
is a per-source emptiness check plus a 600-entry floor across all sources
|
|
181
|
+
combined (driver.mjs:262, 268) — so a partially-reachable or partially-changed
|
|
182
|
+
source set can silently replace good tracked data with less of it. Rebuild, then
|
|
183
|
+
run `git diff --stat` on `memmap.json` before accepting the result, and
|
|
184
|
+
`git checkout` the file if the diff is not explainable as the correction you
|
|
185
|
+
were expecting. Because it mutates the repo, `memmap` belongs behind a GSD
|
|
186
|
+
command (`/gsd-quick`), per this project's GSD Workflow Enforcement rule — it is
|
|
187
|
+
not a read-only lookup like `lookup` and `annotate`.
|
|
188
|
+
|
|
189
|
+
## Troubleshooting
|
|
190
|
+
|
|
191
|
+
| Symptom | Fix |
|
|
192
|
+
|---|---|
|
|
193
|
+
| Comments land on regions too wide to be useful | `--max-span 2`; the default is 4096 bytes. |
|
|
194
|
+
| A `JSR` or branch target got no comment at all | Flow instructions are capped at 2 bytes regardless of `--max-span`; `lookup` the target directly for the ROM routine name. |
|
|
195
|
+
| The output is mostly header | `--no-header`. |
|
|
196
|
+
| `lookup` printed only wide region lines and no specific name | Nothing in the four tables names that address; expected for the game's own code — take the region and name the address from what the code does with it. |
|
|
197
|
+
| `lookup` printed `(not in memory map)` | Nothing covers it; check the address parsed as intended, since a bare `1234` reads as decimal. |
|
|
198
|
+
| `no memmap.json; run: node driver.mjs memmap` | Restore the committed table with `git checkout` rather than rebuilding; the rebuild needs network and overwrites tracked data. |
|
|
199
|
+
| `memmap` rewrote `memmap.json` and the diff is large or negative | A source was unreachable or changed; `git checkout` the file. |
|