@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/bitmap.8bs ADDED
@@ -0,0 +1,175 @@
1
+ // @8bitscript/c64/bitmap — the VIC-II's bitmap modes: 320x200 pixels, or
2
+ // 160x200 in four colours.
3
+ //
4
+ // Named by "8bitscript".exports["./bitmap"] in this package's package.json:
5
+ //
6
+ // import { bitmap } from "@8bitscript/c64/bitmap";
7
+ //
8
+ // bitmap.enter(false); // hi-res: 320x200, two colours a cell
9
+ // bitmap.clear();
10
+ // bitmap.fillColors(1, 0); // white on black, every cell
11
+ // for (let x: usmallint = 0; x < 320; x++) {
12
+ // bitmap.plot(x, x >> 1); // a line
13
+ // }
14
+ // ...
15
+ // bitmap.leave(); // text mode again
16
+ //
17
+ // Hardware-level, C64-only surface, the layer a portable canvas
18
+ // capability (docs/systems.md's bitmap rung) will sit on. The bitmap is
19
+ // 8000 bytes at $E000, laid out the VIC's way: 8 bytes a cell (one a
20
+ // pixel row), 40 cells a row, 25 rows — so pixel (x, y) is bit `7 - x % 8`
21
+ // of byte `(y / 8) * 320 + (x / 8) * 8 + y % 8`, which `offset` computes
22
+ // with a row table and masks, no multiply. Where the picture is, and
23
+ // why the colour matrix is under the I/O area, is ./geometry.8bs's story
24
+ // (the `Bitmap` namespace).
25
+ //
26
+ // ---- colours ----------------------------------------------------------------
27
+ //
28
+ // Hi-res: a set bit shows the cell's high nybble, a clear bit its low
29
+ // nybble, both from the colour matrix — sixteen colours, two per 8x8
30
+ // cell. Multicolour: pixels are pairs of bits, twice as wide (x 0-159),
31
+ // %00 the background register ($D021, screen.setBackground), %01 the
32
+ // matrix's high nybble, %10 its low nybble, %11 the cell's colour RAM
33
+ // nybble — four colours a cell, three of them per cell.
34
+ //
35
+ // The colour matrix is RAM under the I/O area, so a matrix write is a
36
+ // window with I/O banked out (writeUnderIo in ./index.8bs): `setCellColors`
37
+ // is one such window, `fillColors` is one window for all thousand cells —
38
+ // about 15000 cycles, most of a frame — and with the raster interrupt on,
39
+ // that frame's entries wait for it. Colour RAM is I/O and written freely.
40
+ //
41
+ // ---- sprites over a bitmap --------------------------------------------------
42
+ //
43
+ // The sprite pointers move with the screen matrix, to $DFF8 under the I/O
44
+ // area; sprites.setShape() knows (it reads `videoMode`). The shape blocks
45
+ // at $E400 are the bitmap now, so a sprite over a bitmap draws from blocks
46
+ // 96-111 at $D800 (Bitmap.SHAPE_BLOCK_FIRST/SHAPE_COUNT), written with
47
+ // sprites.setShapeByte(), which writes under the I/O area for those.
48
+ //
49
+ // ---- text and bitmap ----------------------------------------------------------
50
+ //
51
+ // One or the other. `enter` switches the VIC to the bitmap over the same
52
+ // $E000 RAM the text screen used, and `leave` switches back — the text is
53
+ // gone, the bitmap is what the screen matrix now holds (screen.blank()
54
+ // clears it). @8bitscript/text writes screen codes into the bitmap while
55
+ // this mode is on; nothing stops it, and it draws noise.
56
+ import {
57
+ control1, control2, memoryPointer, setupVideo, videoMode, VideoMode,
58
+ bankIoOut, bankIoIn, writeUnderIo,
59
+ } from "./index.8bs";
60
+ import { Video, Bitmap, bitmapRam, colorRam } from "./geometry.8bs";
61
+
62
+ // Byte offset of each cell row's first byte: row * 320.
63
+ const ROW_OFFSET: array<usmallint, 25> = [
64
+ 0, 320, 640, 960, 1280, 1600, 1920, 2240, 2560, 2880, 3200, 3520, 3840,
65
+ 4160, 4480, 4800, 5120, 5440, 5760, 6080, 6400, 6720, 7040, 7360, 7680,
66
+ ];
67
+ // Hi-res: the bit of pixel x % 8, left to right.
68
+ const BIT: array<utinyint, 8> = [128, 64, 32, 16, 8, 4, 2, 1];
69
+ const NOT_BIT: array<utinyint, 8> = [127, 191, 223, 239, 247, 251, 253, 254];
70
+ // Multicolour: the two bits of pixel x % 4, and a colour 0-3 shifted into them
71
+ // (index (x % 4) * 4 + colour).
72
+ const PAIR_MASK: array<utinyint, 4> = [0x3F, 0xCF, 0xF3, 0xFC];
73
+ const PAIR_BITS: array<utinyint, 16> = [
74
+ 0x00, 0x40, 0x80, 0xC0,
75
+ 0x00, 0x10, 0x20, 0x30,
76
+ 0x00, 0x04, 0x08, 0x0C,
77
+ 0x00, 0x01, 0x02, 0x03,
78
+ ];
79
+ const PAIR_SHIFT: array<utinyint, 4> = [6, 4, 2, 0];
80
+
81
+ export namespace bitmap {
82
+ const WIDTH: usmallint = 320; // hi-res pixels across
83
+ const HEIGHT: utinyint = 200;
84
+ const MULTICOLOR_WIDTH: utinyint = 160; // multicolour pixels across
85
+ const BYTES: usmallint = 8000;
86
+
87
+ // Bitmap mode on, hi-res or multicolour, over whatever the RAM holds.
88
+ function enter(multicolor: bool): void {
89
+ setupVideo();
90
+ videoMode = VideoMode.BITMAP;
91
+ // Bits 0-4 (YSCROLL, RSEL, DEN) kept, ECM off, BMM on, and bit 7 —
92
+ // the raster compare's ninth bit on a write — clear.
93
+ control1 = (control1 & 0x1F) | 0x20;
94
+ if (multicolor) {
95
+ control2 = control2 | 0x10;
96
+ } else {
97
+ control2 = control2 & 0xEF;
98
+ }
99
+ memoryPointer = Bitmap.MEMORY_POINTER;
100
+ }
101
+
102
+ // Text mode again: the screen matrix at $E000 over the upper-case set.
103
+ function leave(): void {
104
+ control1 = control1 & 0x1F;
105
+ control2 = control2 & 0xEF;
106
+ memoryPointer = Video.MEMORY_POINTER_UPPERCASE;
107
+ videoMode = VideoMode.TEXT;
108
+ }
109
+
110
+ // Every pixel clear (`bits` 0) or set (255), or any pattern byte.
111
+ function clear(bits: utinyint = 0): void {
112
+ for (let i: usmallint = 0; i < Bitmap.BYTES; i++) {
113
+ bitmapRam[i] = bits;
114
+ }
115
+ }
116
+
117
+ // The byte holding hi-res pixel (x, y): see the header.
118
+ function offset(x: usmallint, y: utinyint): usmallint {
119
+ return ROW_OFFSET[y >> 3] + (x & 0x1F8) + (y & 7);
120
+ }
121
+
122
+ function plot(x: usmallint, y: utinyint): void {
123
+ let at: usmallint = bitmap.offset(x, y);
124
+ bitmapRam[at] = bitmapRam[at] | BIT[x & 7];
125
+ }
126
+
127
+ function unplot(x: usmallint, y: utinyint): void {
128
+ let at: usmallint = bitmap.offset(x, y);
129
+ bitmapRam[at] = bitmapRam[at] & NOT_BIT[x & 7];
130
+ }
131
+
132
+ function point(x: usmallint, y: utinyint): bool {
133
+ return (bitmapRam[bitmap.offset(x, y)] & BIT[x & 7]) != 0;
134
+ }
135
+
136
+ // Multicolour pixel (x 0-159, y) set to `color` 0-3 (see the header).
137
+ function plotColor(x: utinyint, y: utinyint, color: utinyint): void {
138
+ let at: usmallint = ROW_OFFSET[y >> 3] + ((x & 0xFC) << 1) + (y & 7);
139
+ let pair: utinyint = x & 3;
140
+ bitmapRam[at] = (bitmapRam[at] & PAIR_MASK[pair]) | PAIR_BITS[pair * 4 + (color & 3)];
141
+ }
142
+
143
+ // The colour 0-3 of multicolour pixel (x, y).
144
+ function pointColor(x: utinyint, y: utinyint): utinyint {
145
+ let at: usmallint = ROW_OFFSET[y >> 3] + ((x & 0xFC) << 1) + (y & 7);
146
+ return (bitmapRam[at] >> PAIR_SHIFT[x & 3]) & 3;
147
+ }
148
+
149
+ // Cell `cell` (row * 40 + column): the colour of set bits and of clear
150
+ // bits (hi-res), or of %01 and %10 pixels (multicolour).
151
+ function setCellColors(cell: usmallint, foreground: utinyint, background: utinyint): void {
152
+ writeUnderIo(Bitmap.MATRIX + cell, (foreground << 4) | (background & 15));
153
+ }
154
+
155
+ // The same pair for every cell, in one window.
156
+ function fillColors(foreground: utinyint, background: utinyint): void {
157
+ let pair: utinyint = (foreground << 4) | (background & 15);
158
+ bankIoOut();
159
+ for (let cell: usmallint = 0; cell < Video.CELL_COUNT; cell++) {
160
+ memory.write(Bitmap.MATRIX + cell, pair);
161
+ }
162
+ bankIoIn();
163
+ }
164
+
165
+ // Multicolour's third colour per cell (%11), from colour RAM.
166
+ function setCellColor3(cell: usmallint, color: utinyint): void {
167
+ colorRam[cell] = color & 15;
168
+ }
169
+
170
+ function fillColor3(color: utinyint): void {
171
+ for (let cell: usmallint = 0; cell < Video.CELL_COUNT; cell++) {
172
+ colorRam[cell] = color & 15;
173
+ }
174
+ }
175
+ }
@@ -0,0 +1,142 @@
1
+ // @8bitscript/c64/charset — the character set is RAM: redefine it.
2
+ //
3
+ // Named by "8bitscript".exports["./charset"] in this package's package.json:
4
+ //
5
+ // import { charset } from "@8bitscript/c64/charset";
6
+ //
7
+ // // Screen code 1 ('A' as text prints it) becomes a solid block with a hole.
8
+ // charset.define(1, 0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xFF);
9
+ // charset.copy(2, 1); // and code 2 the same
10
+ // charset.restore(); // the ROM's glyphs again
11
+ //
12
+ // The set the VIC draws text from is this package's copy of the character
13
+ // ROM in the RAM under the I/O area ($D000, ./geometry.8bs), and a copy is
14
+ // writable: a program's own tiles, a font, a map's terrain are glyphs
15
+ // written over the ROM's, each 8 bytes — one byte a pixel row, bit 7 the
16
+ // left pixel — at `Video.CHARSET + 8 * code`. The upper-case set (256
17
+ // glyphs at $D000-$D7FF) is the one @8bitscript/text selects on every
18
+ // print, so it is the one to define into: codes 1-26 are what text prints
19
+ // for 'A'-'Z', 32-63 the digits and punctuation, 64-127 the graphics, and
20
+ // 128-255 the same in reverse video. A program that never prints text is
21
+ // free to use all 256.
22
+ //
23
+ // Every write here is a window with the I/O area banked out (./index.8bs,
24
+ // writeUnderIo): `define` is one window for a glyph's 8 bytes, `copy`
25
+ // one for 8 reads and 8 writes. With the raster interrupt on, each window
26
+ // is a few dozen cycles that frame's entries wait for; `restore` is a
27
+ // second ROM copy (about four frames of window) — do it before
28
+ // raster.enable(), or accept the frame.
29
+ //
30
+ // Multicolour text ($D016 bit 4, `charset.setMulticolor`): in a cell whose
31
+ // colour RAM nybble has bit 3 set, pixels are pairs — %00 the background,
32
+ // %01 $D022, %10 $D023, %11 the low three bits of the cell's colour — and
33
+ // a cell without bit 3 is drawn hi-res in that colour, so one screen mixes
34
+ // both. `useLowercase` selects the ROM's other set at $D800 for a program
35
+ // that wants it; @8bitscript/text's next print selects upper-case again.
36
+ import {
37
+ control2, memoryPointer, backgroundColor1, backgroundColor2,
38
+ setupVideo, copyCharacterRom, videoMode, VideoMode, bankIoOut, bankIoIn, readUnderIo,
39
+ } from "./index.8bs";
40
+ import { Video } from "./geometry.8bs";
41
+
42
+ export namespace charset {
43
+ const COUNT: usmallint = 256;
44
+ const BYTES: utinyint = 8; // a glyph: one byte a row
45
+
46
+ // Where glyph `code`'s 8 bytes start, in the upper-case set.
47
+ function offset(code: utinyint): usmallint {
48
+ return Video.CHARSET + code * 8;
49
+ }
50
+
51
+ // Glyph `code` becomes the eight rows given, top to bottom.
52
+ function define(code: utinyint, row0: utinyint, row1: utinyint, row2: utinyint, row3: utinyint, row4: utinyint, row5: utinyint, row6: utinyint, row7: utinyint): void {
53
+ setupVideo();
54
+ let at: usmallint = charset.offset(code);
55
+ bankIoOut();
56
+ memory.write(at, row0);
57
+ memory.write(at + 1, row1);
58
+ memory.write(at + 2, row2);
59
+ memory.write(at + 3, row3);
60
+ memory.write(at + 4, row4);
61
+ memory.write(at + 5, row5);
62
+ memory.write(at + 6, row6);
63
+ memory.write(at + 7, row7);
64
+ bankIoIn();
65
+ }
66
+
67
+ // One row of glyph `code`.
68
+ function setRow(code: utinyint, row: utinyint, bits: utinyint): void {
69
+ setupVideo();
70
+ bankIoOut();
71
+ memory.write(charset.offset(code) + (row & 7), bits);
72
+ bankIoIn();
73
+ }
74
+
75
+ // Read one row back.
76
+ function readRow(code: utinyint, row: utinyint): utinyint {
77
+ setupVideo();
78
+ return readUnderIo(charset.offset(code) + (row & 7));
79
+ }
80
+
81
+ // Glyph `target` becomes a copy of glyph `source`.
82
+ function copy(target: utinyint, source: utinyint): void {
83
+ setupVideo();
84
+ let sourceAt: usmallint = charset.offset(source);
85
+ let targetAt: usmallint = charset.offset(target);
86
+ bankIoOut();
87
+ for (let i: utinyint = 0; i < 8; i++) {
88
+ memory.write(targetAt + i, memory.read(sourceAt + i));
89
+ }
90
+ bankIoIn();
91
+ }
92
+
93
+ // Every row of glyph `code` the same byte: 0 blank, 255 solid.
94
+ function fill(code: utinyint, bits: utinyint): void {
95
+ setupVideo();
96
+ let at: usmallint = charset.offset(code);
97
+ bankIoOut();
98
+ for (let i: utinyint = 0; i < 8; i++) {
99
+ memory.write(at + i, bits);
100
+ }
101
+ bankIoIn();
102
+ }
103
+
104
+ // The ROM's glyphs again, both sets — all 4K under the I/O area, so
105
+ // in bitmap mode this also overwrites the colour matrix and the shape
106
+ // blocks that live there: leave bitmap mode first.
107
+ function restore(): void {
108
+ setupVideo();
109
+ copyCharacterRom();
110
+ }
111
+
112
+ // Multicolour text mode on or off (see the header).
113
+ function setMulticolor(on: bool): void {
114
+ if (on) {
115
+ control2 = control2 | 0x10;
116
+ } else {
117
+ control2 = control2 & 0xEF;
118
+ }
119
+ }
120
+
121
+ // The two shared multicolour text colours ($D022, $D023).
122
+ function setSharedColors(color1: utinyint, color2: utinyint): void {
123
+ backgroundColor1 = color1 & 15;
124
+ backgroundColor2 = color2 & 15;
125
+ }
126
+
127
+ // The ROM's lower-case set (text mode only; the next text print
128
+ // selects upper-case again).
129
+ function useLowercase(): void {
130
+ setupVideo();
131
+ if (videoMode == VideoMode.TEXT) {
132
+ memoryPointer = Video.MEMORY_POINTER_LOWERCASE;
133
+ }
134
+ }
135
+
136
+ function useUppercase(): void {
137
+ setupVideo();
138
+ if (videoMode == VideoMode.TEXT) {
139
+ memoryPointer = Video.MEMORY_POINTER_UPPERCASE;
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,116 @@
1
+ // @8bitscript/c64 — where the picture lives: VIC bank 3, above the program.
2
+ //
3
+ // The VIC-II sees one 16K bank at a time, and everything it draws from —
4
+ // the screen matrix, the character set, the sprite shapes, the eight
5
+ // sprite pointers — has to be inside that one bank. The SDK links a
6
+ // program into $0801-$CFFF (mos-platform/c64/lib/link.ld: `ram` is
7
+ // ORIGIN $0801, LENGTH $C7FF, and the C stack grows down from $D000), and
8
+ // it decides where every byte of code and data goes. Nothing the VIC
9
+ // needs can be promised a place inside that region without a build-time
10
+ // overlap check the toolchain does not have, so the picture lives in the
11
+ // only RAM the linker never touches: $D000-$FFFF, the upper half of VIC
12
+ // bank 3 ($C000-$FFFF). The VIC always reads RAM there — its two
13
+ // character-ROM windows are in banks 0 and 2 ($1000 and $9000), not here —
14
+ // and the CPU writes RAM there too: a store to $E000-$FFFF lands under the
15
+ // KERNAL ROM whether the ROM is mapped or not.
16
+ //
17
+ // $D000-$DFFF the character ROM's 4K, copied into the RAM under the I/O
18
+ // area once at start-up (setupVideo() in ./index.8bs): the
19
+ // upper-case/graphics set at $D000, lower-case at $D800.
20
+ // The same offset ($1000) in the bank as the ROM window has
21
+ // in bank 0, so $D018's charset field is the ROM's own.
22
+ // $E000-$E3E7 the 40x25 screen matrix, one screen code per cell.
23
+ // $E3F8-$E3FF the eight sprite pointers (screen + $3F8, a VIC rule).
24
+ // $E400-$FFBF 111 sprite shape blocks of 64 bytes, blocks 144-254 of
25
+ // the bank. Block 255 ($FFC0-$FFFF) is RAM too, but its
26
+ // last six bytes are the CPU's NMI/reset/IRQ vectors — RAM
27
+ // the CPU reads once the KERNAL is banked out, which
28
+ // native/6502/raster.s fills — so it is not a shape block.
29
+ //
30
+ // In bitmap mode (@8bitscript/c64/bitmap) the same bank holds, instead:
31
+ //
32
+ // $E000-$FF3F the 8000-byte bitmap (the second 8K of the bank: $D018
33
+ // bit 3 set), over the screen matrix and the shape blocks.
34
+ // $DC00-$DFE7 the colour matrix, one byte per cell — high nybble the
35
+ // colour of set bits, low nybble of clear bits (hi-res), or
36
+ // the %01 and %10 colours (multicolour) — in the RAM under
37
+ // the I/O area, where the CPU writes only with the area
38
+ // banked out (writeUnderIo in ./index.8bs). It is the second
39
+ // half of the lower-case set, which bitmap mode gives up;
40
+ // the upper-case set at $D000-$D7FF is untouched, so leaving
41
+ // bitmap mode is a register change, not a second ROM copy.
42
+ // $DFF8-$DFFF the sprite pointers, matrix + $3F8 as always.
43
+ // $D800-$DBFF 16 shape blocks (96-111) for sprites over a bitmap, under
44
+ // the I/O area too (the first half of the lower-case set).
45
+ //
46
+ // The colour matrix could not go anywhere the CPU reaches freely: every
47
+ // 1K slot of the bank is either the program's ($C000-$CFFF), the bitmap's
48
+ // ($E000-$FFFF), or under the I/O area.
49
+ //
50
+ // Colour RAM does not move with the bank: it is the 1K of nybbles at $D800
51
+ // in the I/O area, wherever the screen is. So a cell's colour is written
52
+ // through I/O ($01 bit 2 set) while its character is written to RAM — the
53
+ // normal state of the machine, and the state setupVideo() leaves it in.
54
+ //
55
+ // Screenshots of a program using this layout under x64sc (text from the
56
+ // copied set, a sprite from block 144) are how these numbers were checked;
57
+ // packages/c64/AGENTS.md has the table.
58
+
59
+ // The addresses themselves, as this module's own consts: `@address` takes
60
+ // a literal or a const declared beside it, so the arrays over the screen,
61
+ // the colour RAM, the sprite pointers and the shape blocks are declared
62
+ // here, where the numbers are, and `Video` names the same numbers for
63
+ // everything else.
64
+ const SCREEN_ADDRESS: usmallint = 0xE000;
65
+ const COLOR_ADDRESS: usmallint = 0xD800;
66
+ const SPRITE_POINTERS_ADDRESS: usmallint = 0xE3F8;
67
+ const SHAPES_ADDRESS: usmallint = 0xE400;
68
+ const BITMAP_ADDRESS: usmallint = 0xE000;
69
+
70
+ // The screen matrix: 1000 screen codes, cell 0 top-left.
71
+ @address(SCREEN_ADDRESS)
72
+ export let screenRam: array<u8, 1000>;
73
+ // Colour RAM: the low nybble of each byte is that cell's colour.
74
+ @address(COLOR_ADDRESS)
75
+ export let colorRam: array<u8, 1000>;
76
+ // Sprite n's shape block: the VIC reads shape data from bank + 64 * pointer.
77
+ @address(SPRITE_POINTERS_ADDRESS)
78
+ export let spritePointers: array<u8, 8>;
79
+ // The shape blocks, one array: block b's 63 bytes start at 64 * (b - 144).
80
+ @address(SHAPES_ADDRESS)
81
+ export let spriteShapes: array<u8, 7104>;
82
+ // The bitmap, in bitmap mode: 8000 bytes, cell by cell (8 bytes a cell,
83
+ // 40 cells a row — see @8bitscript/c64/bitmap's offset()). The same RAM
84
+ // as the screen matrix and the shape blocks above.
85
+ @address(BITMAP_ADDRESS)
86
+ export let bitmapRam: array<u8, 8000>;
87
+
88
+ export namespace Video {
89
+ const BANK: usmallint = 0xC000; // VIC bank 3, selected through CIA2 port A bits 0-1 (= %00)
90
+ const SCREEN: usmallint = SCREEN_ADDRESS; // screen matrix: one screen code per cell
91
+ const COLOR: usmallint = COLOR_ADDRESS; // colour RAM: one nybble per cell, same cell index, never moves
92
+ const CHARSET: usmallint = 0xD000; // the character ROM's copy in RAM: upper-case set here, lower-case at +$0800
93
+ const SPRITE_POINTERS: usmallint = SPRITE_POINTERS_ADDRESS; // eight bytes: shape block per sprite (address / 64 within the bank)
94
+ const SHAPES: usmallint = SHAPES_ADDRESS; // first sprite shape block
95
+ const SHAPE_BLOCK_FIRST: utinyint = 144; // (SHAPES - BANK) / 64
96
+ const SHAPE_COUNT: utinyint = 111; // blocks 144-254
97
+ const MEMORY_POINTER_UPPERCASE: utinyint = 0x84; // $D018: screen at bank+$2000 (bits 4-7 = 8), charset at bank+$1000 (bits 1-3 = 2)
98
+ const MEMORY_POINTER_LOWERCASE: utinyint = 0x86; // the same screen, the lower-case set at bank+$1800
99
+ const COLUMNS: utinyint = 40;
100
+ const ROWS: utinyint = 25;
101
+ const CELL_COUNT: usmallint = 1000; // COLUMNS * ROWS
102
+ }
103
+
104
+ // Bitmap mode's layout, in the same bank; see the header.
105
+ export namespace Bitmap {
106
+ const ADDRESS: usmallint = BITMAP_ADDRESS; // 8000 bytes, bank + $2000
107
+ const BYTES: usmallint = 8000;
108
+ const WIDTH: usmallint = 320; // pixels, hi-res
109
+ const HEIGHT: utinyint = 200;
110
+ const MATRIX: usmallint = 0xDC00; // the colour matrix, under the I/O area: bank + $1C00
111
+ const SPRITE_POINTERS: usmallint = 0xDFF8; // MATRIX + $3F8
112
+ const SHAPES: usmallint = 0xD800; // 16 shape blocks under the I/O area
113
+ const SHAPE_BLOCK_FIRST: utinyint = 96; // (SHAPES - BANK) / 64
114
+ const SHAPE_COUNT: utinyint = 16; // blocks 96-111
115
+ const MEMORY_POINTER: utinyint = 0x78; // $D018: matrix at bank+$1C00 (bits 4-7 = 7), bitmap at bank+$2000 (bit 3)
116
+ }