@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/raster.8bs ADDED
@@ -0,0 +1,190 @@
1
+ // @8bitscript/c64/raster — a list of register writes, each at a raster line.
2
+ //
3
+ // Named by "8bitscript".exports["./raster"] in this package's package.json:
4
+ //
5
+ // import { raster, Register } from "@8bitscript/c64/raster";
6
+ //
7
+ // raster.clear();
8
+ // raster.at(49, Register.BORDER, 0); // black border over the picture
9
+ // raster.at(49, Register.BACKGROUND, 6); // blue playfield
10
+ // raster.at(210, Register.BACKGROUND, 0); // a black status area from line 210
11
+ // raster.at(210, Register.CONTROL_2, 0x08); // ... that does not scroll
12
+ // raster.at(250, Register.BORDER, 2); // red border below the picture
13
+ // raster.enable();
14
+ //
15
+ // The VIC-II can interrupt the CPU when the raster reaches a line, and a
16
+ // program that changes a register there — border, background, the
17
+ // character set, the scroll position, a sprite's coordinates — shows two
18
+ // things on one screen: a playfield over a status bar, a fixed strip of
19
+ // score under a scrolling world, sixteen objects from eight sprites. This
20
+ // module is that, as a list: an entry says which line, which address,
21
+ // which value, and the handler this package ships in assembly
22
+ // (native/6502/raster.s) applies each at its line, every frame, with no
23
+ // program code running in the interrupt at all. What it is not: a handler
24
+ // of the program's own. 8bitscript has no function values, so nothing a
25
+ // program writes can be named as the code to run at a line; a write list
26
+ // covers colour splits, split scrolling, character-set and screen switches
27
+ // (any $D018 value), and sprite multiplexing (rewriting a sprite's Y, X,
28
+ // pointer and colour at a line, as entries), and a computed effect —
29
+ // a sine border, a per-line colour cycle — is a list a program rebuilds
30
+ // each frame, up to the 63 entries the list holds.
31
+ //
32
+ // ---- rules ----------------------------------------------------------------
33
+ //
34
+ // - Lines run 0-255 and entries are added in ascending order; `at` refuses
35
+ // a line below the last one, and a list full at 63 entries. Entries on
36
+ // the same line are applied together. The visible picture is lines
37
+ // 50-249 (the first text row starts at 51 with the default YSCROLL);
38
+ // 0-49 and 250-255 are border. On PAL the frame has 312 lines, on NTSC
39
+ // 263: lines past 255 cannot be named, and are border on both.
40
+ // - The write lands a few cycles into the line — the handler's entry
41
+ // (about 30 cycles) plus the instruction the interrupt interrupted (up
42
+ // to 7), plus, on a bad line (every eighth, from 51 with the default
43
+ // scroll: the VIC takes 40 of the line's 63 cycles), that. So a colour
44
+ // change at a line inside the picture shows a step partway across the
45
+ // line; put it on the line BEFORE the one that should look different,
46
+ // or in the border, or on a line the VIC is not fetching (not 51, 59,
47
+ // 67, ...). Border and background writes at line 49 and 250 are clean.
48
+ // - Rebuild the list between frames: right after waitFrame(), which is
49
+ // line 0 or so, before the first entry's line. The handler reads the
50
+ // list live; `at` writes an entry's four bytes before it counts it, so
51
+ // a frame never sees a half-written entry, but a list rebuilt while the
52
+ // raster is inside it applies part of the old list and part of the new,
53
+ // for that frame.
54
+ // - `enable()` banks the KERNAL out (setupVideo() does; the CPU's vectors
55
+ // are then RAM, and this package's start-up code has already pointed
56
+ // them at a bare rti), silences both CIAs, acknowledges everything the
57
+ // VIC has pending, points the IRQ vector at the handler and runs `cli`.
58
+ // From then on, `interruptsOn` in @8bitscript/c64 is true, and every
59
+ // window the package opens with the I/O area banked out (a write under
60
+ // it: the character set, the bitmap's colour matrix) is an interrupt-
61
+ // free window — the handler cannot acknowledge $D019 through RAM. A
62
+ // long window (bitmap.fillColors, charset.restore) delays that frame's
63
+ // entries by its length; do those before `enable()`, or between frames
64
+ // and accept the frame.
65
+ // - The handler touches A, X and the list's three state bytes at
66
+ // $0300-$0302, and nothing else: no zero page, no register but $D012 and
67
+ // $D019 besides the entries' own. It costs about 30 cycles plus 25 per
68
+ // entry, out of a frame's 17000-19000.
69
+ import {
70
+ control1, raster as rasterCompare, interruptStatus, interruptMask,
71
+ cia1InterruptControl, cia2InterruptControl, setupVideo, interruptsOn,
72
+ } from "./index.8bs";
73
+
74
+ // The list, where native/6502/raster.s reads it. Four bytes an entry:
75
+ // line, address low, address high, value; then the three state bytes.
76
+ const LIST_ADDRESS: usmallint = 0x0200;
77
+ const END_ADDRESS: usmallint = 0x0300;
78
+ const INDEX_ADDRESS: usmallint = 0x0301;
79
+
80
+ @address(LIST_ADDRESS)
81
+ let rasterList: array<u8, 252>;
82
+ @address(END_ADDRESS)
83
+ let rasterEnd: volatile<u8>; // entries * 4
84
+ @address(INDEX_ADDRESS)
85
+ let rasterIndex: volatile<u8>; // the handler's next entry * 4
86
+
87
+ let lastLine: utinyint = 0;
88
+
89
+ // Addresses for `at`: the registers a split usually writes. `spriteX(n)`
90
+ // and friends below build the per-sprite ones.
91
+ export namespace Register {
92
+ const BORDER: usmallint = 0xD020;
93
+ const BACKGROUND: usmallint = 0xD021;
94
+ const BACKGROUND_1: usmallint = 0xD022;
95
+ const BACKGROUND_2: usmallint = 0xD023;
96
+ const BACKGROUND_3: usmallint = 0xD024;
97
+ const CONTROL_1: usmallint = 0xD011; // YSCROLL bits 0-2, RSEL 3, DEN 4, BMM 5, ECM 6; keep bit 7 clear
98
+ const CONTROL_2: usmallint = 0xD016; // XSCROLL bits 0-2, CSEL 3, MCM 4
99
+ const MEMORY_POINTER: usmallint = 0xD018; // screen and character set (or bitmap) in the bank
100
+ const SPRITE_POSITIONS: usmallint = 0xD000; // + 2n for sprite n's X, + 2n + 1 for its Y
101
+ const SPRITE_X_HIGH: usmallint = 0xD010;
102
+ const SPRITE_ENABLE: usmallint = 0xD015;
103
+ const SPRITE_EXPAND_Y: usmallint = 0xD017;
104
+ const SPRITE_PRIORITY: usmallint = 0xD01B;
105
+ const SPRITE_MULTICOLOR: usmallint = 0xD01C;
106
+ const SPRITE_EXPAND_X: usmallint = 0xD01D;
107
+ const SPRITE_SHARED_0: usmallint = 0xD025;
108
+ const SPRITE_SHARED_1: usmallint = 0xD026;
109
+ const SPRITE_COLORS: usmallint = 0xD027; // + n
110
+ const SPRITE_POINTERS: usmallint = 0xE3F8; // + n, in text mode (Video.SPRITE_POINTERS); in bitmap mode they are under the I/O area, out of the list's reach
111
+ const SID_VOLUME: usmallint = 0xD418;
112
+ }
113
+
114
+ export namespace raster {
115
+ const MAX: utinyint = 63; // entries
116
+
117
+ // Sprite n's registers, for `at`.
118
+ function spriteX(sprite: utinyint): usmallint {
119
+ return Register.SPRITE_POSITIONS + sprite * 2;
120
+ }
121
+ function spriteY(sprite: utinyint): usmallint {
122
+ return Register.SPRITE_POSITIONS + sprite * 2 + 1;
123
+ }
124
+ function spriteColor(sprite: utinyint): usmallint {
125
+ return Register.SPRITE_COLORS + sprite;
126
+ }
127
+ function spritePointer(sprite: utinyint): usmallint {
128
+ return Register.SPRITE_POINTERS + sprite;
129
+ }
130
+
131
+ // Empty the list. The handler, if enabled, then fires at the stale
132
+ // first line and applies nothing until entries are added again.
133
+ function clear(): void {
134
+ rasterEnd = 0;
135
+ rasterIndex = 0;
136
+ lastLine = 0;
137
+ }
138
+
139
+ // Append: at `line`, write `value` to `address`. False, and nothing
140
+ // added, if the list is full or `line` is below the last entry's.
141
+ function at(line: utinyint, address: usmallint, value: utinyint): bool {
142
+ let offset: utinyint = rasterEnd;
143
+ if (offset >= 252 || line < lastLine) {
144
+ return false;
145
+ }
146
+ rasterList[offset] = line;
147
+ rasterList[offset + 1] = address & 0xFF;
148
+ rasterList[offset + 2] = address >> 8;
149
+ rasterList[offset + 3] = value;
150
+ lastLine = line;
151
+ rasterEnd = offset + 4;
152
+ return true;
153
+ }
154
+
155
+ // How many entries the list holds.
156
+ function count(): utinyint {
157
+ return rasterEnd >> 2;
158
+ }
159
+
160
+ // Own the IRQ: the list is applied from the next frame on.
161
+ function enable(): void {
162
+ setupVideo(); // KERNAL out: the vectors are RAM
163
+ rasterIndex = 0;
164
+ control1 = control1 & 0x7F; // raster compare bit 8 = 0: lines 0-255
165
+ rasterCompare = rasterList[0];
166
+ cia1InterruptControl = 0x7F; // no CIA interrupt reaches the handler: the KERNAL's timer A is one
167
+ cia2InterruptControl = 0x7F;
168
+ let pending: utinyint = cia1InterruptControl; // reading acknowledges what was latched
169
+ pending = cia2InterruptControl;
170
+ interruptStatus = 0x0F; // and whatever the VIC had
171
+ interruptMask = 0x01; // the raster compare only
172
+ asm6502 {
173
+ jsr __8bs_c64_raster_install
174
+ }
175
+ interruptsOn = true;
176
+ asm6502 {
177
+ cli
178
+ }
179
+ }
180
+
181
+ // Interrupts off again; the list is kept.
182
+ function disable(): void {
183
+ asm6502 {
184
+ sei
185
+ }
186
+ interruptMask = 0;
187
+ interruptStatus = 0x0F;
188
+ interruptsOn = false;
189
+ }
190
+ }
package/src/reu.8bs ADDED
@@ -0,0 +1,210 @@
1
+ // @8bitscript/c64/reu — a RAM Expansion Unit: is one plugged in, how big,
2
+ // and moving memory to and from it.
3
+ //
4
+ // Named by "8bitscript".exports["./reu"] in this package's package.json:
5
+ //
6
+ // import { reu } from "@8bitscript/c64/reu";
7
+ //
8
+ // let banked: usmallint = reu.detect(); // KiB found, 0 for none
9
+ // if (banked != 0) {
10
+ // reu.stash(0xE000, 0, 0x0000, 1000); // the screen into bank 0
11
+ // ...
12
+ // reu.fetch(0xE000, 0, 0x0000, 1000); // and back
13
+ // }
14
+ //
15
+ // ---- transfers ------------------------------------------------------------
16
+ //
17
+ // The REU is a DMA controller with its own RAM in 64 KiB banks: given a
18
+ // C64 address, a REU address (bank, then 16 bits within it) and a length,
19
+ // it copies in either direction, swaps, or compares, with the CPU halted
20
+ // for about a cycle a byte — a thousand bytes in a millisecond, which no
21
+ // loop on the 6510 approaches. `stash` copies C64 → REU, `fetch` REU →
22
+ // C64, `swap` exchanges, `verify` compares and says whether they matched;
23
+ // `fillReu` writes one byte across a REU range. A length of 0 means 65536.
24
+ // The C64 side goes through the CPU's memory map as $01 has it (./index.8bs):
25
+ // $E000-$FFFF is the screen's RAM (KERNAL out), $D000-$DFFF the I/O area,
26
+ // so a transfer touching the character set or a bitmap's colour matrix
27
+ // needs the I/O area banked out around it (bankIoOut/bankIoIn) — and
28
+ // nothing here does that for a caller. Every transfer sets every register,
29
+ // so the REU's own address stepping after a transfer never matters. The
30
+ // REU's contents survive a RESET but not power off; VICE's `-reu` keeps
31
+ // nothing between runs unless told to.
32
+ //
33
+ // The first run-time hardware probe. A build fitted with a REU
34
+ // (`--hardware ram=reu512`, or a profile) says so at compile time —
35
+ // `Memory.BANKED` from @8bitscript/system is true, `Memory.BANKED_KIB` is
36
+ // what the build was fitted with — and that means "this build may use
37
+ // one", not "one is there": the same binary runs on a C64 with no REU, a
38
+ // 1700, or a 16 MiB modern one, and `detect()` is how it finds out which.
39
+ // A program that never imports this file carries none of it: only what a
40
+ // program imports is compiled, so an app with no use for a REU pays
41
+ // nothing for one, and the same goes for every probe to come. See
42
+ // docs/systems.md, "Facts", the third rule.
43
+ //
44
+ // C64-only, hardware-level, like ./sid.8bs: the layer a portable banked-
45
+ // memory capability will sit on. Two questions, answered in order:
46
+ //
47
+ // 1. Is there a REU at all? A register round trip. `$DF02` (C64 base
48
+ // address, low byte) stores what is written and reads it back on every
49
+ // REU; on a C64 without one, `$DF00`–`$DFFF` is open bus and reads
50
+ // whatever byte last crossed it, which is not two different values in
51
+ // a row. Writing $55 then $AA and reading each back is the test.
52
+ // 2. How much RAM? Sixty-four-KiB banks, found by writing a marker to the
53
+ // first byte of each bank in turn and looking for it to land somewhere
54
+ // it should not. One-byte DMA transfers do the writing and reading:
55
+ // `stash` copies a byte from C64 RAM into the REU, `fetch` the other
56
+ // way. Bank 0 is marked first; then for each higher bank, its marker
57
+ // is stashed and two things are checked — did bank 0's marker change
58
+ // (the address wrapped: this bank *is* bank 0 again), and does the
59
+ // bank read its own marker back (a bank that does not exist reads
60
+ // nothing back). Either ends the count. The units with 3-bit bank
61
+ // registers (1700, 1764, 1750: 128, 256, 512 KiB) wrap at 8 banks or
62
+ // alias below it; the larger units have all 8 bits, up to 256 banks.
63
+ // Run under VICE at 128 KiB, 256, 512, 1 MiB and 16 MiB, and with no
64
+ // REU (see AGENTS.md); verified against VICE's reu.c: the bank register's
65
+ // unused bits are forced high on those units, addresses wrap at the
66
+ // unit's size, every register reads back what was written, and
67
+ // autoload restores the bank register too.
68
+ //
69
+ // The byte the DMA reads and writes is `probe`, one location in the
70
+ // cassette buffer ($033C–$03FB), which nothing else in an 8bitscript
71
+ // program uses (interrupts are off; there is no tape). The registers are
72
+ // set once: with bit 5 of the command (autoload) the REU reloads every
73
+ // address and the length from what was written after each transfer, and
74
+ // with bit 4 the transfer runs immediately, without the $FF00 trigger the
75
+ // original REU firmware wants. So a transfer is two writes — the bank and
76
+ // the command. Measured: a program that calls `detect()` is about 180
77
+ // bytes bigger than the same program without it (the size loop is most
78
+ // of that; `present()` alone is a couple of dozen), and a program that
79
+ // does not import this file is not one byte bigger.
80
+ //
81
+ // `detect()` is safe to call on a C64 without a REU (question 1 stops it),
82
+ // costs a few hundred DMA cycles at most, and is meant to run once at
83
+ // start-up; keep the answer in a variable rather than asking again.
84
+ import {
85
+ reuStatus, reuCommand, reuC64AddressLow, reuC64AddressHigh, reuAddressLow, reuAddressHigh,
86
+ reuBank, reuLengthLow, reuLengthHigh, reuAddressControl,
87
+ } from "./index.8bs";
88
+
89
+ // The one C64 byte the transfers touch. Its address goes into $DF02/$DF03.
90
+ @address(0x033C) let probe: volatile<u8>;
91
+
92
+ namespace Reu {
93
+ const PROBE_LOW: utinyint = 0x3C; // low byte of `probe`'s address
94
+ const PROBE_HIGH: utinyint = 0x03; // high byte
95
+ const STASH: utinyint = 0xB0; // execute now, autoload, C64 → REU
96
+ const FETCH: utinyint = 0xB1; // execute now, autoload, REU → C64
97
+ const EXECUTE: utinyint = 0x90; // execute now, no autoload; OR the direction in
98
+ const TO_REU: utinyint = 0; // command bits 0-1
99
+ const FROM_REU: utinyint = 1;
100
+ const EXCHANGE: utinyint = 2;
101
+ const COMPARE: utinyint = 3;
102
+ const FIX_C64: utinyint = 0x80; // address control: the C64 address does not advance
103
+ const FAULT: utinyint = 0x20; // status: a verify found a difference
104
+ }
105
+
106
+ // One transfer: every register, then the command. `control` is the
107
+ // address-control register (0, or Reu.FIX_C64).
108
+ function transfer(direction: utinyint, control: utinyint, c64Address: usmallint, bank: utinyint, address: usmallint, length: usmallint): void {
109
+ reuC64AddressLow = c64Address & 0xFF;
110
+ reuC64AddressHigh = c64Address >> 8;
111
+ reuAddressLow = address & 0xFF;
112
+ reuAddressHigh = address >> 8;
113
+ reuBank = bank;
114
+ reuLengthLow = length & 0xFF;
115
+ reuLengthHigh = length >> 8;
116
+ reuAddressControl = control;
117
+ reuCommand = Reu.EXECUTE | direction;
118
+ }
119
+
120
+ export namespace reu {
121
+ // Question 1: is a REU present? A register that stores what it is
122
+ // given, twice, with different values.
123
+ function present(): bool {
124
+ reuC64AddressLow = 0x55;
125
+ if (reuC64AddressLow != 0x55) {
126
+ return false;
127
+ }
128
+ reuC64AddressLow = 0xAA;
129
+ return reuC64AddressLow == 0xAA;
130
+ }
131
+
132
+ // Question 2, given a REU: how many 64 KiB banks it has, 1..255, or 0
133
+ // meaning all 256 (16 MiB — the count does not fit the byte). Every
134
+ // transfer is one byte between `probe` and the first byte of a bank.
135
+ function banks(): utinyint {
136
+ reuC64AddressLow = Reu.PROBE_LOW;
137
+ reuC64AddressHigh = Reu.PROBE_HIGH;
138
+ reuAddressLow = 0;
139
+ reuAddressHigh = 0;
140
+ reuLengthLow = 1;
141
+ reuLengthHigh = 0;
142
+ reuAddressControl = 0;
143
+ probe = 0;
144
+ reuBank = 0;
145
+ reuCommand = Reu.STASH;
146
+ let bank: utinyint = 1;
147
+ while (bank != 0) {
148
+ probe = bank;
149
+ reuBank = bank;
150
+ reuCommand = Reu.STASH;
151
+ probe = 0xFF;
152
+ reuBank = 0;
153
+ reuCommand = Reu.FETCH;
154
+ if (probe == bank) {
155
+ return bank; // wrapped: this bank is bank 0 again
156
+ }
157
+ probe = 0xFF;
158
+ reuBank = bank;
159
+ reuCommand = Reu.FETCH;
160
+ if (probe != bank) {
161
+ return bank; // nothing there
162
+ }
163
+ bank = bank + 1;
164
+ }
165
+ return 0;
166
+ }
167
+
168
+ // KiB of REU RAM plugged in: 0 for none, 128 for a 1700, 512 for a
169
+ // 1750, up to 16384. One call at start-up; keep the answer.
170
+ function detect(): usmallint {
171
+ if (!reu.present()) {
172
+ return 0;
173
+ }
174
+ let found: utinyint = reu.banks();
175
+ if (found == 0) {
176
+ return 16384;
177
+ }
178
+ let kib: usmallint = found;
179
+ return kib * 64;
180
+ }
181
+
182
+ // `length` bytes from C64 `c64Address` into the REU at `bank`:`address`.
183
+ function stash(c64Address: usmallint, bank: utinyint, address: usmallint, length: usmallint): void {
184
+ transfer(Reu.TO_REU, 0, c64Address, bank, address, length);
185
+ }
186
+
187
+ // `length` bytes from the REU at `bank`:`address` into C64 `c64Address`.
188
+ function fetch(c64Address: usmallint, bank: utinyint, address: usmallint, length: usmallint): void {
189
+ transfer(Reu.FROM_REU, 0, c64Address, bank, address, length);
190
+ }
191
+
192
+ // The two ranges exchanged.
193
+ function swap(c64Address: usmallint, bank: utinyint, address: usmallint, length: usmallint): void {
194
+ transfer(Reu.EXCHANGE, 0, c64Address, bank, address, length);
195
+ }
196
+
197
+ // True if the two ranges hold the same bytes.
198
+ function verify(c64Address: usmallint, bank: utinyint, address: usmallint, length: usmallint): bool {
199
+ let cleared: utinyint = reuStatus; // reading clears the fault bit from before
200
+ transfer(Reu.COMPARE, 0, c64Address, bank, address, length);
201
+ return (reuStatus & Reu.FAULT) == 0;
202
+ }
203
+
204
+ // `length` bytes of the REU at `bank`:`address` all set to `value`: a
205
+ // stash from one C64 byte that does not advance.
206
+ function fillReu(bank: utinyint, address: usmallint, length: usmallint, value: utinyint): void {
207
+ probe = value;
208
+ transfer(Reu.TO_REU, Reu.FIX_C64, 0x033C, bank, address, length);
209
+ }
210
+ }
package/src/screen.8bs ADDED
@@ -0,0 +1,120 @@
1
+ // @8bitscript/c64/screen — the C64's implementation of @8bitscript/screen.
2
+ //
3
+ // Named by "8bitscript".exports["./screen"] in this package's package.json,
4
+ // and what
5
+ //
6
+ // import { screen, BorderColor, BackgroundColor } from "@8bitscript/screen";
7
+ //
8
+ // resolves to when the build is for the C64: @8bitscript/screen's entry is
9
+ // keyed by machine and delegates here. Built on the registers @8bitscript/c64
10
+ // itself exports, one layer up from them. Every machine's screen.8bs exports
11
+ // this same surface, which is what lets a program import it from
12
+ // @8bitscript/screen and never name the hardware.
13
+ //
14
+ // Where the screen is — VIC bank 3, above the program, with the ROM
15
+ // character set copied under the I/O area — is ./geometry.8bs's story, and
16
+ // `setupVideo()` (in ./index.8bs) is what puts it there, once, before the
17
+ // first cell is written.
18
+ import { borderColor, backgroundColor, setupVideo } from "./index.8bs";
19
+ import { Video, screenRam } from "./geometry.8bs";
20
+
21
+ // ---- the screen: border and background --------------------------------------
22
+ //
23
+ // The VIC-II gives border and background their own registers, sixteen
24
+ // colours each; the masks make the wrap around the palette explicit.
25
+ export namespace screen {
26
+ function setColors(border: u8, background: u8): void {
27
+ borderColor = border & 15;
28
+ backgroundColor = background & 15;
29
+ }
30
+
31
+ // `blank(border, background)`: every cell blank and both colours set —
32
+ // black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
33
+ // to leave one as it is. `setBorder` and `setBackground` are one
34
+ // colour each, `setColors` the pair; `text.setColor` owns what the
35
+ // next print looks like.
36
+ function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
37
+ setupVideo();
38
+ if (border != BorderColor.KEEP) {
39
+ screen.setBorder(border);
40
+ }
41
+ if (background != BackgroundColor.KEEP) {
42
+ screen.setBackground(background);
43
+ }
44
+ // Four quarters at once, indexed by one byte. Written as a single
45
+ // `cell < CELL_COUNT` loop this is a sixteen-bit counter and a
46
+ // sixteen-bit pointer walked a byte at a time with its own carry —
47
+ // what LLVM-MOS has to emit when an index does not fit a register.
48
+ // Four constant offsets off one 8-bit index is the shape a 6502
49
+ // wants: `sta $0400,x` and friends, a quarter of the iterations.
50
+ for (let i: utinyint = 0; i < 250; i++) {
51
+ screenRam[i] = 32; // 32: the space screen code
52
+ screenRam[i + 250] = 32;
53
+ screenRam[i + 500] = 32;
54
+ screenRam[i + 750] = 32;
55
+ }
56
+ }
57
+
58
+ function setBackground(background: u8): void {
59
+ backgroundColor = background & 15;
60
+ }
61
+
62
+ function setBorder(border: u8): void {
63
+ borderColor = border & 15;
64
+ }
65
+ }
66
+
67
+ // ---- colour names ----------------------------------------------------------
68
+ //
69
+ // The same eight names every machine's screen.8bs exports — Black, White,
70
+ // Red, Cyan, Purple, Green, Blue, Yellow — so a program can write
71
+ // `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` through
72
+ // @8bitscript/screen and get blue on every target; the values are this
73
+ // machine's own. The VIC-II has sixteen colours in both registers, so both
74
+ // namespaces carry all sixteen, in the VIC-II's own numbering — the
75
+ // numbering the VIC-20 shares for its first eight, and that @8bitscript/web
76
+ // and @8bitscript/cx16 borrow.
77
+
78
+ export namespace BorderColor {
79
+ const BLACK: utinyint = 0;
80
+ const WHITE: utinyint = 1;
81
+ const RED: utinyint = 2;
82
+ const CYAN: utinyint = 3;
83
+ const PURPLE: utinyint = 4;
84
+ const GREEN: utinyint = 5;
85
+ const BLUE: utinyint = 6;
86
+ const YELLOW: utinyint = 7;
87
+ const ORANGE: utinyint = 8;
88
+ const BROWN: utinyint = 9;
89
+ const LIGHT_RED: utinyint = 10;
90
+ const DARK_GREY: utinyint = 11;
91
+ const GREY: utinyint = 12;
92
+ const LIGHT_GREEN: utinyint = 13;
93
+ const LIGHT_BLUE: utinyint = 14;
94
+ const LIGHT_GREY: utinyint = 15;
95
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
96
+ // a value no register here takes.
97
+ const KEEP: utinyint = 255;
98
+ }
99
+
100
+ export namespace BackgroundColor {
101
+ const BLACK: utinyint = 0;
102
+ const WHITE: utinyint = 1;
103
+ const RED: utinyint = 2;
104
+ const CYAN: utinyint = 3;
105
+ const PURPLE: utinyint = 4;
106
+ const GREEN: utinyint = 5;
107
+ const BLUE: utinyint = 6;
108
+ const YELLOW: utinyint = 7;
109
+ const ORANGE: utinyint = 8;
110
+ const BROWN: utinyint = 9;
111
+ const LIGHT_RED: utinyint = 10;
112
+ const DARK_GREY: utinyint = 11;
113
+ const GREY: utinyint = 12;
114
+ const LIGHT_GREEN: utinyint = 13;
115
+ const LIGHT_BLUE: utinyint = 14;
116
+ const LIGHT_GREY: utinyint = 15;
117
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
118
+ // a value no register here takes.
119
+ const KEEP: utinyint = 255;
120
+ }
package/src/scroll.8bs ADDED
@@ -0,0 +1,130 @@
1
+ // @8bitscript/c64/scroll — the VIC-II's fine scroll, and coarse shifts of
2
+ // the text screen.
3
+ //
4
+ // Named by "8bitscript".exports["./scroll"] in this package's package.json:
5
+ //
6
+ // import { scroll } from "@8bitscript/c64/scroll";
7
+ //
8
+ // scroll.setNarrow(true); // 38 columns: the edges are border
9
+ // let fine: u8 = 7;
10
+ // ...
11
+ // // each frame, a world moving left:
12
+ // if (fine == 0) {
13
+ // fine = 7;
14
+ // scroll.shiftLeft(32, 1); // a column of spaces in, white
15
+ // ... draw the new column at 39 ...
16
+ // } else {
17
+ // fine = fine - 1;
18
+ // }
19
+ // scroll.setX(fine);
20
+ //
21
+ // Hardware-level, C64-only surface. The VIC-II shifts the whole picture
22
+ // by 0-7 pixels horizontally ($D016 bits 0-2, XSCROLL) and vertically
23
+ // ($D011 bits 0-2, YSCROLL; 3 is the boot value that puts the first row
24
+ // at line 51), and hides the edge a scroll exposes by drawing 38 columns
25
+ // instead of 40 ($D016 bit 3, CSEL, clear) or 24 rows instead of 25
26
+ // ($D011 bit 3, RSEL, clear) — the border grows over the outer half-cells.
27
+ // Smooth scrolling is the two together: fine scroll one pixel a frame,
28
+ // and every eighth frame shift the screen a cell and reset the fine scroll,
29
+ // as the sketch above does. The shifts move screen codes and colour RAM
30
+ // together and fill the column or row that opens with one code and colour;
31
+ // each is a thousand-cell copy, some 20000 cycles — a frame's worth of
32
+ // CPU on the frame it happens.
33
+ //
34
+ // YSCROLL moves the bad lines: the VIC fetches a row of screen codes on
35
+ // every line whose low three bits equal YSCROLL, so a raster list's timing
36
+ // (@8bitscript/c64/raster) shifts with it. Reads of the screen work
37
+ // because the KERNAL is banked out (./index.8bs, setupVideo).
38
+ import { control1, control2, setupVideo } from "./index.8bs";
39
+ import { Video, screenRam, colorRam } from "./geometry.8bs";
40
+
41
+ export namespace scroll {
42
+ // Horizontal fine scroll, 0-7 pixels right.
43
+ function setX(pixels: utinyint): void {
44
+ control2 = (control2 & 0xF8) | (pixels & 7);
45
+ }
46
+
47
+ // Vertical fine scroll, 0-7 pixels down; 3 is the default position.
48
+ // Bit 7 — the raster compare's ninth bit on a write — is kept clear.
49
+ function setY(pixels: utinyint): void {
50
+ control1 = (control1 & 0x78) | (pixels & 7);
51
+ }
52
+
53
+ // 38 columns (true) or 40: the border covers half a cell at each side.
54
+ function setNarrow(on: bool): void {
55
+ if (on) {
56
+ control2 = control2 & 0xF7;
57
+ } else {
58
+ control2 = control2 | 0x08;
59
+ }
60
+ }
61
+
62
+ // 24 rows (true) or 25: the border covers half a cell top and bottom.
63
+ function setShort(on: bool): void {
64
+ if (on) {
65
+ control1 = control1 & 0x77;
66
+ } else {
67
+ control1 = (control1 & 0x7F) | 0x08;
68
+ }
69
+ }
70
+
71
+ // Every cell moves one column left; column 39 becomes `code` in `color`.
72
+ function shiftLeft(code: utinyint, color: utinyint): void {
73
+ setupVideo();
74
+ let cell: usmallint = 0;
75
+ for (let row: utinyint = 0; row < Video.ROWS; row++) {
76
+ for (let column: utinyint = 0; column < 39; column++) {
77
+ screenRam[cell] = screenRam[cell + 1];
78
+ colorRam[cell] = colorRam[cell + 1];
79
+ cell = cell + 1;
80
+ }
81
+ screenRam[cell] = code;
82
+ colorRam[cell] = color;
83
+ cell = cell + 1;
84
+ }
85
+ }
86
+
87
+ // Every cell moves one column right; column 0 becomes `code` in `color`.
88
+ function shiftRight(code: utinyint, color: utinyint): void {
89
+ setupVideo();
90
+ let cell: usmallint = Video.CELL_COUNT - 1;
91
+ for (let row: utinyint = 0; row < Video.ROWS; row++) {
92
+ for (let column: utinyint = 0; column < 39; column++) {
93
+ screenRam[cell] = screenRam[cell - 1];
94
+ colorRam[cell] = colorRam[cell - 1];
95
+ cell = cell - 1;
96
+ }
97
+ screenRam[cell] = code;
98
+ colorRam[cell] = color;
99
+ cell = cell - 1;
100
+ }
101
+ }
102
+
103
+ // Every row moves up one; row 24 becomes `code` in `color`.
104
+ function shiftUp(code: utinyint, color: utinyint): void {
105
+ setupVideo();
106
+ for (let cell: usmallint = 0; cell < 960; cell++) {
107
+ screenRam[cell] = screenRam[cell + 40];
108
+ colorRam[cell] = colorRam[cell + 40];
109
+ }
110
+ for (let cell: usmallint = 960; cell < Video.CELL_COUNT; cell++) {
111
+ screenRam[cell] = code;
112
+ colorRam[cell] = color;
113
+ }
114
+ }
115
+
116
+ // Every row moves down one; row 0 becomes `code` in `color`.
117
+ function shiftDown(code: utinyint, color: utinyint): void {
118
+ setupVideo();
119
+ let cell: usmallint = Video.CELL_COUNT - 1;
120
+ while (cell >= 40) {
121
+ screenRam[cell] = screenRam[cell - 40];
122
+ colorRam[cell] = colorRam[cell - 40];
123
+ cell = cell - 1;
124
+ }
125
+ for (let first: utinyint = 0; first < 40; first++) {
126
+ screenRam[first] = code;
127
+ colorRam[first] = color;
128
+ }
129
+ }
130
+ }