@moonbase-machines/c64 0.0.0-stage → 0.5.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.
@@ -0,0 +1,35 @@
1
+ // A 1541 disk read back as the package writes it (d64.js make and read), and a BASIC program listed as the 64
2
+ // lists it (basic-list.js), from bytes the tests build, nothing of a title's.
3
+ //
4
+ // node --test test/*.test.js
5
+ import { test } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { make, read, petsciiName } from '@moonbase-machines/c64/d64.js';
8
+ import { listing } from '@moonbase-machines/c64/basic-list.js';
9
+
10
+ test('a disk made and read back: its name, a program at its address over several sectors, a sequential file', () => {
11
+ const program = [0x01, 0x08, ...Array.from({ length: 600 }, (_, i) => i & 0xff)];
12
+ const image = make({ name: 'ZOOPAK', id: 'ZP', files: [{ name: 'MENU', data: program, type: 'PRG' }, { name: 'NOTES', data: [0x48, 0x49], type: 'SEQ' }] });
13
+ const disk = read(image);
14
+ assert.equal(disk.name, 'ZOOPAK'); assert.equal(disk.id, 'ZP');
15
+ assert.deepEqual(disk.files.map(f => [f.name, f.type]), [['MENU', 'PRG'], ['NOTES', 'SEQ']]);
16
+ const [menu, notes] = disk.files;
17
+ assert.equal(menu.address, 0x0801); assert.equal(menu.body.length, 600); assert.equal(menu.body[599], 599 & 0xff);
18
+ assert.deepEqual([...menu.raw.subarray(0, 2)], [0x01, 0x08]);
19
+ assert.deepEqual([...notes.body], [0x48, 0x49]);
20
+ assert.throws(() => read(new Uint8Array(1000)), /174,848 bytes/);
21
+ });
22
+
23
+ test('a name as the 1541 keeps it: shifted letters as capitals, the padding gone', () => {
24
+ assert.equal(petsciiName([0xc8, 0x49, 0xa0, 0xa0]), 'HI');
25
+ });
26
+
27
+ test('a BASIC program listed as LIST prints it, in either character set', () => {
28
+ // 10 PRINT"{CLR}HELLO" / 20 GOTO 10, at $0801
29
+ const lines = [[10, [0x99, 0x22, 0x93, 0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x22]], [20, [0x89, 0x20, 0x31, 0x30]]];
30
+ const bytes = [];
31
+ for (const [n, body] of lines) { const next = 0x801 + bytes.length + 4 + body.length + 1; bytes.push(next & 0xff, next >> 8, n & 0xff, n >> 8, ...body, 0); }
32
+ bytes.push(0, 0);
33
+ assert.equal(listing(Uint8Array.from(bytes)), '10 PRINT"{CLR}HELLO"\n20 GOTO 10\n');
34
+ assert.equal(listing(Uint8Array.from(bytes), { lower: true }), '10 print"{CLR}hello"\n20 goto 10\n');
35
+ });
package/vic.js ADDED
@@ -0,0 +1,122 @@
1
+ // The VIC-II, drawn a raster line at a time into a frame of 384 by 272: the 320 by 200 screen
2
+ // with the border around it. Text in the standard, multicolor and extended modes, bitmaps in
3
+ // hi-res and multicolor, the eight sprites with their expansion, priority and multicolor, and
4
+ // the raster interrupt. The VIC-II reads memory in the 16K bank CIA2 selects, and sees the
5
+ // character ROM at $1000 in the first and third banks.
6
+ export const PALETTE = [[0, 0, 0], [255, 255, 255], [136, 0, 0], [170, 255, 238], [204, 68, 204], [0, 204, 85], [0, 0, 170], [238, 238, 119],
7
+ [221, 136, 85], [102, 68, 0], [255, 119, 119], [51, 51, 51], [119, 119, 119], [170, 255, 102], [0, 136, 255], [187, 187, 187]];
8
+
9
+ export const WIDTH = 384, HEIGHT = 272, LEFT = 32, TOP = 35; // where the 320 by 200 screen sits in the frame
10
+ const FIRST_LINE = 16, LAST_LINE = 287; // the raster lines of the frame's 272, out of 312
11
+
12
+ export class VIC {
13
+ constructor(m) {
14
+ this.m = m; this.r = new Uint8Array(0x40); this.r[0x11] = 0x1b; this.r[0x16] = 0xc8; this.r[0x18] = 0x14;
15
+ this.frame = new Uint8Array(WIDTH * HEIGHT); // a color a pixel
16
+ this.rasterIrq = false; this.spriteHits = 0; this.spriteBackground = 0; this.inside = [0, WIDTH - 1];
17
+ this.priority = new Uint8Array(WIDTH); // this line: 1 where the background's foreground is
18
+ }
19
+ read(r) {
20
+ if (r === 0x11) return (this.r[0x11] & 0x7f) | ((this.m.line >> 1) & 0x80);
21
+ if (r === 0x12) return this.m.line & 0xff;
22
+ if (r === 0x19) return this.r[0x19] | 0x70;
23
+ if (r === 0x1e) { const v = this.spriteHits; this.spriteHits = 0; return v; }
24
+ if (r === 0x1f) { const v = this.spriteBackground; this.spriteBackground = 0; return v; }
25
+ if (r >= 0x20 && r < 0x2f) return this.r[r] | 0xf0;
26
+ return this.r[r];
27
+ }
28
+ write(r, v) {
29
+ if (r === 0x19) { this.r[0x19] &= ~(v & 0x0f); return; } // acknowledged by writing the bit
30
+ this.r[r] = v;
31
+ }
32
+ irq() {
33
+ const line = this.m.line, target = this.r[0x12] | (this.r[0x11] & 0x80) << 1;
34
+ if (line === target && !this.rasterIrq) { this.rasterIrq = true; this.r[0x19] |= 1; }
35
+ if (line !== target) this.rasterIrq = false;
36
+ if (this.r[0x19] & this.r[0x1a] & 0x0f) { this.r[0x19] |= 0x80; return true; }
37
+ return false;
38
+ }
39
+ // the VIC-II's view of memory
40
+ bank() { return (~this.m.cia2.pra & 3) << 14; }
41
+ fetch(a) { const b = this.bank(), full = b | (a & 0x3fff); if ((full & 0x7000) === 0x1000 && !(b & 0x4000)) return this.m.chargen[full & 0xfff]; return this.m.ram[full]; }
42
+
43
+ // the raster line `y` of the frame, into the pixels
44
+ line(y) {
45
+ if (y < FIRST_LINE || y > LAST_LINE) return;
46
+ const py = y - FIRST_LINE, out = this.frame, row = py * WIDTH, r = this.r;
47
+ const border = r[0x20] & 15, bg = r[0x21] & 15;
48
+ const den = r[0x11] & 0x10, rsel = r[0x11] & 8, csel = r[0x16] & 8;
49
+ const top = rsel ? 51 : 55, bottom = rsel ? 250 : 246, left = csel ? 24 : 31, right = csel ? 343 : 334;
50
+ const prio = this.priority; prio.fill(0);
51
+ for (let x = 0; x < WIDTH; x++) out[row + x] = border;
52
+ if (!den || y < top || y > bottom) { this.sprites(y, row, true); return; }
53
+ const yscroll = r[0x11] & 7, xscroll = r[0x16] & 7, bitmap = r[0x11] & 0x20, ecm = r[0x11] & 0x40, mcm = r[0x16] & 0x10;
54
+ const screenBase = ((r[0x18] >> 4) & 15) << 10, charBase = ((r[0x18] >> 1) & 7) << 11, bitmapBase = (r[0x18] & 8) << 10;
55
+ const sy = y - 48 - yscroll; // the line within the 200 of the screen: with the usual scroll of 3, the first is raster 51
56
+ if (sy < 0 || sy >= 200) { this.sprites(y, row, false); return; }
57
+ const cy = sy >> 3, ly = sy & 7;
58
+ for (let cx = 0; cx < 40; cx++) {
59
+ const cell = cy * 40 + cx, code = this.fetch(screenBase + cell), color = this.m.color[cell] & 15;
60
+ let bits, colors;
61
+ if (bitmap) {
62
+ bits = this.fetch(bitmapBase + cell * 8 + ly);
63
+ colors = mcm ? [bg, code >> 4, code & 15, color] : [code & 15, code >> 4];
64
+ } else {
65
+ bits = this.fetch(charBase + ((ecm ? code & 0x3f : code) << 3) + ly);
66
+ if (ecm) colors = [r[0x21 + (code >> 6)] & 15, color];
67
+ else if (mcm && color & 8) colors = [bg, r[0x22] & 15, r[0x23] & 15, color & 7];
68
+ else colors = [bg, color];
69
+ }
70
+ const multi = colors.length === 4 && (bitmap ? true : true);
71
+ const x0 = LEFT + xscroll + cx * 8;
72
+ if (multi && (bitmap || (mcm && (color & 8)))) {
73
+ for (let k = 0; k < 4; k++) { const pair = (bits >> (6 - k * 2)) & 3, c = colors[pair]; const x = x0 + k * 2; if (x >= 0 && x + 1 < WIDTH) { out[row + x] = c; out[row + x + 1] = c; if (pair >= 2) { prio[x] = 1; prio[x + 1] = 1; } } }
74
+ } else {
75
+ for (let k = 0; k < 8; k++) { const set = (bits >> (7 - k)) & 1, x = x0 + k; if (x >= 0 && x < WIDTH) { out[row + x] = set ? colors[1] : colors[0]; if (set) prio[x] = 1; } }
76
+ }
77
+ }
78
+ // the side borders over the screen's edge, and the whole border row outside it; the border over
79
+ // the sprites too, as the 64's is (they show in it only where a program opens it)
80
+ for (let x = 0; x < LEFT + left - 24; x++) out[row + x] = border;
81
+ for (let x = LEFT + right - 24 + 1; x < WIDTH; x++) out[row + x] = border;
82
+ this.inside = [LEFT + left - 24, LEFT + right - 24];
83
+ this.sprites(y, row, false);
84
+ }
85
+
86
+ // the sprites on raster line `y`: each is 24 by 21 at its position, doubled when expanded,
87
+ // its pixels from the 63 bytes its pointer names, over or under the background by priority. A
88
+ // sprite shows from the line after its Y: the VIC-II reads its first line's data on line Y and
89
+ // shows it on the next (as VICE shows it, held against it on Subway Arts' curtains)
90
+ sprites(y, row, borderOnly) {
91
+ const r = this.r, enabled = r[0x15];
92
+ if (!enabled) return;
93
+ const screenBase = ((r[0x18] >> 4) & 15) << 10, out = this.frame, prio = this.priority;
94
+ const drawn = new Uint8Array(WIDTH); // which sprite drew each pixel, plus one
95
+ for (let s = 7; s >= 0; s--) {
96
+ if (!(enabled & (1 << s))) continue;
97
+ const sx = r[s * 2] | ((r[0x10] >> s) & 1) << 8, sy = r[s * 2 + 1], yx = r[0x17] & (1 << s), xx = r[0x1d] & (1 << s);
98
+ const height = yx ? 42 : 21;
99
+ const ry = y - sy - 1; // the sprite is placed by raster line, from the one after its Y
100
+ if (ry < 0 || ry >= height) continue;
101
+ const line = yx ? ry >> 1 : ry, pointer = this.fetch(screenBase + 0x3f8 + s), data = pointer << 6;
102
+ const multi = r[0x1c] & (1 << s), behind = r[0x1b] & (1 << s), color = r[0x27 + s] & 15;
103
+ const px0 = LEFT + sx - 24;
104
+ for (let byte = 0; byte < 3; byte++) {
105
+ const bits = this.fetch(data + line * 3 + byte);
106
+ if (multi) {
107
+ for (let k = 0; k < 4; k++) { const pair = (bits >> (6 - k * 2)) & 3; if (!pair) continue; const c = pair === 1 ? r[0x25] & 15 : pair === 2 ? color : r[0x26] & 15; const base = (byte * 8 + k * 2) * (xx ? 2 : 1); for (let d = 0; d < (xx ? 4 : 2); d++) this.spritePixel(row, px0 + base + d, c, s, behind, drawn, borderOnly); }
108
+ } else {
109
+ for (let k = 0; k < 8; k++) { if (!((bits >> (7 - k)) & 1)) continue; const base = (byte * 8 + k) * (xx ? 2 : 1); for (let d = 0; d < (xx ? 2 : 1); d++) this.spritePixel(row, px0 + base + d, color, s, behind, drawn, borderOnly); }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ spritePixel(row, x, c, s, behind, drawn, borderOnly) {
115
+ if (x < 0 || x >= WIDTH) return;
116
+ if (drawn[x]) { this.spriteHits |= (1 << s) | (1 << (drawn[x] - 1)); }
117
+ drawn[x] = s + 1;
118
+ if (this.priority[x]) { this.spriteBackground |= 1 << s; if (behind) return; }
119
+ if (borderOnly || x < this.inside[0] || x > this.inside[1]) return;
120
+ this.frame[row + x] = c;
121
+ }
122
+ }