@8bitscript/c64 0.1.0

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/src/index.8bs ADDED
@@ -0,0 +1,427 @@
1
+ // @8bitscript/c64 — C64 target support: the hardware underneath.
2
+ //
3
+ // The module entry point named by the "8bitscript".entry field in this
4
+ // package's package.json, resolved and linked for:
5
+ //
6
+ // import { borderColor, backgroundColor, memoryPointer } from "@8bitscript/c64";
7
+ // import { spriteEnable, spritePositions, cia1PortA } from "@8bitscript/c64";
8
+ // import { reuStatus, reuCommand } from "@8bitscript/c64"; // --profile reu*
9
+ //
10
+ // The surface is the hardware, one register at a time — the VIC-II, the
11
+ // SID, the two CIAs, the 6510's own port — under the names llvm-mos-sdk's
12
+ // c64.h gives the chips (`VIC`, `SID`, `CIA1`, `CIA2`) and the fields its
13
+ // _vic2.h/_sid.h/_6526.h give the registers. What a program usually wants
14
+ // sits one layer up, in this package's own implementations of the portable
15
+ // capability packages — `./src/screen.8bs` behind @8bitscript/screen and
16
+ // `./src/text.8bs` behind @8bitscript/text, named by the "8bitscript".exports
17
+ // map in package.json — and in the C64-only subpaths beside them:
18
+ // `@8bitscript/c64/sprites`, `/raster`, `/bitmap`, `/charset`, `/scroll`,
19
+ // `/keyboard` + `/keys`, `/joystick`, `/sid`, `/reu`, `/mouse`. All of
20
+ // them import these registers and are built on nothing else. A
21
+ // project that imports THIS package has declared itself C64 specific.
22
+ //
23
+ // Every fact stated here was read in a primary source or seen on screen
24
+ // under x64sc; packages/c64/AGENTS.md says which, and holds what was not
25
+ // checked. Where the VIC-20's VIC packs border, background, and video mode
26
+ // into one register, the C64's VIC-II gives each its own; both take the low
27
+ // four bits, and the VIC-II has sixteen colours everywhere.
28
+ import { Video } from "./geometry.8bs";
29
+
30
+ // ---- the 6510's port: what the CPU sees at $A000, $D000, $E000 -------------
31
+ //
32
+ // $0001, the processor port. Bits 0-2 are LORAM, HIRAM and CHAREN: which of
33
+ // BASIC ROM, KERNAL ROM and the I/O area / character ROM the CPU sees over
34
+ // the RAM that is always underneath. The SDK's start-up (unmap-basic.o's
35
+ // .init section: `ldx #$2f / stx $00 / ldx #$3e / stx $01`) leaves it at
36
+ // $3E — BASIC out, KERNAL in, I/O in — so $A000-$BFFF is program RAM. Bits
37
+ // 3-5 are the cassette lines. Change it by reading, masking and writing
38
+ // back, never with a literal: the three functions at the end of this file
39
+ // are the only places this package does.
40
+ //
41
+ // The low three bits, and what the CPU sees (c64-wiki, "Bank Switching"):
42
+ // %111 $37 BASIC, KERNAL, I/O the machine's boot state
43
+ // %110 BASIC, KERNAL, character ROM
44
+ // %011 $3B RAM at $A000, KERNAL, I/O the SDK's state (as $3E)
45
+ // %010 RAM at $A000, KERNAL, character ROM the copy below
46
+ // %101 $35 RAM at $A000 and $E000, I/O this package's state (as $3D)
47
+ // %100, %00x RAM everywhere, no I/O writes under I/O (as $3C)
48
+ // HIRAM (bit 1) low hides the character ROM as well as the KERNAL: from
49
+ // %101, clearing CHAREN gives RAM everywhere, not the ROM, so a copy of
50
+ // the ROM sets %010 first and %101 after.
51
+ @address(0x0001)
52
+ export let processorPort: volatile<u8>;
53
+
54
+ // ---- VIC-II ($D000-$D02E) ------------------------------------------------
55
+
56
+ // $D000-$D00F: sprite 0's X, sprite 0's Y, sprite 1's X, ... — `2 * n` is
57
+ // sprite n's X and `2 * n + 1` its Y (`spr_pos[n].x/.y` in _vic2.h). X is
58
+ // nine bits wide: the ninth of each sprite is a bit of $D010.
59
+ @address(0xD000)
60
+ export let spritePositions: array<u8, 16>;
61
+
62
+ // $D010, bit n = bit 8 of sprite n's X coordinate (`spr_hi_x`).
63
+ @address(0xD010)
64
+ export let spriteXHigh: volatile<u8>;
65
+
66
+ // $D011, control register 1 (`ctrl1`): bit 7 RST8 (bit 8 of the raster
67
+ // line, read; of the raster compare, write), 6 ECM, 5 BMM, 4 DEN (display
68
+ // enable), 3 RSEL (25 rows / 24), 0-2 YSCROLL. The frame runtime reads
69
+ // bit 7 (FRAME_SYNC.c64 in packages/backend-6502).
70
+ @address(0xD011)
71
+ export let control1: volatile<u8>;
72
+
73
+ // $D012, the raster line's low eight bits (read) / raster compare (write).
74
+ @address(0xD012)
75
+ export let raster: volatile<u8>;
76
+
77
+ // $D015, one enable bit per sprite (`spr_ena`).
78
+ @address(0xD015)
79
+ export let spriteEnable: volatile<u8>;
80
+
81
+ // $D016, control register 2 (`ctrl2`): bit 4 MCM (multicolour text/bitmap),
82
+ // 3 CSEL (40 columns / 38), 0-2 XSCROLL.
83
+ @address(0xD016)
84
+ export let control2: volatile<u8>;
85
+
86
+ // $D017 / $D01D, one bit per sprite: drawn twice as tall / twice as wide.
87
+ @address(0xD017)
88
+ export let spriteExpandY: volatile<u8>;
89
+ @address(0xD01D)
90
+ export let spriteExpandX: volatile<u8>;
91
+
92
+ // $D018, the VIC-II's memory pointer (`addr` in _vic2.h): bits 4-7 the
93
+ // screen matrix's position in the bank in 1K steps, bits 1-3 the character
94
+ // set's in 2K steps (bit 3 alone for a bitmap). $15 is the KERNAL's boot
95
+ // value (screen $0400, ROM upper-case set at $1000, POKE 53272,21 in the
96
+ // Programmer's Reference Guide); this package's screen is at $E000 with
97
+ // its own copy of the ROM at $D000, which in bank 3 is $84 — see
98
+ // Video.MEMORY_POINTER_UPPERCASE in ./geometry.8bs.
99
+ @address(0xD018)
100
+ export let memoryPointer: volatile<u8>;
101
+
102
+ // $D019 / $D01A, interrupt status and mask (`irr`/`imr`): bit 0 raster,
103
+ // 1 sprite-background collision, 2 sprite-sprite collision, 3 light pen;
104
+ // status bit 7 = any. Writing a 1 to a status bit acknowledges it. The
105
+ // program runs with interrupts off (see FRAME_SYNC.c64) and polls; the
106
+ // one interrupt this package ever enables is the raster compare, for
107
+ // @8bitscript/c64/raster's write list, whose handler acknowledges it.
108
+ @address(0xD019)
109
+ export let interruptStatus: volatile<u8>;
110
+ @address(0xD01A)
111
+ export let interruptMask: volatile<u8>;
112
+
113
+ // $D01B, one bit per sprite: 1 = the sprite is drawn BEHIND the
114
+ // background's foreground pixels (`spr_bg_prio`). Sprite-to-sprite order
115
+ // is fixed: a lower-numbered sprite is always in front.
116
+ @address(0xD01B)
117
+ export let spritePriority: volatile<u8>;
118
+
119
+ // $D01C, one bit per sprite: multicolour (12x21 double-width pixels, three
120
+ // colours plus transparent: the sprite's own and the two shared ones).
121
+ @address(0xD01C)
122
+ export let spriteMulticolor: volatile<u8>;
123
+
124
+ // $D01E / $D01F: which sprites touched another sprite / the background's
125
+ // foreground since last read. Both CLEAR WHEN READ (and cannot be
126
+ // written), so read each once a frame and keep the value.
127
+ @address(0xD01E)
128
+ export let spriteCollision: volatile<u8>;
129
+ @address(0xD01F)
130
+ export let spriteBackgroundCollision: volatile<u8>;
131
+
132
+ // $D020, the border colour; $D021-$D024, background colours 0-3 (only 0
133
+ // shows in the standard text mode; 1-3 are for extended-colour and
134
+ // multicolour modes); $D025 / $D026, the two colours every multicolour
135
+ // sprite shares; $D027-$D02E, each sprite's own colour. Low four bits.
136
+ @address(0xD020)
137
+ export let borderColor: volatile<u8>;
138
+ @address(0xD021)
139
+ export let backgroundColor: volatile<u8>;
140
+ @address(0xD022)
141
+ export let backgroundColor1: volatile<u8>;
142
+ @address(0xD023)
143
+ export let backgroundColor2: volatile<u8>;
144
+ @address(0xD024)
145
+ export let backgroundColor3: volatile<u8>;
146
+ @address(0xD025)
147
+ export let spriteSharedColor0: volatile<u8>;
148
+ @address(0xD026)
149
+ export let spriteSharedColor1: volatile<u8>;
150
+ @address(0xD027)
151
+ export let spriteColors: array<u8, 8>;
152
+
153
+ // ---- SID ($D400-$D41C) ------------------------------------------------------
154
+ //
155
+ // Twenty-five registers, seven per voice then the filter and volume. The
156
+ // write-only ones read back as nothing useful — the SID has no readable
157
+ // state below $D419 — so @8bitscript/c64/sid keeps its own copy of what it
158
+ // wrote. Voice n's registers start at 7 * n: 0-1 frequency (low, high),
159
+ // 2-3 pulse width (low, high nybble), 4 control (bit 0 gate, 1 sync, 2
160
+ // ring, 3 test, 4 triangle, 5 sawtooth, 6 pulse, 7 noise), 5 attack/decay
161
+ // nybbles, 6 sustain/release nybbles. 21-22 filter cutoff (low three bits,
162
+ // high eight), 23 resonance nybble / which voices are filtered, 24 = $D418:
163
+ // filter mode bits 4-6, voice 3 off bit 7, master volume in the low nybble.
164
+ @address(0xD400)
165
+ export let sidRegisters: array<u8, 25>;
166
+
167
+ // $D419 / $D41A, the two paddle A/D converters (`ad1`/`ad2`); which port's
168
+ // paddles is CIA1 port A bits 6-7's choice. $D41B / $D41C, voice 3's
169
+ // oscillator and envelope, readable — the oscillator with a noise waveform
170
+ // is the hardware entropy the root AGENTS.md wants kept out of the
171
+ // deterministic PRNG.
172
+ @address(0xD419)
173
+ export let paddleX: volatile<u8>;
174
+ @address(0xD41A)
175
+ export let paddleY: volatile<u8>;
176
+ @address(0xD41B)
177
+ export let voice3Oscillator: volatile<u8>;
178
+ @address(0xD41C)
179
+ export let voice3Envelope: volatile<u8>;
180
+
181
+ // ---- CIA1 ($DC00): keyboard, joysticks, the KERNAL's IRQ timer ---------------
182
+ //
183
+ // Port A ($DC00) drives the keyboard's eight columns (a 0 bit selects a
184
+ // column) and reads joystick port 2 on bits 0-4; port B ($DC01) reads the
185
+ // eight rows of the selected column, a 0 bit where a key is down, and
186
+ // joystick port 1 on the same bits 0-4. The joystick lines sit in parallel
187
+ // with the matrix — port 1's pull the rows, port 2's pull the columns —
188
+ // which is why a joystick "types" and why @8bitscript/c64/keyboard and
189
+ // /joystick read the ports in a fixed order once a frame. The KERNAL sets
190
+ // the directions (A out, B in) at boot; the packages set them again.
191
+ // Timer A ($DC04-$DC05) is the KERNAL's 60 Hz IRQ source, silenced by the
192
+ // `sei` the frame runtime starts with; $DC0D is its interrupt control.
193
+ @address(0xDC00)
194
+ export let cia1PortA: volatile<u8>;
195
+ @address(0xDC01)
196
+ export let cia1PortB: volatile<u8>;
197
+ @address(0xDC02)
198
+ export let cia1DirectionA: volatile<u8>;
199
+ @address(0xDC03)
200
+ export let cia1DirectionB: volatile<u8>;
201
+ @address(0xDC0D)
202
+ export let cia1InterruptControl: volatile<u8>;
203
+
204
+ // ---- CIA2 ($DD00): the VIC bank, the serial bus, the user port ---------------
205
+ //
206
+ // Port A bits 0-1 choose the VIC's 16K bank, INVERTED: %11 = bank 0
207
+ // ($0000, the boot state), %10 = bank 1 ($4000), %01 = bank 2 ($8000),
208
+ // %00 = bank 3 ($C000, this package). Bits 3-5 are the serial bus (ATN,
209
+ // CLOCK and DATA out) and bits 6-7 its inputs, so the bank is changed by
210
+ // masking, never by storing a whole byte. $DD02 is the direction register:
211
+ // bits 0-1 must be outputs (the KERNAL sets $3F). $DD0D is CIA2's
212
+ // interrupt control: its interrupts are the NMI line (the RESTORE key is
213
+ // the other NMI source), and @8bitscript/c64/raster masks them.
214
+ @address(0xDD00)
215
+ export let cia2PortA: volatile<u8>;
216
+ @address(0xDD02)
217
+ export let cia2DirectionA: volatile<u8>;
218
+ @address(0xDD0D)
219
+ export let cia2InterruptControl: volatile<u8>;
220
+
221
+ // ---- the screen's memory --------------------------------------------------
222
+ //
223
+ // `screenRam`, `colorRam`, `spritePointers` and `spriteShapes` — the arrays
224
+ // over the RAM the VIC reads — are declared in ./geometry.8bs beside the
225
+ // addresses they are built from, and imported from there.
226
+
227
+ // ---- the machine's state: which mode, and whether interrupts are on --------
228
+ //
229
+ // What the VIC is showing — `VideoMode.TEXT` (the screen at $E000 over the
230
+ // character set at $D000, the state setupVideo() leaves) or
231
+ // `VideoMode.BITMAP` (@8bitscript/c64/bitmap: the bitmap at $E000, its
232
+ // colour matrix under the I/O area). The sprite layer reads it, because
233
+ // the sprite pointers move with the screen matrix; text's prepare() reads
234
+ // it, because its $D018 write is the text mode's.
235
+ export namespace VideoMode {
236
+ const TEXT: utinyint = 0;
237
+ const BITMAP: utinyint = 1;
238
+ }
239
+ export let videoMode: utinyint = 0;
240
+
241
+ // True once @8bitscript/c64/raster has enabled the raster interrupt. The
242
+ // one thing it changes here: a window with the I/O area banked out
243
+ // (bankIoOut/bankIoIn below) has to be an interrupt-free window, because
244
+ // an interrupt handler that fires inside it acknowledges $D019 into RAM
245
+ // and fires again forever — the hang the copy below documents.
246
+ export let interruptsOn: bool = false;
247
+
248
+ // ---- video set-up: the bank, the character set, the pointer -----------------
249
+ //
250
+ // Once, before the first thing is drawn: copy the character ROM into the
251
+ // RAM under it, point the VIC at bank 3, set $D018, and bank the KERNAL
252
+ // out. Every surface in this package calls it first (screen.blank(),
253
+ // text's prepare(), the sprites' show()), and the guard makes every call
254
+ // after the first cost a load and a branch.
255
+ //
256
+ // The copy (copyCharacterRom): with CHAREN ($01 bit 2) clear and HIRAM
257
+ // set, the CPU READS the character ROM at $D000-$DFFF, and its WRITES
258
+ // there still land in the RAM underneath — so reading each byte and
259
+ // storing it back to the same address copies the ROM into RAM in place.
260
+ // The I/O area is gone for the length of the loop (about four frames at
261
+ // 1 MHz), so nothing here touches a register inside it, and interrupts are
262
+ // off around it: the KERNAL's IRQ handler ends by reading CIA1's interrupt
263
+ // register to acknowledge, which with I/O banked out reads the ROM
264
+ // instead and never acknowledges — the interrupt fires again on return,
265
+ // forever. Seen here: the first version of this copy, run before the
266
+ // frame runtime's own `sei`, hung the machine exactly so. `sei` here as
267
+ // well, so a program that draws before its first waitFrame() — or never
268
+ // calls it — is safe too; from this point on the program owns the machine
269
+ // (packages/c64/AGENTS.md).
270
+ //
271
+ // The KERNAL goes out ($01 low bits %101) at the end, and stays out. Two
272
+ // reasons, both structural. The screen is at $E000, and with the KERNAL
273
+ // ROM mapped the CPU's writes there land in RAM but its READS return ROM:
274
+ // `screenRam[cell]` read back the KERNAL, a coarse scroll copied it, a
275
+ // bitmap plot would read-modify-write it. With the ROM out, $E000-$FFFF
276
+ // reads as the RAM the VIC shows. And the CPU's interrupt vectors at
277
+ // $FFFA-$FFFF are then RAM too, which is how @8bitscript/c64/raster owns
278
+ // the IRQ without the KERNAL's handler in the way: this package's native
279
+ // start-up code (native/6502/raster.s, an .init section, so it runs before
280
+ // main() in every program that links this package) has already pointed
281
+ // both the NMI and the IRQ vector at a bare `rti`, so a RESTORE key press
282
+ // with the ROM out does nothing at all. Nothing in an 8bitscript program
283
+ // calls the KERNAL after this point — main() never returns to it either.
284
+ let videoReady: bool = false;
285
+
286
+ export function setupVideo(): void {
287
+ if (videoReady) {
288
+ return;
289
+ }
290
+ videoReady = true;
291
+ asm6502 {
292
+ sei
293
+ }
294
+ copyCharacterRom();
295
+ cia2DirectionA = cia2DirectionA | 0x03;
296
+ cia2PortA = cia2PortA & 0xFC; // bank 3
297
+ memoryPointer = Video.MEMORY_POINTER_UPPERCASE;
298
+ spriteEnable = 0; // no sprite until a program asks; the pointers are whatever RAM held
299
+ }
300
+
301
+ // The character ROM's 4K into the RAM under it, in place, and the port left
302
+ // at %101 (KERNAL out, I/O in). Safe from any port state, and callable
303
+ // again — @8bitscript/c64/charset's restore() is a second copy over a
304
+ // program's own glyphs. Interrupts are off for the copy and back on after
305
+ // it only if the raster interrupt had them on.
306
+ export function copyCharacterRom(): void {
307
+ asm6502 {
308
+ sei
309
+ }
310
+ processorPort = (processorPort & 0xF8) | 0x02; // %010: KERNAL in, character ROM at $D000, I/O out
311
+ for (let i: usmallint = 0; i < 4096; i++) {
312
+ memory.write(Video.CHARSET + i, memory.read(Video.CHARSET + i));
313
+ }
314
+ processorPort = (processorPort & 0xF8) | 0x05; // %101: KERNAL out, I/O in
315
+ if (interruptsOn) {
316
+ asm6502 {
317
+ cli
318
+ }
319
+ }
320
+ }
321
+
322
+ // ---- the region, at run time ------------------------------------------------
323
+ //
324
+ // PAL or NTSC is not a build fact: one .prg runs on both, and the frame
325
+ // runtime finds out for itself at start-up (FRAME_SYNC.c64's probe,
326
+ // packages/backend-6502) — but keeps the answer to itself. So this is the
327
+ // same probe, for a program that needs it: @8bitscript/c64/sid's note
328
+ // tables (the SID's pitch is the CPU clock's), a program timing something
329
+ // in raster lines. Sync to the top of a frame, then watch one whole frame
330
+ // go by: only a PAL raster (312 lines) ever reaches line 288 — bit 8 set
331
+ // in $D011 with $D012 at 32 or more — where an NTSC frame stops at 262.
332
+ // Two frames, once; keep the answer. A cross-machine builtin that
333
+ // publishes the runtime's own answer is the design this waits for.
334
+ export namespace Region {
335
+ const PAL: utinyint = 0;
336
+ const NTSC: utinyint = 1;
337
+ }
338
+
339
+ function rasterInTopHalf(): bool {
340
+ // $D012 first, then $D011 bit 8 (FRAME_SYNC.c64 says why the order).
341
+ return raster < 128 && (control1 & 0x80) == 0;
342
+ }
343
+
344
+ export function detectRegion(): utinyint {
345
+ let pal: bool = false;
346
+ while (rasterInTopHalf()) {}
347
+ while (!rasterInTopHalf()) {} // the wrap to line 0
348
+ while (rasterInTopHalf()) {}
349
+ while (!rasterInTopHalf()) {
350
+ if ((control1 & 0x80) != 0 && raster >= 32) {
351
+ pal = true;
352
+ }
353
+ }
354
+ if (pal) {
355
+ return Region.PAL;
356
+ }
357
+ return Region.NTSC;
358
+ }
359
+
360
+ // ---- writes under the I/O area --------------------------------------------
361
+ //
362
+ // $D000-$DFFF is RAM the VIC reads (the character set, and in bitmap mode
363
+ // the colour matrix and the sprite pointers) that the CPU can only reach
364
+ // with the I/O area banked out — so every write there is a window: `sei`,
365
+ // CHAREN clear (%100 from %101: RAM everywhere), the writes, I/O back,
366
+ // `cli` if the raster interrupt is on. Nothing between bankIoOut() and
367
+ // bankIoIn() may touch a register, and the window is kept short: the
368
+ // raster interrupt, if enabled, is late by the window's length.
369
+ export function bankIoOut(): void {
370
+ asm6502 {
371
+ sei
372
+ }
373
+ processorPort = processorPort & 0xFB;
374
+ }
375
+
376
+ export function bankIoIn(): void {
377
+ processorPort = processorPort | 0x04;
378
+ if (interruptsOn) {
379
+ asm6502 {
380
+ cli
381
+ }
382
+ }
383
+ }
384
+
385
+ // One byte into the RAM under the I/O area, in its own window.
386
+ export function writeUnderIo(address: usmallint, value: utinyint): void {
387
+ bankIoOut();
388
+ memory.write(address, value);
389
+ bankIoIn();
390
+ }
391
+
392
+ // One byte from the RAM under the I/O area.
393
+ export function readUnderIo(address: usmallint): utinyint {
394
+ bankIoOut();
395
+ let value: utinyint = memory.read(address);
396
+ bankIoIn();
397
+ return value;
398
+ }
399
+
400
+ // ---- REU (RAM Expansion Unit) --------------------------------------------
401
+ //
402
+ // $DF00-$DF0A, the standard Commodore 1764/1750-compatible REU DMA
403
+ // controller register map — confirmed against VICE's own `-reu`/`-reusize`
404
+ // support (x64sc -help) and the register layout every REU-compatible
405
+ // expansion since the 1700 has shared. These addresses exist in the SDK for
406
+ // every c64 program (the same reasoning @8bitscript/pet's screen.8bs
407
+ // documents for `setColors` on a machine without colour): they only do
408
+ // something when the program is actually built and run against a `reu*`
409
+ // hardware profile (--profile in packages/cli/src/build.mjs and run.mjs's
410
+ // `-reu -reusize`) — on the stock C64 these are ordinary, if unusual,
411
+ // memory-mapped I/O addresses, not a real DMA controller. This is raw
412
+ // register access, not a transfer driver: a caller sequences the registers
413
+ // itself (base addresses, length, then a command byte — bit 7 set to
414
+ // execute, bits 0-1 for the transfer direction: 00 stash C64→REU, 01 fetch
415
+ // REU→C64, 10 swap, 11 verify), the same level @8bitscript/vic20's
416
+ // `vicColor` and this package's own `borderColor` above operate at.
417
+ @address(0xDF00) export let reuStatus: volatile<u8>;
418
+ @address(0xDF01) export let reuCommand: volatile<u8>;
419
+ @address(0xDF02) export let reuC64AddressLow: volatile<u8>;
420
+ @address(0xDF03) export let reuC64AddressHigh: volatile<u8>;
421
+ @address(0xDF04) export let reuAddressLow: volatile<u8>;
422
+ @address(0xDF05) export let reuAddressHigh: volatile<u8>;
423
+ @address(0xDF06) export let reuBank: volatile<u8>;
424
+ @address(0xDF07) export let reuLengthLow: volatile<u8>;
425
+ @address(0xDF08) export let reuLengthHigh: volatile<u8>;
426
+ @address(0xDF09) export let reuInterruptMask: volatile<u8>;
427
+ @address(0xDF0A) export let reuAddressControl: volatile<u8>;