@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/sid.8bs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// @8bitscript/c64/sid — the SID, voice by voice.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./sid"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { sid, Waveform, Note } from "@8bitscript/c64/sid";
|
|
6
|
+
//
|
|
7
|
+
// sid.setVolume(15);
|
|
8
|
+
// sid.setEnvelope(0, 0, 9, 8, 6);
|
|
9
|
+
// sid.setWaveform(0, Waveform.PULSE);
|
|
10
|
+
// sid.setPulseWidth(0, 2048);
|
|
11
|
+
// sid.play(0, Note.A4); // gate on, at 440 Hz on a PAL machine
|
|
12
|
+
// ...
|
|
13
|
+
// sid.release(0); // gate off: the release phase begins
|
|
14
|
+
//
|
|
15
|
+
// Hardware-level, C64-only surface — the layer a portable, note-level
|
|
16
|
+
// sound capability (packages/studio/AGENTS.md) will sit on. It is the
|
|
17
|
+
// 6581/8580's three voices as they are: an oscillator with four waveforms
|
|
18
|
+
// (triangle, sawtooth, pulse with a 12-bit width, noise), an ADSR envelope
|
|
19
|
+
// per voice, ring modulation and hard sync between neighbours, one filter
|
|
20
|
+
// shared by all three, and a master volume. Nothing here is a tracker, a
|
|
21
|
+
// sequencer or a driver; it is the registers with their units named.
|
|
22
|
+
//
|
|
23
|
+
// The SID's registers are write-only, so this file keeps a copy of each
|
|
24
|
+
// voice's control byte: `setWaveform` changes the waveform bits and keeps
|
|
25
|
+
// the gate, `play`/`release` change the gate and keep the waveform.
|
|
26
|
+
//
|
|
27
|
+
// ---- pitch --------------------------------------------------------------
|
|
28
|
+
//
|
|
29
|
+
// A voice's 16-bit frequency register is `Fn`, and the pitch it plays is
|
|
30
|
+
// `Fn * clock / 16777216` Hz (the 6581 datasheet: 2^24), where `clock` is
|
|
31
|
+
// the CPU clock — 985248 Hz on a PAL machine, 1022727 on NTSC. `Note`
|
|
32
|
+
// names the equal-tempered notes C0 to B6 (A4 = 440 Hz), and `NOTE_PAL`
|
|
33
|
+
// and `NOTE_NTSC` hold each one's `Fn` for the two clocks, computed from
|
|
34
|
+
// that formula and checked by packages/compiler/test/c64-package.test.mjs.
|
|
35
|
+
// One build runs on both regions (there is no region at build time), so
|
|
36
|
+
// which table `play` reads is a run-time choice: `sid.setRegion(Region.X)`
|
|
37
|
+
// from a program that knows, or `sid.detectRegion()`, which watches one
|
|
38
|
+
// frame of the raster (detectRegion in @8bitscript/c64: two frames, once)
|
|
39
|
+
// and chooses. Until either is called the PAL table is in use, and on an
|
|
40
|
+
// NTSC machine that plays every note about 3.8 percent sharp: two thirds
|
|
41
|
+
// of a semitone, in tune with itself. Both tables stop at the last
|
|
42
|
+
// complete octave: B7's `Fn` would overflow the register on PAL (the
|
|
43
|
+
// ceiling is about 3848 Hz), though C7 to A#7 would fit, and NTSC's
|
|
44
|
+
// higher ceiling is not a reason for the tables to differ; `setFrequency`
|
|
45
|
+
// takes any `Fn` directly.
|
|
46
|
+
import { sidRegisters, detectRegion, Region } from "./index.8bs";
|
|
47
|
+
|
|
48
|
+
// Control-register waveform bits; one of the four, or the ring/sync
|
|
49
|
+
// modifiers OR-ed in. Ring modulation replaces a voice's triangle output
|
|
50
|
+
// with the ring product against the previous voice (voice 0's partner is
|
|
51
|
+
// voice 2); sync resets the oscillator on the previous voice's cycle.
|
|
52
|
+
export namespace Waveform {
|
|
53
|
+
const TRIANGLE: utinyint = 0x10;
|
|
54
|
+
const SAWTOOTH: utinyint = 0x20;
|
|
55
|
+
const PULSE: utinyint = 0x40;
|
|
56
|
+
const NOISE: utinyint = 0x80;
|
|
57
|
+
const RING: utinyint = 0x04;
|
|
58
|
+
const SYNC: utinyint = 0x02;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Filter modes for `setFilter`: OR them together for more than one.
|
|
62
|
+
export namespace Filter {
|
|
63
|
+
const OFF: utinyint = 0x00;
|
|
64
|
+
const LOW_PASS: utinyint = 0x10;
|
|
65
|
+
const BAND_PASS: utinyint = 0x20;
|
|
66
|
+
const HIGH_PASS: utinyint = 0x40;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Note indexes into NOTE_PAL: octave * 12 + semitone, C0 = 0, A4 = 57.
|
|
70
|
+
// CS is C sharp, and so on.
|
|
71
|
+
export namespace Note {
|
|
72
|
+
const C0: utinyint = 0; const CS0: utinyint = 1; const D0: utinyint = 2; const DS0: utinyint = 3; const E0: utinyint = 4; const F0: utinyint = 5; const FS0: utinyint = 6; const G0: utinyint = 7; const GS0: utinyint = 8; const A0: utinyint = 9; const AS0: utinyint = 10; const B0: utinyint = 11;
|
|
73
|
+
const C1: utinyint = 12; const CS1: utinyint = 13; const D1: utinyint = 14; const DS1: utinyint = 15; const E1: utinyint = 16; const F1: utinyint = 17; const FS1: utinyint = 18; const G1: utinyint = 19; const GS1: utinyint = 20; const A1: utinyint = 21; const AS1: utinyint = 22; const B1: utinyint = 23;
|
|
74
|
+
const C2: utinyint = 24; const CS2: utinyint = 25; const D2: utinyint = 26; const DS2: utinyint = 27; const E2: utinyint = 28; const F2: utinyint = 29; const FS2: utinyint = 30; const G2: utinyint = 31; const GS2: utinyint = 32; const A2: utinyint = 33; const AS2: utinyint = 34; const B2: utinyint = 35;
|
|
75
|
+
const C3: utinyint = 36; const CS3: utinyint = 37; const D3: utinyint = 38; const DS3: utinyint = 39; const E3: utinyint = 40; const F3: utinyint = 41; const FS3: utinyint = 42; const G3: utinyint = 43; const GS3: utinyint = 44; const A3: utinyint = 45; const AS3: utinyint = 46; const B3: utinyint = 47;
|
|
76
|
+
const C4: utinyint = 48; const CS4: utinyint = 49; const D4: utinyint = 50; const DS4: utinyint = 51; const E4: utinyint = 52; const F4: utinyint = 53; const FS4: utinyint = 54; const G4: utinyint = 55; const GS4: utinyint = 56; const A4: utinyint = 57; const AS4: utinyint = 58; const B4: utinyint = 59;
|
|
77
|
+
const C5: utinyint = 60; const CS5: utinyint = 61; const D5: utinyint = 62; const DS5: utinyint = 63; const E5: utinyint = 64; const F5: utinyint = 65; const FS5: utinyint = 66; const G5: utinyint = 67; const GS5: utinyint = 68; const A5: utinyint = 69; const AS5: utinyint = 70; const B5: utinyint = 71;
|
|
78
|
+
const C6: utinyint = 72; const CS6: utinyint = 73; const D6: utinyint = 74; const DS6: utinyint = 75; const E6: utinyint = 76; const F6: utinyint = 77; const FS6: utinyint = 78; const G6: utinyint = 79; const GS6: utinyint = 80; const A6: utinyint = 81; const AS6: utinyint = 82; const B6: utinyint = 83;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fn per note for a 985248 Hz clock: round(f * 2^24 / 985248).
|
|
82
|
+
const NOTE_PAL: array<usmallint, 84> = [
|
|
83
|
+
278, 295, 313, 331, 351, 372, 394, 417, 442, 468, 496, 526, // octave 0
|
|
84
|
+
557, 590, 625, 662, 702, 743, 788, 834, 884, 937, 992, 1051, // octave 1
|
|
85
|
+
1114, 1180, 1250, 1325, 1403, 1487, 1575, 1669, 1768, 1873, 1985, 2103, // octave 2
|
|
86
|
+
2228, 2360, 2500, 2649, 2807, 2973, 3150, 3338, 3536, 3746, 3969, 4205, // octave 3
|
|
87
|
+
4455, 4720, 5001, 5298, 5613, 5947, 6300, 6675, 7072, 7493, 7938, 8410, // octave 4
|
|
88
|
+
8910, 9440, 10001, 10596, 11226, 11894, 12601, 13350, 14144, 14985, 15876, 16820, // octave 5
|
|
89
|
+
17820, 18880, 20003, 21192, 22452, 23787, 25202, 26700, 28288, 29970, 31752, 33640, // octave 6
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// Fn per note for a 1022727 Hz clock: round(f * 2^24 / 1022727).
|
|
93
|
+
const NOTE_NTSC: array<usmallint, 84> = [
|
|
94
|
+
268, 284, 301, 319, 338, 358, 379, 402, 426, 451, 478, 506, // octave 0
|
|
95
|
+
536, 568, 602, 638, 676, 716, 759, 804, 852, 902, 956, 1013, // octave 1
|
|
96
|
+
1073, 1137, 1204, 1276, 1352, 1432, 1517, 1608, 1703, 1804, 1912, 2025, // octave 2
|
|
97
|
+
2146, 2274, 2409, 2552, 2704, 2864, 3035, 3215, 3406, 3609, 3824, 4051, // octave 3
|
|
98
|
+
4292, 4547, 4817, 5104, 5407, 5729, 6070, 6430, 6813, 7218, 7647, 8102, // octave 4
|
|
99
|
+
8584, 9094, 9635, 10208, 10815, 11458, 12139, 12861, 13626, 14436, 15294, 16204, // octave 5
|
|
100
|
+
17167, 18188, 19270, 20415, 21629, 22916, 24278, 25722, 27251, 28872, 30589, 32407, // octave 6
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// Which table `play` reads: Region.PAL until told otherwise.
|
|
104
|
+
let region: utinyint = 0;
|
|
105
|
+
|
|
106
|
+
// Each voice's register base: 7 * voice, without a multiply.
|
|
107
|
+
const VOICE_BASE: array<utinyint, 3> = [0, 7, 14];
|
|
108
|
+
|
|
109
|
+
// The control byte last written per voice (write-only register).
|
|
110
|
+
let control: array<utinyint, 3>;
|
|
111
|
+
// $D417 and $D418, likewise: filter routing/resonance and mode/volume.
|
|
112
|
+
let filterControl: utinyint = 0;
|
|
113
|
+
let modeVolume: utinyint = 0;
|
|
114
|
+
|
|
115
|
+
export namespace sid {
|
|
116
|
+
const VOICE_COUNT: utinyint = 3;
|
|
117
|
+
|
|
118
|
+
// Master volume, 0-15. Filter mode bits are kept.
|
|
119
|
+
function setVolume(volume: utinyint): void {
|
|
120
|
+
modeVolume = (modeVolume & 0xF0) | (volume & 15);
|
|
121
|
+
sidRegisters[24] = modeVolume;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The raw 16-bit frequency register; see the pitch formula above.
|
|
125
|
+
function setFrequency(voice: utinyint, frequency: usmallint): void {
|
|
126
|
+
sidRegisters[VOICE_BASE[voice]] = frequency & 0xFF;
|
|
127
|
+
sidRegisters[VOICE_BASE[voice] + 1] = frequency >> 8;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Pulse width for Waveform.PULSE, 0-4095; 2048 is a square wave.
|
|
131
|
+
function setPulseWidth(voice: utinyint, width: usmallint): void {
|
|
132
|
+
sidRegisters[VOICE_BASE[voice] + 2] = width & 0xFF;
|
|
133
|
+
sidRegisters[VOICE_BASE[voice] + 3] = (width >> 8) & 0x0F;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Attack and decay rates 0-15 (2 ms to 8 s), sustain level 0-15,
|
|
137
|
+
// release rate 0-15 (6 ms to 24 s) — the datasheet's tables.
|
|
138
|
+
function setEnvelope(voice: utinyint, attack: utinyint, decay: utinyint, sustain: utinyint, release: utinyint): void {
|
|
139
|
+
sidRegisters[VOICE_BASE[voice] + 5] = (attack << 4) | (decay & 15);
|
|
140
|
+
sidRegisters[VOICE_BASE[voice] + 6] = (sustain << 4) | (release & 15);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The waveform (and ring/sync) bits; the gate is left as it is.
|
|
144
|
+
function setWaveform(voice: utinyint, waveform: utinyint): void {
|
|
145
|
+
control[voice] = (control[voice] & 0x01) | (waveform & 0xF6);
|
|
146
|
+
sidRegisters[VOICE_BASE[voice] + 4] = control[voice];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Gate on: the envelope starts its attack, at whatever pitch is set.
|
|
150
|
+
function gateOn(voice: utinyint): void {
|
|
151
|
+
control[voice] = control[voice] | 0x01;
|
|
152
|
+
sidRegisters[VOICE_BASE[voice] + 4] = control[voice];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Gate off: the envelope enters its release phase.
|
|
156
|
+
function release(voice: utinyint): void {
|
|
157
|
+
control[voice] = control[voice] & 0xFE;
|
|
158
|
+
sidRegisters[VOICE_BASE[voice] + 4] = control[voice];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The `Fn` for `note` at the current region's clock.
|
|
162
|
+
function frequencyOf(note: utinyint): usmallint {
|
|
163
|
+
if (region == Region.NTSC) {
|
|
164
|
+
return NOTE_NTSC[note];
|
|
165
|
+
}
|
|
166
|
+
return NOTE_PAL[note];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Set the pitch to `note` (a `Note.*` index) and gate on.
|
|
170
|
+
function play(voice: utinyint, note: utinyint): void {
|
|
171
|
+
sid.setFrequency(voice, sid.frequencyOf(note));
|
|
172
|
+
sid.gateOn(voice);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Which clock the notes are computed for: Region.PAL or Region.NTSC.
|
|
176
|
+
function setRegion(which: utinyint): void {
|
|
177
|
+
region = which;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Find the region from the raster (two frames) and use it. Returns it.
|
|
181
|
+
function detectRegion(): utinyint {
|
|
182
|
+
region = detectRegion();
|
|
183
|
+
return region;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The filter: an 11-bit cutoff, resonance 0-15, which voices go
|
|
187
|
+
// through it (bits 0-2, one per voice), and the mode. Mode bits share
|
|
188
|
+
// $D418 with the volume; both copies are kept here.
|
|
189
|
+
function setFilter(cutoff: usmallint, resonance: utinyint, voices: utinyint, mode: utinyint): void {
|
|
190
|
+
sidRegisters[21] = cutoff & 0x07;
|
|
191
|
+
sidRegisters[22] = (cutoff >> 3) & 0xFF;
|
|
192
|
+
filterControl = (resonance << 4) | (voices & 0x07);
|
|
193
|
+
sidRegisters[23] = filterControl;
|
|
194
|
+
modeVolume = (modeVolume & 0x0F) | (mode & 0x70);
|
|
195
|
+
sidRegisters[24] = modeVolume;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Every voice gated off with its waveform cleared, the filter off,
|
|
199
|
+
// volume 0: the state a program should leave the chip in, and a good
|
|
200
|
+
// one to start from.
|
|
201
|
+
function reset(): void {
|
|
202
|
+
for (let voice: utinyint = 0; voice < 3; voice++) {
|
|
203
|
+
control[voice] = 0;
|
|
204
|
+
sidRegisters[VOICE_BASE[voice] + 4] = 0;
|
|
205
|
+
}
|
|
206
|
+
filterControl = 0;
|
|
207
|
+
sidRegisters[23] = 0;
|
|
208
|
+
modeVolume = 0;
|
|
209
|
+
sidRegisters[24] = 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/sprites.8bs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// @8bitscript/c64/sprites — the VIC-II's eight hardware sprites.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./sprites"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { sprites } from "@8bitscript/c64/sprites";
|
|
6
|
+
// import { spriteShapes } from "@8bitscript/c64/video";
|
|
7
|
+
//
|
|
8
|
+
// This is hardware-level, C64-only surface — the layer a portable sprite
|
|
9
|
+
// capability (the `sprites.place(...)` intent API the root AGENTS.md
|
|
10
|
+
// describes) will sit on, the way ./text.8bs is the layer under
|
|
11
|
+
// @8bitscript/text. Importing it makes a program C64-specific, and it
|
|
12
|
+
// promises what the VIC-II has: eight movable objects, no more, each 24x21
|
|
13
|
+
// pixels of one colour over a transparent background (or 12x21 double-
|
|
14
|
+
// width pixels of three colours in multicolour), each drawn in front of
|
|
15
|
+
// or behind the background by its own bit, and in a fixed order among
|
|
16
|
+
// themselves — sprite 0 is always in front of sprite 1.
|
|
17
|
+
//
|
|
18
|
+
// ---- shapes -----------------------------------------------------------------
|
|
19
|
+
//
|
|
20
|
+
// A sprite's picture is 63 bytes — 21 rows of 3 bytes, one bit per pixel
|
|
21
|
+
// left to right — in a 64-byte block inside the VIC's bank, and the
|
|
22
|
+
// sprite's pointer (screen + $3F8 + n, `spritePointers[n]`) names the block
|
|
23
|
+
// as its address / 64. The blocks this package owns are $E400-$FFBF,
|
|
24
|
+
// blocks 144-254 in bank 3 (./geometry.8bs says why the picture is there),
|
|
25
|
+
// reachable as one array: `spriteShapes[64 * (block - FIRST_BLOCK) + i]`.
|
|
26
|
+
// Shape data is a program's own `const` table, and a const array is data
|
|
27
|
+
// wherever the linker put it — which the VIC cannot see — so a program
|
|
28
|
+
// copies each shape into a block once, at start-up:
|
|
29
|
+
//
|
|
30
|
+
// const SHIP: array<u8, 63> = [ ... ];
|
|
31
|
+
// const SHIP_BLOCK: u8 = sprites.FIRST_BLOCK;
|
|
32
|
+
// for (let i: u8 = 0; i < 63; i++) {
|
|
33
|
+
// spriteShapes[sprites.blockOffset(SHIP_BLOCK) + i] = SHIP[i];
|
|
34
|
+
// }
|
|
35
|
+
// sprites.setShape(0, SHIP_BLOCK);
|
|
36
|
+
//
|
|
37
|
+
// and from then on `setShape` is one byte, which is how animation works:
|
|
38
|
+
// several blocks, one pointer write a frame.
|
|
39
|
+
//
|
|
40
|
+
// ---- positions --------------------------------------------------------------
|
|
41
|
+
//
|
|
42
|
+
// Coordinates are the VIC's: X is 9 bits (0-511) and Y 8 bits, in a space
|
|
43
|
+
// where the visible 320x200 window starts at (24, 50) with 25 rows and 40
|
|
44
|
+
// columns on — so a sprite at X = 24, Y = 50 has its top-left pixel in the
|
|
45
|
+
// top-left corner of the screen, X < 24 or Y < 50 slides it under the
|
|
46
|
+
// border, and the last fully visible position is (320, 229). `place` takes
|
|
47
|
+
// the 9-bit X as one number and splits it across $D000 and $D010 itself.
|
|
48
|
+
//
|
|
49
|
+
// ---- collisions -------------------------------------------------------------
|
|
50
|
+
//
|
|
51
|
+
// The VIC records which sprites overlapped another sprite ($D01E) or a
|
|
52
|
+
// foreground pixel of the background ($D01F) as it drew the last frame,
|
|
53
|
+
// and CLEARS EACH REGISTER WHEN IT IS READ. So `collisions()` and
|
|
54
|
+
// `backgroundCollisions()` are read once, right after waitFrame(), and the
|
|
55
|
+
// byte is kept: a second read in the same frame is zero. A bit says a
|
|
56
|
+
// sprite touched something, not what; two set bits in $D01E were a pair.
|
|
57
|
+
import {
|
|
58
|
+
spritePositions, spriteXHigh, spriteEnable, spriteExpandX, spriteExpandY,
|
|
59
|
+
spritePriority, spriteMulticolor, spriteCollision, spriteBackgroundCollision,
|
|
60
|
+
spriteSharedColor0, spriteSharedColor1, spriteColors, setupVideo,
|
|
61
|
+
videoMode, VideoMode, writeUnderIo,
|
|
62
|
+
} from "./index.8bs";
|
|
63
|
+
import { Video, Bitmap, spritePointers, spriteShapes } from "./geometry.8bs";
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// Bit masks, indexed rather than shifted: `1 << n` with a variable n is a
|
|
67
|
+
// loop on the 6502, a table lookup is one indexed load.
|
|
68
|
+
const BIT: array<utinyint, 8> = [1, 2, 4, 8, 16, 32, 64, 128];
|
|
69
|
+
const NOT_BIT: array<utinyint, 8> = [254, 253, 251, 247, 239, 223, 191, 127];
|
|
70
|
+
|
|
71
|
+
export namespace sprites {
|
|
72
|
+
const COUNT: utinyint = 8;
|
|
73
|
+
const WIDTH: utinyint = 24; // pixels, before X expansion
|
|
74
|
+
const HEIGHT: utinyint = 21; // pixels, before Y expansion
|
|
75
|
+
const BYTES: utinyint = 63; // one shape: 21 rows of 3 bytes
|
|
76
|
+
const FIRST_BLOCK: utinyint = Video.SHAPE_BLOCK_FIRST; // 144: the first block this package owns
|
|
77
|
+
const BLOCK_COUNT: utinyint = Video.SHAPE_COUNT; // 111 blocks, 144-254
|
|
78
|
+
const LEFT: usmallint = 24; // X of the screen's left edge
|
|
79
|
+
const TOP: utinyint = 50; // Y of the screen's top edge
|
|
80
|
+
const VISIBLE_WIDTH: usmallint = 320;
|
|
81
|
+
const VISIBLE_HEIGHT: utinyint = 200;
|
|
82
|
+
|
|
83
|
+
// Where block `block`'s 63 bytes start in `spriteShapes`.
|
|
84
|
+
function blockOffset(block: utinyint): usmallint {
|
|
85
|
+
return (block - Video.SHAPE_BLOCK_FIRST) * 64;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Put sprite `index` at (x, y) — X 0-511, Y 0-255, the VIC's space.
|
|
89
|
+
function place(index: utinyint, x: usmallint, y: utinyint): void {
|
|
90
|
+
spritePositions[index * 2] = x;
|
|
91
|
+
spritePositions[index * 2 + 1] = y;
|
|
92
|
+
if (x >= 256) {
|
|
93
|
+
spriteXHigh = spriteXHigh | BIT[index];
|
|
94
|
+
} else {
|
|
95
|
+
spriteXHigh = spriteXHigh & NOT_BIT[index];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Draw sprite `index` from shape block `block` (FIRST_BLOCK and up; in
|
|
100
|
+
// bitmap mode, Bitmap.SHAPE_BLOCK_FIRST and up). The pointer is at
|
|
101
|
+
// screen + $3F8 wherever the screen matrix is: $E3F8 in text mode,
|
|
102
|
+
// $DFF8 under the I/O area in bitmap mode.
|
|
103
|
+
function setShape(index: utinyint, block: utinyint): void {
|
|
104
|
+
if (videoMode == VideoMode.BITMAP) {
|
|
105
|
+
writeUnderIo(Bitmap.SPRITE_POINTERS + index, block);
|
|
106
|
+
} else {
|
|
107
|
+
spritePointers[index] = block;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Byte `i` (0-62) of shape block `block`, wherever the block is:
|
|
112
|
+
// blocks 144-254 are `spriteShapes` in plain RAM, blocks 96-111 are
|
|
113
|
+
// under the I/O area (for sprites over a bitmap) and each byte is its
|
|
114
|
+
// own window there. A shape copy loop can use this for either. Any
|
|
115
|
+
// other block is refused: below 64 is the program's own RAM, 64-95
|
|
116
|
+
// the upper-case character set, 112-143 the text screen (or the
|
|
117
|
+
// bitmap's colour matrix), 255 the CPU's vectors.
|
|
118
|
+
function setShapeByte(block: utinyint, i: utinyint, value: utinyint): void {
|
|
119
|
+
if (block >= Video.SHAPE_BLOCK_FIRST && block < Video.SHAPE_BLOCK_FIRST + Video.SHAPE_COUNT) {
|
|
120
|
+
spriteShapes[sprites.blockOffset(block) + i] = value;
|
|
121
|
+
} else if (block >= Bitmap.SHAPE_BLOCK_FIRST && block < Bitmap.SHAPE_BLOCK_FIRST + Bitmap.SHAPE_COUNT) {
|
|
122
|
+
writeUnderIo(Video.BANK + block * 64 + i, value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setColor(index: utinyint, color: utinyint): void {
|
|
127
|
+
spriteColors[index] = color & 15;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The sprite is drawn from the next frame on. Sets the picture up
|
|
131
|
+
// (bank 3, the character set) if nothing has yet.
|
|
132
|
+
function show(index: utinyint): void {
|
|
133
|
+
setupVideo();
|
|
134
|
+
spriteEnable = spriteEnable | BIT[index];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hide(index: utinyint): void {
|
|
138
|
+
spriteEnable = spriteEnable & NOT_BIT[index];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function hideAll(): void {
|
|
142
|
+
spriteEnable = 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Twice as wide and/or twice as tall, from the same 24x21 shape.
|
|
146
|
+
function expand(index: utinyint, wide: bool, tall: bool): void {
|
|
147
|
+
if (wide) {
|
|
148
|
+
spriteExpandX = spriteExpandX | BIT[index];
|
|
149
|
+
} else {
|
|
150
|
+
spriteExpandX = spriteExpandX & NOT_BIT[index];
|
|
151
|
+
}
|
|
152
|
+
if (tall) {
|
|
153
|
+
spriteExpandY = spriteExpandY | BIT[index];
|
|
154
|
+
} else {
|
|
155
|
+
spriteExpandY = spriteExpandY & NOT_BIT[index];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Behind the background's foreground pixels (text, a bitmap's set
|
|
160
|
+
// bits) rather than in front of them. Background colour always shows
|
|
161
|
+
// through a sprite's transparent pixels either way.
|
|
162
|
+
function setBehindBackground(index: utinyint, behind: bool): void {
|
|
163
|
+
if (behind) {
|
|
164
|
+
spritePriority = spritePriority | BIT[index];
|
|
165
|
+
} else {
|
|
166
|
+
spritePriority = spritePriority & NOT_BIT[index];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Multicolour: pixel pairs, %01 shared colour 0, %10 the sprite's own
|
|
171
|
+
// colour, %11 shared colour 1, %00 transparent. Half the horizontal
|
|
172
|
+
// resolution for two more colours; the shared pair is one for all.
|
|
173
|
+
function setMulticolor(index: utinyint, on: bool): void {
|
|
174
|
+
if (on) {
|
|
175
|
+
spriteMulticolor = spriteMulticolor | BIT[index];
|
|
176
|
+
} else {
|
|
177
|
+
spriteMulticolor = spriteMulticolor & NOT_BIT[index];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function setSharedColors(color0: utinyint, color1: utinyint): void {
|
|
182
|
+
spriteSharedColor0 = color0 & 15;
|
|
183
|
+
spriteSharedColor1 = color1 & 15;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Sprites that overlapped another sprite while the last frame was
|
|
187
|
+
// drawn, one bit each. Clears when read: once a frame, keep the byte.
|
|
188
|
+
function collisions(): utinyint {
|
|
189
|
+
return spriteCollision;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Sprites that overlapped a foreground pixel of the background while
|
|
193
|
+
// the last frame was drawn. Clears when read, the same way.
|
|
194
|
+
function backgroundCollisions(): utinyint {
|
|
195
|
+
return spriteBackgroundCollision;
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/text.8bs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// @8bitscript/c64/text — the C64's implementation of @8bitscript/text.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./text"] in this package's package.json,
|
|
4
|
+
// and what
|
|
5
|
+
//
|
|
6
|
+
// import { text } from "@8bitscript/text";
|
|
7
|
+
//
|
|
8
|
+
// resolves to when the build is for the C64. Built on the registers
|
|
9
|
+
// @8bitscript/c64 exports. Every machine's text.8bs exports this same
|
|
10
|
+
// namespace — the same names, the same shapes, ASCII codes, and a cell 0
|
|
11
|
+
// at the top-left corner inside the border — so a program that imports it
|
|
12
|
+
// from @8bitscript/text draws the same thing on every target.
|
|
13
|
+
import { memoryPointer, setupVideo, videoMode, VideoMode } from "./index.8bs";
|
|
14
|
+
import { Video, screenRam, colorRam } from "./geometry.8bs";
|
|
15
|
+
|
|
16
|
+
// The screen is addressed by flat cell index — a position is
|
|
17
|
+
// `y * text.COLUMNS + x` — and these are the plainest things a text API
|
|
18
|
+
// can be built from: poke one cell's character code, poke one cell's
|
|
19
|
+
// colour, and how many cells the whole screen has. Whether a caller uses
|
|
20
|
+
// these to clear the screen, lay out a label, or nothing at all is entirely
|
|
21
|
+
// up to the caller — this package draws nothing on its own. Where the
|
|
22
|
+
// 40x25 screen is (VIC bank 3, $E000 — ./geometry.8bs says why it is not
|
|
23
|
+
// the KERNAL's $0400) reaches this file as `Video`; colour RAM is the
|
|
24
|
+
// fixed 1K of nybbles at $D800 wherever the screen is.
|
|
25
|
+
//
|
|
26
|
+
// ---- character codes: ASCII in, screen codes out ----------------------------
|
|
27
|
+
//
|
|
28
|
+
// `text.putChar` takes ASCII on every machine — space (32), '0'-'9'
|
|
29
|
+
// (48-57), 'A'-'Z' (65-90) and the punctuation `! , - . : ?` — upper case
|
|
30
|
+
// only: that is the portable set, the characters every target's character
|
|
31
|
+
// set has (the NES ships its own font, and that is what it has). The
|
|
32
|
+
// hardware wants screen codes: in every Commodore character ROM, codes
|
|
33
|
+
// 32-63 are those same ASCII values (space, digits, that punctuation), and
|
|
34
|
+
// 'A'-'Z' are 1-26, so asciiToScreenCode() moves 64-95 down by 64 and
|
|
35
|
+
// leaves 32-63 alone. Whether 1-26 then LOOK like 'A'-'Z' or 'a'-'z' is
|
|
36
|
+
// the character set's choice — the ROM's boot state, whatever a program
|
|
37
|
+
// ran before, or a user's SHIFT+C= — so this package owns the choice
|
|
38
|
+
// rather than inheriting one: prepare() selects the upper-case set, every
|
|
39
|
+
// run in text mode (in bitmap mode $D018 is the bitmap's, and left alone),
|
|
40
|
+
// through the VIC-II's memory pointer (see @8bitscript/c64's
|
|
41
|
+
// `memoryPointer`): one register write, of the value that also names this
|
|
42
|
+
// screen's position in the bank, and "TICK" reads as TICK here the way it
|
|
43
|
+
// does on the NES, the Atari, and the X16, none of which have a lower-case
|
|
44
|
+
// set to fall into. (LLVM-MOS's libc used to flip every Commodore machine
|
|
45
|
+
// to lower-case before main() with a hidden CHR$(14); packages/backend-6502
|
|
46
|
+
// keeps that out of the link — see its commodoreCharsetGuard().)
|
|
47
|
+
|
|
48
|
+
function asciiToScreenCode(code: utinyint): utinyint {
|
|
49
|
+
if (code >= 64 && code < 96) {
|
|
50
|
+
return code - 64;
|
|
51
|
+
}
|
|
52
|
+
return code;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- print: strings and number fields ------------------------------------
|
|
56
|
+
//
|
|
57
|
+
// `text.print(cell, s)` writes a string's characters into consecutive
|
|
58
|
+
// cells from `cell`, and `text.printNumber(cell, value, width)` writes
|
|
59
|
+
// `value` as exactly `width` decimal digits, zero-padded and right-aligned,
|
|
60
|
+
// so a field on a HUD never shifts columns. Both draw in the current colour —
|
|
61
|
+
// white until `text.setColor(TextColor.CYAN)` changes it, and that one
|
|
62
|
+
// call then colours everything printed after it, on the machines that have
|
|
63
|
+
// per-cell colour. They are also the two
|
|
64
|
+
// functions the compiler's template layout targets: `text.print(0,
|
|
65
|
+
// \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
|
|
66
|
+
// (see packages/compiler/src/ir).
|
|
67
|
+
//
|
|
68
|
+
// Under them: `place()` puts one character at one cell in the current
|
|
69
|
+
// colour, and is all a run of text costs per character, with `prepare()`
|
|
70
|
+
// making sure the picture is set up and selecting the upper-case set once
|
|
71
|
+
// for the run. `text.putChar` and `text.putColor` stay the one-cell pokes
|
|
72
|
+
// a caller can build anything from.
|
|
73
|
+
let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
|
|
74
|
+
let currentReverse: bool = false; // until text.setReverse() says otherwise
|
|
75
|
+
|
|
76
|
+
// Reverse video is bit 7 of the screen code: 128-255 are the ROM's
|
|
77
|
+
// inverted copies of 0-127. A reverse space is a solid block in the
|
|
78
|
+
// cell's colour, which is how a selected menu item becomes a filled bar.
|
|
79
|
+
function toScreen(code: utinyint): utinyint {
|
|
80
|
+
let screen: utinyint = asciiToScreenCode(code);
|
|
81
|
+
if (currentReverse) {
|
|
82
|
+
screen = screen + 128;
|
|
83
|
+
}
|
|
84
|
+
return screen;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// What a run of text needs once, before its first character.
|
|
88
|
+
function prepare(): void {
|
|
89
|
+
setupVideo();
|
|
90
|
+
if (videoMode == VideoMode.TEXT) {
|
|
91
|
+
memoryPointer = Video.MEMORY_POINTER_UPPERCASE;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function place(cell: usmallint, code: utinyint): void {
|
|
96
|
+
screenRam[cell] = toScreen(code);
|
|
97
|
+
colorRam[cell] = currentColor; // colour RAM is four bits wide; the VIC-II ignores the rest
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- digits -------------------------------------------------------------
|
|
101
|
+
//
|
|
102
|
+
// A number is written one place at a time, high to low, by subtracting the
|
|
103
|
+
// place value until it no longer fits: the 6502 has no divide instruction,
|
|
104
|
+
// and `value / 10` would link a 250-byte routine to do it. Places above the
|
|
105
|
+
// field are still taken off, so a field narrower than its number shows the
|
|
106
|
+
// low digits; places the number does not reach print as zeros.
|
|
107
|
+
const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
|
|
108
|
+
|
|
109
|
+
export namespace text {
|
|
110
|
+
const CELL_COUNT: usmallint = Video.CELL_COUNT; // 40 columns x 25 rows
|
|
111
|
+
const COLUMNS: utinyint = Video.COLUMNS; // cells per row, so cell = y * text.COLUMNS + x
|
|
112
|
+
|
|
113
|
+
function putChar(cell: usmallint, code: utinyint): void {
|
|
114
|
+
prepare();
|
|
115
|
+
screenRam[cell] = toScreen(code);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function putColor(cell: usmallint, color: utinyint): void {
|
|
119
|
+
colorRam[cell] = color;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setColor(color: utinyint): void {
|
|
123
|
+
currentColor = color;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Invert glyphs printed after this: the character pixels take the
|
|
127
|
+
// background colour and the rest of the cell takes the text colour.
|
|
128
|
+
function setReverse(on: bool): void {
|
|
129
|
+
currentReverse = on;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function print(cell: usmallint, s: string): void {
|
|
133
|
+
prepare();
|
|
134
|
+
for (let i: utinyint = 0; i < s.length; i++) {
|
|
135
|
+
place(cell + i, s[i]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
|
|
140
|
+
prepare();
|
|
141
|
+
let k: utinyint = width;
|
|
142
|
+
if (k < DIGIT_PLACES.length) {
|
|
143
|
+
k = DIGIT_PLACES.length;
|
|
144
|
+
}
|
|
145
|
+
while (k > 0) {
|
|
146
|
+
let digit: utinyint = 48; // '0'
|
|
147
|
+
if (k <= DIGIT_PLACES.length) {
|
|
148
|
+
while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
|
|
149
|
+
value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
|
|
150
|
+
digit++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (k <= width) {
|
|
154
|
+
place(cell + width - k, digit);
|
|
155
|
+
}
|
|
156
|
+
k--;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The colours `text.setColor()` takes: the eight names every machine
|
|
162
|
+
// shares, with this machine's colour-RAM values. Colour RAM takes all
|
|
163
|
+
// sixteen VIC-II colours; the other eight are numbers 8-15, the ones
|
|
164
|
+
// `BorderColor`/`BackgroundColor` in ./screen.8bs name.
|
|
165
|
+
export namespace TextColor {
|
|
166
|
+
const BLACK: utinyint = 0;
|
|
167
|
+
const WHITE: utinyint = 1;
|
|
168
|
+
const RED: utinyint = 2;
|
|
169
|
+
const CYAN: utinyint = 3;
|
|
170
|
+
const PURPLE: utinyint = 4;
|
|
171
|
+
const GREEN: utinyint = 5;
|
|
172
|
+
const BLUE: utinyint = 6;
|
|
173
|
+
const YELLOW: utinyint = 7;
|
|
174
|
+
}
|