@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// The loader of a generated source (D48): a PRG that copies each segment and
|
|
2
|
+
// the entry state's pages $0002-$03ff into place, sets the display registers
|
|
3
|
+
// of the VIC-II and the VIC bank, waits for the entry's raster line (D60),
|
|
4
|
+
// sets the CIA timers and interrupt masks (D63), sets $00/$01, the stack
|
|
5
|
+
// pointer, A, X, Y and the flags, and jumps to the entry. The copies run
|
|
6
|
+
// from a free page with SEI and all RAM visible, in an order where no copy
|
|
7
|
+
// overwrites the packed bytes of a copy that has not run yet. The plan is
|
|
8
|
+
// simulated here before the emulator runs it.
|
|
9
|
+
|
|
10
|
+
import { formatC64Address } from "#src/c64.ts";
|
|
11
|
+
import { WireFailure } from "#src/host-client/tools.ts";
|
|
12
|
+
import type { Range } from "./source.ts";
|
|
13
|
+
|
|
14
|
+
export interface EntryState {
|
|
15
|
+
pc: number;
|
|
16
|
+
a: number;
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
sp: number;
|
|
20
|
+
/** The status register as PLP reads it. */
|
|
21
|
+
flags: number;
|
|
22
|
+
port: [number, number];
|
|
23
|
+
/** $0000-$03ff at the entry. */
|
|
24
|
+
pages: Uint8Array;
|
|
25
|
+
/** The VIC-II at the entry (D60): $d000-$d02e, and the VIC bank (bits 0-1 of $dd00). */
|
|
26
|
+
vic?: { registers: Uint8Array; bank: number };
|
|
27
|
+
/** CIA 1 and CIA 2 at the entry (D63). */
|
|
28
|
+
cia?: [EntryCia, EntryCia];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A CIA at the entry: $xx00-$xx0f as a read without side effects gives them, and what no read gives. */
|
|
32
|
+
export interface EntryCia {
|
|
33
|
+
registers: Uint8Array;
|
|
34
|
+
latchA: number;
|
|
35
|
+
latchB: number;
|
|
36
|
+
interruptMask: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The VIC-II registers that the loader sets (D60), each with the bits that a
|
|
41
|
+
* read gives back: not the raster, the light pen, the interrupt registers or
|
|
42
|
+
* the collisions. The raster line comes from a wait instead.
|
|
43
|
+
*/
|
|
44
|
+
export const VIC_DISPLAY_REGISTERS: ReadonlyArray<{ register: number; mask: number }> = [
|
|
45
|
+
...Array.from({ length: 0x11 }, (_, register) => ({ register, mask: 0xff })),
|
|
46
|
+
{ register: 0x11, mask: 0x7f },
|
|
47
|
+
{ register: 0x15, mask: 0xff },
|
|
48
|
+
{ register: 0x16, mask: 0x3f },
|
|
49
|
+
{ register: 0x17, mask: 0xff },
|
|
50
|
+
{ register: 0x18, mask: 0xfe },
|
|
51
|
+
{ register: 0x1b, mask: 0xff },
|
|
52
|
+
{ register: 0x1c, mask: 0xff },
|
|
53
|
+
{ register: 0x1d, mask: 0xff },
|
|
54
|
+
...Array.from({ length: 0x0f }, (_, index) => ({ register: 0x20 + index, mask: 0x0f })),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** The raster line of the entry, from $d011 bit 7 and $d012 as the capture read them. */
|
|
58
|
+
export function rasterLine(registers: Uint8Array): number {
|
|
59
|
+
return ((registers[0x11]! & 0x80) << 1) | registers[0x12]!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const FLAG_BITS: Record<string, number> = { n: 0x80, v: 0x40, b: 0x10, d: 0x08, i: 0x04, z: 0x02, c: 0x01 };
|
|
63
|
+
|
|
64
|
+
/** Reads entry_state.json of the c64-unpacker skill. */
|
|
65
|
+
export function readEntryState(text: string, path: string): EntryState {
|
|
66
|
+
const fail = (what: string): never => {
|
|
67
|
+
throw new WireFailure("invalid-input", `${path} is not an entry_state.json of the c64-unpacker skill: ${what}.`);
|
|
68
|
+
};
|
|
69
|
+
let value: Record<string, any>;
|
|
70
|
+
try {
|
|
71
|
+
value = JSON.parse(text) as Record<string, any>;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return fail((error as Error).message);
|
|
74
|
+
}
|
|
75
|
+
const registers = value.registers;
|
|
76
|
+
const byte = (item: unknown, what: string) => (Number.isInteger(item) && (item as number) >= 0 && (item as number) <= 0xff ? (item as number) : fail(`${what} is not a byte`));
|
|
77
|
+
if (typeof registers !== "object" || registers === null) return fail("it has no registers");
|
|
78
|
+
const pc = typeof registers.pc === "string" && /^\$[0-9a-f]{4}$/i.test(registers.pc) ? Number.parseInt(registers.pc.slice(1), 16) : fail("registers.pc is not a $ address");
|
|
79
|
+
if (typeof registers.flags !== "object" || registers.flags === null) fail("registers.flags is missing");
|
|
80
|
+
let flags = 0x20;
|
|
81
|
+
for (const [name, bit] of Object.entries(FLAG_BITS)) if (registers.flags[name] === true) flags |= bit;
|
|
82
|
+
const pages = new Uint8Array(0x400);
|
|
83
|
+
for (let page = 0; page < 4; page++) {
|
|
84
|
+
const hex = value.pages?.[formatC64Address(page << 8)];
|
|
85
|
+
if (typeof hex !== "string" || !/^[0-9a-f]{512}$/i.test(hex)) fail(`pages.${formatC64Address(page << 8)} is not 256 bytes of hex`);
|
|
86
|
+
pages.set(Buffer.from(hex as string, "hex"), page << 8);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
pc,
|
|
90
|
+
a: byte(registers.a, "registers.a"),
|
|
91
|
+
x: byte(registers.x, "registers.x"),
|
|
92
|
+
y: byte(registers.y, "registers.y"),
|
|
93
|
+
sp: byte(registers.sp, "registers.sp"),
|
|
94
|
+
flags,
|
|
95
|
+
port: [byte(value.cpuPort?.$00, "cpuPort.$00"), byte(value.cpuPort?.$01, "cpuPort.$01")],
|
|
96
|
+
pages,
|
|
97
|
+
...readVic(value.io, fail),
|
|
98
|
+
...readCia(value, fail),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface Copy {
|
|
103
|
+
/** What the copy writes: a segment or a part of pages 0-3. */
|
|
104
|
+
what: string;
|
|
105
|
+
target: Range;
|
|
106
|
+
source: Range;
|
|
107
|
+
/** True when the copy runs from the last byte down: its target is above an overlapping source. */
|
|
108
|
+
backward: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** What the loader holds, in its order: a segment or a part of pages 0-3, packed at an address of the loader. */
|
|
112
|
+
export interface PackedBlock {
|
|
113
|
+
what: string;
|
|
114
|
+
range: Range;
|
|
115
|
+
packedAt: number;
|
|
116
|
+
bytes: (address: number) => number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface LoaderPlan {
|
|
120
|
+
blocks: PackedBlock[];
|
|
121
|
+
copies: Copy[];
|
|
122
|
+
/** The first of the two free pages where the copier runs. */
|
|
123
|
+
copier: number;
|
|
124
|
+
/** The address of the free stack byte below the stack pointer, which the flags pass through. */
|
|
125
|
+
stackByte: number;
|
|
126
|
+
end: number;
|
|
127
|
+
text: string;
|
|
128
|
+
/** The CIA timers and masks that the loader sets, when the entry state has them (D63). */
|
|
129
|
+
cia?: CiaPlan;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* What the loader cannot set as it was at the entry (D63). An entry state
|
|
134
|
+
* without CIA values (from a capture before D63) gives no timers at all.
|
|
135
|
+
*/
|
|
136
|
+
export function loaderLimits(plan: { cia?: CiaPlan }): string[] {
|
|
137
|
+
if (plan.cia === undefined) {
|
|
138
|
+
return ["The entry state has no CIA values, so the loader does not set the CIA timers. Capture the program again with c64-unpacker to get them."];
|
|
139
|
+
}
|
|
140
|
+
return plan.cia.limits;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The label of the loader's jump to the entry, in ACME's symbol list. */
|
|
144
|
+
export const LOADER_JUMP = "c64_source_loader_jump";
|
|
145
|
+
|
|
146
|
+
const LOAD = 0x0801;
|
|
147
|
+
const STUB = 0x080d;
|
|
148
|
+
/** sei, all RAM, ldx, the 2-page move loop and jmp. */
|
|
149
|
+
const STUB_SIZE = 25;
|
|
150
|
+
/** The copier code and its three variables, without the table. */
|
|
151
|
+
const COPIER_CODE_SIZE = 167;
|
|
152
|
+
/** The VIC-II code of the copier (D60), without its two tables of one byte per register. */
|
|
153
|
+
const VIC_CODE_SIZE = 44;
|
|
154
|
+
const VIC_SIZE = VIC_CODE_SIZE + 2 * VIC_DISPLAY_REGISTERS.length;
|
|
155
|
+
/** The copier and the stub's move of it: two pages. */
|
|
156
|
+
const COPIER_LIMIT = 0x200;
|
|
157
|
+
const COPY_ENTRY = 7;
|
|
158
|
+
/** X indexes the table, so the table fits in 256 bytes, and the copier with the VIC-II code fits in two pages. */
|
|
159
|
+
export const MAX_COPIES = Math.min(Math.floor((256 - 2) / COPY_ENTRY), Math.floor((COPIER_LIMIT - COPIER_CODE_SIZE - VIC_SIZE - 2) / COPY_ENTRY));
|
|
160
|
+
/** A loader that loads into $d000 or higher would write the I/O area. */
|
|
161
|
+
const LOAD_LIMIT = 0xcfff;
|
|
162
|
+
const COPIER_PAGES = [...Array.from({ length: 15 }, (_, index) => 0xc000 + index * 0x100), ...Array.from({ length: 0x9b }, (_, index) => 0x9e00 - index * 0x100)];
|
|
163
|
+
|
|
164
|
+
const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
|
|
165
|
+
const hex4 = formatC64Address;
|
|
166
|
+
const overlaps = (a: Range, b: Range) => a.start <= b.end && b.start <= a.end;
|
|
167
|
+
const show = (range: Range) => `${hex4(range.start)}-${hex4(range.end)}`;
|
|
168
|
+
|
|
169
|
+
/** Parts of `range` that no range in `holes` covers. */
|
|
170
|
+
function subtract(range: Range, holes: Range[]): Range[] {
|
|
171
|
+
let pieces = [range];
|
|
172
|
+
for (const hole of holes) {
|
|
173
|
+
pieces = pieces.flatMap((piece) => {
|
|
174
|
+
if (!overlaps(piece, hole)) return [piece];
|
|
175
|
+
return [...(hole.start > piece.start ? [{ start: piece.start, end: hole.start - 1 }] : []), ...(hole.end < piece.end ? [{ start: hole.end + 1, end: piece.end }] : [])];
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return pieces;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Orders the copies so that no copy writes over the source of a later one; undefined when they form a cycle. */
|
|
182
|
+
export function copyOrder(copies: Array<Omit<Copy, "backward">>): number[] | undefined {
|
|
183
|
+
const order: number[] = [];
|
|
184
|
+
const done = new Set<number>();
|
|
185
|
+
while (order.length < copies.length) {
|
|
186
|
+
const next = copies.findIndex((copy, index) => !done.has(index) && copies.every((other, at) => at === index || done.has(at) || !overlaps(copy.target, other.source)));
|
|
187
|
+
if (next < 0) return undefined;
|
|
188
|
+
order.push(next);
|
|
189
|
+
done.add(next);
|
|
190
|
+
}
|
|
191
|
+
return order;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Runs the copies on a memory model as the copier does, byte by byte; returns the first wrong target byte. */
|
|
195
|
+
export function simulate(memory: Int16Array, copies: Copy[], expected: (address: number) => number): number | undefined {
|
|
196
|
+
for (const copy of copies) {
|
|
197
|
+
const length = copy.target.end - copy.target.start + 1;
|
|
198
|
+
for (let step = 0; step < length; step++) {
|
|
199
|
+
const offset = copy.backward ? length - 1 - step : step;
|
|
200
|
+
memory[copy.target.start + offset] = memory[copy.source.start + offset]!;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const copy of copies) for (let address = copy.target.start; address <= copy.target.end; address++) if (memory[address] !== expected(address)) return address;
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** True when a read of the address with $01 = $37, as RUN leaves it, gives a ROM byte. */
|
|
208
|
+
const underRom = (address: number) => (address >= 0xa000 && address <= 0xbfff) || address >= 0xe000;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Why a loader stopped before its jump, from the RAM after the stop: the first
|
|
212
|
+
* byte of the moved copier that is not the byte of the loader file, else, when
|
|
213
|
+
* the copier ran, the first byte that a copy did not write as planned.
|
|
214
|
+
*/
|
|
215
|
+
export function stopCause(ram: Uint8Array, plan: LoaderPlan, copierSource: number, fileByte: (address: number) => number, copied: boolean, expected: (address: number) => number): string | undefined {
|
|
216
|
+
for (let offset = 0; offset <= plan.end - copierSource; offset++) {
|
|
217
|
+
const got = ram[plan.copier + offset]!;
|
|
218
|
+
const wanted = fileByte(copierSource + offset);
|
|
219
|
+
if (got === wanted) continue;
|
|
220
|
+
const rom = underRom(copierSource + offset) ? `; ${hex4(copierSource + offset)} is under a ROM, so the move read the ROM` : "";
|
|
221
|
+
return `The copier at ${hex4(plan.copier + offset)} holds ${hex2(got)}, not ${hex2(wanted)} from ${hex4(copierSource + offset)} of the loader file${rom}.`;
|
|
222
|
+
}
|
|
223
|
+
if (!copied) return undefined;
|
|
224
|
+
for (const copy of plan.copies) {
|
|
225
|
+
for (let address = copy.target.start; address <= copy.target.end; address++) {
|
|
226
|
+
if (ram[address] !== expected(address)) return `The copy of ${copy.what} wrote ${hex2(ram[address]!)} at ${hex4(address)}, not ${hex2(expected(address))}.`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The VIC-II and its bank from the io part of an entry state, when it has them. */
|
|
233
|
+
function readVic(io: Record<string, any> | undefined, fail: (what: string) => never): { vic: NonNullable<EntryState["vic"]> } | Record<string, never> {
|
|
234
|
+
if (io?.vic === undefined) return {};
|
|
235
|
+
const vic = io.vic.bytes;
|
|
236
|
+
const cia2 = io.cia2?.bytes;
|
|
237
|
+
if (typeof vic !== "string" || !/^[0-9a-f]{94}$/i.test(vic)) return fail("io.vic.bytes is not 47 bytes of hex");
|
|
238
|
+
if (typeof cia2 !== "string" || !/^[0-9a-f]{2}/i.test(cia2)) return fail("io.cia2.bytes is missing");
|
|
239
|
+
return { vic: { registers: Buffer.from(vic, "hex"), bank: Number.parseInt(cia2.slice(0, 2), 16) & 0x03 } };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function readCia(value: Record<string, any>, fail: (what: string) => never): { cia: [EntryCia, EntryCia] } | Record<string, never> {
|
|
243
|
+
if (value.cia === undefined) return {};
|
|
244
|
+
const chip = (name: "cia1" | "cia2"): EntryCia => {
|
|
245
|
+
const bytes = value.io?.[name]?.bytes;
|
|
246
|
+
if (typeof bytes !== "string" || !/^[0-9a-f]{32}$/i.test(bytes)) return fail(`io.${name}.bytes is not 16 bytes of hex`);
|
|
247
|
+
const hidden = value.cia[name];
|
|
248
|
+
const number = (item: unknown, what: string, max: number) => (Number.isInteger(item) && (item as number) >= 0 && (item as number) <= max ? (item as number) : fail(`cia.${name}.${what} is not a number from 0 to ${max}`));
|
|
249
|
+
return { registers: Buffer.from(bytes, "hex"), latchA: number(hidden?.latchA, "latchA", 0xffff), latchB: number(hidden?.latchB, "latchB", 0xffff), interruptMask: number(hidden?.interruptMask, "interruptMask", 0x1f) };
|
|
250
|
+
};
|
|
251
|
+
return { cia: [chip("cia1"), chip("cia2")] };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** One CIA timer that the loader sets, and what the check of the loader expects of it at the jump. */
|
|
255
|
+
export interface CiaTimerPlan {
|
|
256
|
+
cia: 1 | 2;
|
|
257
|
+
timer: "A" | "B";
|
|
258
|
+
/** The control register as a read gives it: without bit 4, the force load. */
|
|
259
|
+
control: number;
|
|
260
|
+
latch: number;
|
|
261
|
+
/** The counter at the jump, 3 cycles before the entry; undefined when the loader cannot set it. */
|
|
262
|
+
counterAtJump?: number;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface CiaPlan {
|
|
266
|
+
timers: CiaTimerPlan[];
|
|
267
|
+
masks: [number, number];
|
|
268
|
+
/** What the loader cannot set as it was at the entry. */
|
|
269
|
+
limits: string[];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The CIA timers as the KERNAL leaves them when RUN starts the loader, on a
|
|
274
|
+
* PAL C64: only CIA 1 timer A runs, for the 60 Hz interrupt. A timer that the
|
|
275
|
+
* entry has in this state is not written. The loader check reads every timer.
|
|
276
|
+
*/
|
|
277
|
+
const KERNAL_TIMERS: ReadonlyArray<{ control: number; latch: number; counter?: number }> = [
|
|
278
|
+
{ control: 0x01, latch: 0x4025 },
|
|
279
|
+
{ control: 0x08, latch: 0xffff, counter: 0xffff },
|
|
280
|
+
{ control: 0x08, latch: 0xffff, counter: 0xffff },
|
|
281
|
+
{ control: 0x08, latch: 0xffff, counter: 0xffff },
|
|
282
|
+
];
|
|
283
|
+
const KERNAL_MASKS: readonly [number, number] = [0x01, 0x00];
|
|
284
|
+
/** Cycles from the end of the CIA part to the entry: $00, $01, the stack pointer, the flags, A, X, Y, PLP and JMP. */
|
|
285
|
+
const TAIL_CYCLES = 33;
|
|
286
|
+
/** Cycles after the write that starts a CIA timer in which it does not count yet, measured in VICE. */
|
|
287
|
+
const CIA_START_DELAY = 1;
|
|
288
|
+
/** Cycles of one raster line of a PAL C64. */
|
|
289
|
+
const LINE_CYCLES = 63;
|
|
290
|
+
const PAL_LINES = 312;
|
|
291
|
+
|
|
292
|
+
interface Op {
|
|
293
|
+
text: string;
|
|
294
|
+
bytes: number;
|
|
295
|
+
cycles: number;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The copier's CIA part (D63), after the raster wait: the timers and
|
|
300
|
+
* interrupt masks of the entry state. A running timer is stopped, loaded
|
|
301
|
+
* with its counter at the entry plus the cycles to the entry, started, and
|
|
302
|
+
* given its latch, so it counts to the entry's value just at the entry.
|
|
303
|
+
* Then the interrupt flags that the copy left are read away.
|
|
304
|
+
*/
|
|
305
|
+
export function ciaSection(state: EntryState): { ops: Op[]; plan: CiaPlan } | undefined {
|
|
306
|
+
if (state.cia === undefined) return undefined;
|
|
307
|
+
const lda = (value: number): Op => ({ text: `lda #${hex2(value)}`, bytes: 2, cycles: 2 });
|
|
308
|
+
const sta = (address: number): Op => ({ text: `sta ${hex4(address)}`, bytes: 3, cycles: 4 });
|
|
309
|
+
const limits: string[] = [];
|
|
310
|
+
const timers: CiaTimerPlan[] = [];
|
|
311
|
+
const disable: Op[] = [];
|
|
312
|
+
const loads: Array<Op | ((cycles: number[]) => Op)> = [];
|
|
313
|
+
const starts: Op[] = [];
|
|
314
|
+
const latches: Op[] = [];
|
|
315
|
+
const acks: Op[] = [];
|
|
316
|
+
const masks: Op[] = [];
|
|
317
|
+
/** For each running timer: its index in starts, and how its load value follows from the cycles after its start. */
|
|
318
|
+
const running: Array<{ start: number; counter: number; latch: number; system: boolean; name: string; plan: CiaTimerPlan }> = [];
|
|
319
|
+
state.cia.forEach((chip, index) => {
|
|
320
|
+
const id = (index + 1) as 1 | 2;
|
|
321
|
+
const base = index === 0 ? 0xdc00 : 0xdd00;
|
|
322
|
+
let touched = false;
|
|
323
|
+
for (const timer of ["A", "B"] as const) {
|
|
324
|
+
const at = timer === "A" ? 0x04 : 0x06;
|
|
325
|
+
const controlAt = base + (timer === "A" ? 0x0e : 0x0f);
|
|
326
|
+
const control = chip.registers[timer === "A" ? 0x0e : 0x0f]! & 0xef;
|
|
327
|
+
const counter = chip.registers[at]! | (chip.registers[at + 1]! << 8);
|
|
328
|
+
const latch = timer === "A" ? chip.latchA : chip.latchB;
|
|
329
|
+
const name = `CIA ${id} timer ${timer}`;
|
|
330
|
+
const plan: CiaTimerPlan = { cia: id, timer, control, latch, counterAtJump: counter };
|
|
331
|
+
timers.push(plan);
|
|
332
|
+
const kernal = KERNAL_TIMERS[index * 2 + (timer === "A" ? 0 : 1)]!;
|
|
333
|
+
const runs = (control & 0x01) !== 0;
|
|
334
|
+
if (!runs && control === kernal.control && latch === kernal.latch && counter === kernal.counter) continue;
|
|
335
|
+
touched = true;
|
|
336
|
+
// Stopped, a write of the high latch byte also loads the counter.
|
|
337
|
+
loads.push(lda(control & 0xee), sta(controlAt));
|
|
338
|
+
if (runs) {
|
|
339
|
+
// Timer A counts system cycles without bit 5, timer B without bits 5 and 6; other inputs do not count in the loader.
|
|
340
|
+
const system = timer === "A" ? (control & 0x20) === 0 : (control & 0x60) === 0;
|
|
341
|
+
const slot = running.length;
|
|
342
|
+
running.push({ start: starts.length, counter, latch, system, name, plan });
|
|
343
|
+
loads.push((values) => lda(values[slot]! & 0xff), sta(base + at), (values) => lda(values[slot]! >> 8), sta(base + at + 1));
|
|
344
|
+
starts.push(lda(control), sta(controlAt));
|
|
345
|
+
latches.push(lda(latch & 0xff), sta(base + at), lda(latch >> 8), sta(base + at + 1));
|
|
346
|
+
if (system) plan.counterAtJump = counter + 3;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
loads.push(lda(counter & 0xff), sta(base + at), lda(counter >> 8), sta(base + at + 1));
|
|
350
|
+
if (latch === counter) continue;
|
|
351
|
+
if (latch >> 8 === counter >> 8) {
|
|
352
|
+
loads.push(lda(latch & 0xff), sta(base + at));
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// The high byte loads the counter of a stopped timer too.
|
|
356
|
+
loads.push(lda(latch & 0xff), sta(base + at), lda(latch >> 8), sta(base + at + 1));
|
|
357
|
+
plan.counterAtJump = latch;
|
|
358
|
+
limits.push(`${name} is stopped with the counter $${counter.toString(16).padStart(4, "0")} and the latch $${latch.toString(16).padStart(4, "0")}: a stopped CIA timer loads its latch, so the counter is the latch.`);
|
|
359
|
+
}
|
|
360
|
+
const mask = chip.interruptMask;
|
|
361
|
+
if (mask !== KERNAL_MASKS[index]) {
|
|
362
|
+
disable.push(lda(0x7f), sta(base + 0x0d));
|
|
363
|
+
if (mask !== 0) masks.push(lda(0x80 | mask), sta(base + 0x0d));
|
|
364
|
+
touched = true;
|
|
365
|
+
}
|
|
366
|
+
if (touched) acks.push({ text: `lda ${hex4(base + 0x0d)}`, bytes: 3, cycles: 4 });
|
|
367
|
+
const flags = chip.registers[0x0d]! & 0x1f;
|
|
368
|
+
if (flags !== 0) limits.push(`CIA ${id} has the interrupt flags $${flags.toString(16).padStart(2, "0")} at the entry: only an event sets a flag, so the loader cannot.`);
|
|
369
|
+
});
|
|
370
|
+
if (loads.length === 0 && disable.length === 0) return { ops: [], plan: { timers, masks: [state.cia[0].interruptMask, state.cia[1].interruptMask], limits } };
|
|
371
|
+
// The cycles from each start write to the entry: what follows it here, and the tail.
|
|
372
|
+
const after = [...starts, ...latches, ...acks, ...masks];
|
|
373
|
+
const values = running.map((item) => {
|
|
374
|
+
const followers = after.slice(item.start + 2).reduce((sum, op) => sum + op.cycles, 0);
|
|
375
|
+
if (!item.system) return item.counter;
|
|
376
|
+
const load = item.counter + followers + TAIL_CYCLES - CIA_START_DELAY;
|
|
377
|
+
if (load <= 0xffff) return load;
|
|
378
|
+
limits.push(`${item.name} counts from $${item.counter.toString(16).padStart(4, "0")} at the entry: the loader cannot start it so near $ffff without an underflow, so the counter differs.`);
|
|
379
|
+
delete item.plan.counterAtJump;
|
|
380
|
+
return item.counter;
|
|
381
|
+
});
|
|
382
|
+
const pre: Op[] = state.vic === undefined ? [lda(0x35), { text: "sta $01", bytes: 2, cycles: 3 }] : [];
|
|
383
|
+
const ops = [...pre, ...disable, ...loads.map((op) => (typeof op === "function" ? op(values) : op)), ...after];
|
|
384
|
+
return { ops, plan: { timers, masks: [state.cia[0].interruptMask, state.cia[1].interruptMask], limits } };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface LoaderInput {
|
|
388
|
+
/** The segments with the files that hold them, in address order. */
|
|
389
|
+
segments: Array<Range & { files: string[] }>;
|
|
390
|
+
byteAt: (address: number) => number;
|
|
391
|
+
state: EntryState;
|
|
392
|
+
entry: number;
|
|
393
|
+
/** The label of the entry in the source: the loader jumps to it, so the jump follows the code when it moves (D66). */
|
|
394
|
+
entryName?: string;
|
|
395
|
+
header: string[];
|
|
396
|
+
/** The source has macros.asm (D73): the loader includes it after the constants. */
|
|
397
|
+
macros?: boolean;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** The label of a block (D66): its first byte and the byte after it in the loader file, and its first address where it runs. */
|
|
401
|
+
const blockLabel = (start: number, part: "from" | "end" | "to") => `loader_${part}_${start.toString(16).padStart(4, "0")}`;
|
|
402
|
+
const plus = (label: string, offset: number) => (offset === 0 ? label : `${label}+${offset}`);
|
|
403
|
+
/** The label of the copier in the loader file, which the stub moves to its two pages. */
|
|
404
|
+
export const COPIER_SOURCE = "loader_copier_src";
|
|
405
|
+
|
|
406
|
+
/** Plans the loader: the packed layout, the copy order and the copier page, and writes loader.asm. Refuses by name when no plan exists. */
|
|
407
|
+
export function planLoader(input: LoaderInput): LoaderPlan {
|
|
408
|
+
const { state } = input;
|
|
409
|
+
// What the copies write: each segment without $00/$01, which the loader sets last, and pages $0002-$03ff where no segment is.
|
|
410
|
+
const blocks: Array<{ what: string; range: Range; files?: string[]; pseudo: number; bytes: (address: number) => number }> = [];
|
|
411
|
+
for (const segment of input.segments) blocks.push({ what: `segment ${show(segment)}`, range: segment, files: segment.files, pseudo: segment.start, bytes: input.byteAt });
|
|
412
|
+
for (const part of subtract({ start: 0x0002, end: 0x03ff }, input.segments)) {
|
|
413
|
+
blocks.push({ what: `entry state ${show(part)}`, range: part, pseudo: part.start, bytes: (address) => state.pages[address]! });
|
|
414
|
+
}
|
|
415
|
+
blocks.sort((a, b) => a.range.start - b.range.start);
|
|
416
|
+
|
|
417
|
+
let at = STUB + STUB_SIZE;
|
|
418
|
+
const plain: Array<Omit<Copy, "backward">> = [];
|
|
419
|
+
const packedAt: number[] = [];
|
|
420
|
+
/** The block start of each copy in `plain`, which names its labels. */
|
|
421
|
+
const blockOf: number[] = [];
|
|
422
|
+
for (const block of blocks) {
|
|
423
|
+
packedAt.push(at);
|
|
424
|
+
const target = { start: Math.max(block.range.start, 0x0002), end: block.range.end };
|
|
425
|
+
if (target.start <= target.end) {
|
|
426
|
+
const source = { start: at + (target.start - block.range.start), end: at + (target.end - block.range.start) };
|
|
427
|
+
plain.push({ what: block.what, target, source });
|
|
428
|
+
blockOf.push(block.range.start);
|
|
429
|
+
}
|
|
430
|
+
at += block.range.end - block.range.start + 1;
|
|
431
|
+
}
|
|
432
|
+
const cia = ciaSection(state);
|
|
433
|
+
const ciaSize = cia === undefined ? 0 : cia.ops.reduce((sum, op) => sum + op.bytes, 0);
|
|
434
|
+
// The CIA part takes room from the copy table in the copier's two pages.
|
|
435
|
+
const maxCopies = Math.min(MAX_COPIES, Math.floor((COPIER_LIMIT - COPIER_CODE_SIZE - (state.vic === undefined ? 0 : VIC_SIZE) - ciaSize - 2) / COPY_ENTRY));
|
|
436
|
+
if (plain.length > maxCopies) throw new WireFailure("invalid-input", `The loader needs ${plain.length} copies; it can do at most ${maxCopies}. Join segments in the unpacker capture (--range) or write the loader by hand.`);
|
|
437
|
+
const copierImage = at;
|
|
438
|
+
const copierSize = COPIER_CODE_SIZE + (state.vic === undefined ? 0 : VIC_SIZE) + ciaSize + COPY_ENTRY * plain.length + 2;
|
|
439
|
+
const end = copierImage + copierSize - 1;
|
|
440
|
+
if (end > LOAD_LIMIT) throw new WireFailure("invalid-input", `The loader would end at ${hex4(end)}, after ${hex4(LOAD_LIMIT)}: loading it would write the I/O area. The image is too large for one loader file.`);
|
|
441
|
+
|
|
442
|
+
const order = copyOrder(plain);
|
|
443
|
+
if (order === undefined) {
|
|
444
|
+
const clashing = plain.filter((copy) => plain.some((other) => other !== copy && overlaps(copy.target, other.source))).map((copy) => `${copy.what} (packed at ${show(copy.source)})`);
|
|
445
|
+
throw new WireFailure("invalid-input", `No copy order exists: each of these copies writes over the packed bytes of another one: ${clashing.join(", ")}. Write the loader by hand.`);
|
|
446
|
+
}
|
|
447
|
+
const loaded = { start: LOAD, end };
|
|
448
|
+
const copier = COPIER_PAGES.find((page) => {
|
|
449
|
+
const pages = { start: page, end: page + 0x1ff };
|
|
450
|
+
return !overlaps(pages, loaded) && plain.every((copy) => !overlaps(pages, copy.target));
|
|
451
|
+
});
|
|
452
|
+
if (copier === undefined) throw new WireFailure("invalid-input", `No two free pages exist for the copier in $0400-$9fff or $c000-$cfff outside the loader (${show(loaded)}) and the targets: ${plain.map((copy) => show(copy.target)).join(", ")}.`);
|
|
453
|
+
const copies: Copy[] = order.map((index) => {
|
|
454
|
+
const copy = plain[index]!;
|
|
455
|
+
return { ...copy, backward: copy.target.start > copy.source.start && overlaps(copy.target, copy.source) };
|
|
456
|
+
});
|
|
457
|
+
const copyBlocks = order.map((index) => blockOf[index]!);
|
|
458
|
+
|
|
459
|
+
const stackByte = 0x0100 + state.sp;
|
|
460
|
+
const lines: string[] = [...input.header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', ...(input.macros ? ['!source "macros.asm"'] : []), "", `* = ${hex4(LOAD)}`];
|
|
461
|
+
lines.push(` !byte $0b,$08,$0a,$00,$9e,${[...String(STUB)].map((digit) => hex2(digit.charCodeAt(0))).join(",")},$00,$00,$00 ; 10 SYS ${STUB}`);
|
|
462
|
+
lines.push(
|
|
463
|
+
"!zone loader_stub {",
|
|
464
|
+
" sei",
|
|
465
|
+
// The copier lies at the end of the loader file, which can be at $a000 or higher, under the BASIC ROM.
|
|
466
|
+
" lda #$34",
|
|
467
|
+
" sta $01",
|
|
468
|
+
" ldx #0",
|
|
469
|
+
`- lda ${COPIER_SOURCE},x`,
|
|
470
|
+
` sta ${hex4(copier)},x`,
|
|
471
|
+
` lda ${COPIER_SOURCE}+$100,x`,
|
|
472
|
+
` sta ${hex4(copier + 0x100)},x`,
|
|
473
|
+
" inx",
|
|
474
|
+
" bne -",
|
|
475
|
+
` jmp ${hex4(copier)}`,
|
|
476
|
+
"}",
|
|
477
|
+
);
|
|
478
|
+
// The labels around each block give the copy table its numbers, so a block can change its length (D66).
|
|
479
|
+
for (const block of blocks) {
|
|
480
|
+
lines.push("", `; ${block.what}`, blockLabel(block.range.start, "from"), `!pseudopc ${hex4(block.pseudo)} {`, blockLabel(block.range.start, "to"));
|
|
481
|
+
if (block.files !== undefined) for (const file of block.files) lines.push(`!source "${file}"`);
|
|
482
|
+
else {
|
|
483
|
+
for (let address = block.range.start; address <= block.range.end; address += 16) {
|
|
484
|
+
const last = Math.min(block.range.end, address + 15);
|
|
485
|
+
lines.push(` !byte ${Array.from({ length: last - address + 1 }, (_, offset) => hex2(block.bytes(address + offset))).join(",")}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
lines.push("}", blockLabel(block.range.start, "end"));
|
|
489
|
+
}
|
|
490
|
+
const [port0, port1] = state.port;
|
|
491
|
+
lines.push(
|
|
492
|
+
"",
|
|
493
|
+
`; The copier runs at ${hex4(copier)}. The flags pass through ${hex4(stackByte)}, the free stack byte below the stack pointer.`,
|
|
494
|
+
COPIER_SOURCE,
|
|
495
|
+
`!pseudopc ${hex4(copier)} {`,
|
|
496
|
+
"!zone loader_copier {",
|
|
497
|
+
" lda #$34",
|
|
498
|
+
" sta $01",
|
|
499
|
+
" ldx #0",
|
|
500
|
+
".next lda .table,x",
|
|
501
|
+
" sta .count",
|
|
502
|
+
" lda .table+1,x",
|
|
503
|
+
" sta .count+1",
|
|
504
|
+
" ora .count",
|
|
505
|
+
" beq .done",
|
|
506
|
+
" lda .table+2,x",
|
|
507
|
+
" sta .read+1",
|
|
508
|
+
" lda .table+3,x",
|
|
509
|
+
" sta .read+2",
|
|
510
|
+
" lda .table+4,x",
|
|
511
|
+
" sta .write+1",
|
|
512
|
+
" lda .table+5,x",
|
|
513
|
+
" sta .write+2",
|
|
514
|
+
" lda .table+6,x",
|
|
515
|
+
" sta .back",
|
|
516
|
+
" stx .index",
|
|
517
|
+
".read lda $ffff",
|
|
518
|
+
".write sta $ffff",
|
|
519
|
+
" lda .back",
|
|
520
|
+
" bne .down",
|
|
521
|
+
" inc .read+1",
|
|
522
|
+
" bne +",
|
|
523
|
+
" inc .read+2",
|
|
524
|
+
"+ inc .write+1",
|
|
525
|
+
" bne +",
|
|
526
|
+
" inc .write+2",
|
|
527
|
+
"+ jmp .counted",
|
|
528
|
+
".down lda .read+1",
|
|
529
|
+
" bne +",
|
|
530
|
+
" dec .read+2",
|
|
531
|
+
"+ dec .read+1",
|
|
532
|
+
" lda .write+1",
|
|
533
|
+
" bne +",
|
|
534
|
+
" dec .write+2",
|
|
535
|
+
"+ dec .write+1",
|
|
536
|
+
".counted",
|
|
537
|
+
" lda .count",
|
|
538
|
+
" bne +",
|
|
539
|
+
" dec .count+1",
|
|
540
|
+
"+ dec .count",
|
|
541
|
+
" lda .count",
|
|
542
|
+
" ora .count+1",
|
|
543
|
+
" bne .read",
|
|
544
|
+
" lda .index",
|
|
545
|
+
" clc",
|
|
546
|
+
" adc #7",
|
|
547
|
+
" tax",
|
|
548
|
+
" jmp .next",
|
|
549
|
+
".done",
|
|
550
|
+
// The CIA part runs after the raster wait: the wait ends its whole lines earlier, so the entry comes in its line.
|
|
551
|
+
...vicLines(state, Math.floor((cia?.ops.reduce((sum, op) => sum + op.cycles, 0) ?? 0) / LINE_CYCLES)),
|
|
552
|
+
...(cia === undefined ? [] : cia.ops.map((op) => ` ${op.text}`)),
|
|
553
|
+
` lda #${hex2(port0)}`,
|
|
554
|
+
" sta $00",
|
|
555
|
+
` lda #${hex2(port1)}`,
|
|
556
|
+
" sta $01",
|
|
557
|
+
` ldx #${hex2((state.sp - 1) & 0xff)}`,
|
|
558
|
+
" txs",
|
|
559
|
+
` lda #${hex2(state.flags)}`,
|
|
560
|
+
` sta ${hex4(stackByte)}`,
|
|
561
|
+
` lda #${hex2(state.a)}`,
|
|
562
|
+
` ldx #${hex2(state.x)}`,
|
|
563
|
+
` ldy #${hex2(state.y)}`,
|
|
564
|
+
" plp",
|
|
565
|
+
// A global label: the check of the loader stops here, before an interrupt can run (F-32).
|
|
566
|
+
`${LOADER_JUMP} jmp ${input.entryName ?? hex4(input.entry)}`,
|
|
567
|
+
".count !word 0",
|
|
568
|
+
".index !byte 0",
|
|
569
|
+
".back !byte 0",
|
|
570
|
+
...(state.vic === undefined
|
|
571
|
+
? []
|
|
572
|
+
: [
|
|
573
|
+
`.vicreg !byte ${VIC_DISPLAY_REGISTERS.map((item) => hex2(item.register)).join(",")}`,
|
|
574
|
+
`.vicval !byte ${VIC_DISPLAY_REGISTERS.map((item) => hex2(state.vic!.registers[item.register]!)).join(",")}`,
|
|
575
|
+
]),
|
|
576
|
+
".table",
|
|
577
|
+
);
|
|
578
|
+
copies.forEach((copy, index) => {
|
|
579
|
+
// A block that starts at $0000 skips $00/$01, which the loader sets last.
|
|
580
|
+
const start = copyBlocks[index]!;
|
|
581
|
+
const skip = copy.target.start - start;
|
|
582
|
+
const [from, end, to] = [blockLabel(start, "from"), blockLabel(start, "end"), blockLabel(start, "to")];
|
|
583
|
+
const length = skip === 0 ? `${end} - ${from}` : `${end} - ${from} - ${skip}`;
|
|
584
|
+
const read = copy.backward ? `${end} - 1` : plus(from, skip);
|
|
585
|
+
const write = copy.backward ? `${to} + ${end} - ${from} - 1` : plus(to, skip);
|
|
586
|
+
lines.push(` !word ${length}, ${read}, ${write} : !byte ${copy.backward ? 1 : 0} ; ${copy.what}`);
|
|
587
|
+
});
|
|
588
|
+
lines.push(" !word 0", "}", "}");
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
blocks: blocks.map((block, index) => ({ what: block.what, range: block.range, packedAt: packedAt[index]!, bytes: block.bytes })),
|
|
592
|
+
copies,
|
|
593
|
+
copier,
|
|
594
|
+
stackByte,
|
|
595
|
+
end,
|
|
596
|
+
text: `${lines.join("\n")}\n`,
|
|
597
|
+
...(cia === undefined ? {} : { cia: cia.plan }),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* The copier's VIC-II part (D60): with the I/O area visible it sets the
|
|
603
|
+
* display registers and the VIC bank of the entry state, then waits for the
|
|
604
|
+
* entry's raster line, so the program starts in the same phase of the frame.
|
|
605
|
+
*/
|
|
606
|
+
function vicLines(state: EntryState, earlier: number): string[] {
|
|
607
|
+
if (state.vic === undefined) return [];
|
|
608
|
+
const line = (rasterLine(state.vic.registers) - earlier + PAL_LINES) % PAL_LINES;
|
|
609
|
+
return [
|
|
610
|
+
" lda #$35",
|
|
611
|
+
" sta $01",
|
|
612
|
+
` ldx #${hex2(VIC_DISPLAY_REGISTERS.length - 1)}`,
|
|
613
|
+
".vic ldy .vicreg,x",
|
|
614
|
+
" lda .vicval,x",
|
|
615
|
+
" sta $d000,y",
|
|
616
|
+
" dex",
|
|
617
|
+
" bpl .vic",
|
|
618
|
+
" lda $dd00",
|
|
619
|
+
" and #$fc",
|
|
620
|
+
` ora #${hex2(state.vic.bank)}`,
|
|
621
|
+
" sta $dd00",
|
|
622
|
+
`.raster lda $d012`,
|
|
623
|
+
` cmp #${hex2(line & 0xff)}`,
|
|
624
|
+
" bne .raster",
|
|
625
|
+
" lda $d011",
|
|
626
|
+
" and #$80",
|
|
627
|
+
` cmp #${hex2((line >> 1) & 0x80)}`,
|
|
628
|
+
" bne .raster",
|
|
629
|
+
];
|
|
630
|
+
}
|