@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/LICENSE +21 -0
- package/native/6502/raster.s +121 -0
- package/package.json +396 -0
- package/src/bitmap.8bs +175 -0
- package/src/charset.8bs +142 -0
- package/src/geometry.8bs +116 -0
- package/src/index.8bs +427 -0
- package/src/input.8bs +269 -0
- package/src/joystick.8bs +78 -0
- package/src/keyboard.8bs +84 -0
- package/src/keys.8bs +103 -0
- package/src/mouse.8bs +265 -0
- package/src/pointer.8bs +183 -0
- package/src/random.8bs +81 -0
- package/src/raster.8bs +190 -0
- package/src/reu.8bs +210 -0
- package/src/screen.8bs +120 -0
- package/src/scroll.8bs +130 -0
- package/src/sid.8bs +211 -0
- package/src/sprites.8bs +197 -0
- package/src/text.8bs +174 -0
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
|
+
}
|