@8bitscript/atari8 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/package.json +658 -0
- package/src/banks.8bs +124 -0
- package/src/console.8bs +85 -0
- package/src/index.8bs +150 -0
- package/src/input.8bs +178 -0
- package/src/joystick.8bs +147 -0
- package/src/keyboard.8bs +157 -0
- package/src/keys.8bs +116 -0
- package/src/pointer.8bs +39 -0
- package/src/pokey.8bs +225 -0
- package/src/random.8bs +59 -0
- package/src/screen.8bs +129 -0
- package/src/text.8bs +159 -0
package/src/pokey.8bs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// @8bitscript/atari8/pokey — the four voices, one register pair each.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./pokey"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { pokey, Distortion, Note } from "@8bitscript/atari8/pokey";
|
|
6
|
+
//
|
|
7
|
+
// pokey.reset();
|
|
8
|
+
// pokey.setDistortion(0, Distortion.TONE);
|
|
9
|
+
// pokey.setVolume(0, 8);
|
|
10
|
+
// pokey.play(0, Note.A4);
|
|
11
|
+
//
|
|
12
|
+
// Hardware-level, Atari-only surface, the same shape as @8bitscript/c64/sid:
|
|
13
|
+
// a voice count, `setVolume`/`play`/`silence`/`reset`, a `Note` namespace
|
|
14
|
+
// indexed the same way (C0 = 0, A4 = 57) and a per-region divider table, so
|
|
15
|
+
// a tune written against one machine's layer transposes to the other by
|
|
16
|
+
// changing the import. What POKEY has instead of the SID's envelopes and
|
|
17
|
+
// filter is six kinds of noise, and this file does not pretend otherwise.
|
|
18
|
+
//
|
|
19
|
+
// ---- what a voice is -------------------------------------------------------
|
|
20
|
+
//
|
|
21
|
+
// Four channels at $D200-$D207, a pair of registers each: AUDF (the divider)
|
|
22
|
+
// and AUDC (four bits of volume in the low nybble, three bits of distortion
|
|
23
|
+
// in the high). There is no envelope generator — an Atari envelope is the
|
|
24
|
+
// program writing AUDC every frame — no filter beyond two high-pass links,
|
|
25
|
+
// and no waveform choice: a "waveform" here is which polynomial counter
|
|
26
|
+
// gates the square wave, which is why five of the six settings are flavours
|
|
27
|
+
// of noise and only `Distortion.TONE` is a clean pitch.
|
|
28
|
+
//
|
|
29
|
+
// ---- the pitch you can actually ask for ------------------------------------
|
|
30
|
+
//
|
|
31
|
+
// AUDF is eight bits and the default clock is the 64 kHz one, so a voice
|
|
32
|
+
// runs at `clock / (2 * (AUDF + 1))` — about 125 Hz at AUDF 255, and coarser
|
|
33
|
+
// and coarser going up. That is a real ceiling and a real floor, and this
|
|
34
|
+
// file exposes both rather than rounding them away:
|
|
35
|
+
//
|
|
36
|
+
// - Octaves 0 to 2 do not exist on this clock. `frequencyOf()` answers 255
|
|
37
|
+
// for them, the lowest pitch the machine has (~125 Hz, near B2), and
|
|
38
|
+
// `play()` sounds that. A program that needs a bass line uses the 15 kHz
|
|
39
|
+
// clock (`Audctl.CLOCK_15KHZ`, which divides every voice by about four)
|
|
40
|
+
// or joins two channels into one 16-bit divider (`Audctl.JOIN_1_2`),
|
|
41
|
+
// and drives AUDF itself with `setDivider()`.
|
|
42
|
+
// - Near the top the divider runs out of resolution: on NTSC, A6 comes out
|
|
43
|
+
// at 1775.6 Hz against 1760 — about 15 cents sharp — and it gets worse
|
|
44
|
+
// above that. The tables stop at B6 for that reason.
|
|
45
|
+
//
|
|
46
|
+
// The tables are computed from POKEY's own clock, which is the machine clock
|
|
47
|
+
// divided by 28: 1789790/28 = 63921 Hz on NTSC, 1773447/28 = 63337 Hz on PAL
|
|
48
|
+
// (the machine clocks are the nominal ones packages/backend-6502's
|
|
49
|
+
// FRAME_SYNC.atari8 uses; see ../AGENTS.md on which figure is which). The
|
|
50
|
+
// `clock / (2 * (AUDF + 1))` relation is the standard published one and is
|
|
51
|
+
// derived here, not measured on screen — *to verify* against a tone before
|
|
52
|
+
// anything depends on the exact cents.
|
|
53
|
+
//
|
|
54
|
+
// ---- what this file does not touch -----------------------------------------
|
|
55
|
+
//
|
|
56
|
+
// SKCTL ($D20F) must have its two low bits set for POKEY to run its keyboard
|
|
57
|
+
// scan and debounce, and the OS leaves it that way ($03) — writing SKCTL is
|
|
58
|
+
// how a program breaks its own keyboard, so nothing here writes it.
|
|
59
|
+
// $D209 is KBCODE reading and STIMER writing, and $D20F is SKSTAT reading
|
|
60
|
+
// and SKCTL writing: same address, different register, which is why
|
|
61
|
+
// ./index.8bs declares each of them twice. The RANDOM register shares the
|
|
62
|
+
// chip and is deliberately not here — it is @8bitscript/atari8/random, a
|
|
63
|
+
// separate import, because hardware entropy is never something a program
|
|
64
|
+
// gets by accident.
|
|
65
|
+
import { audctl, palRegister } from "./index.8bs";
|
|
66
|
+
|
|
67
|
+
// AUDF1 at $D200; each voice is two bytes on from the last.
|
|
68
|
+
const AUDIO_BASE: usmallint = 0xD200;
|
|
69
|
+
|
|
70
|
+
// AUDC's high three bits: which polynomial counter gates the square wave.
|
|
71
|
+
// Only TONE is a clean pitch; the rest are the noises Atari games are made
|
|
72
|
+
// of. VOLUME_ONLY is the sample-playback bit — the voice stops oscillating
|
|
73
|
+
// and the low nybble drives the speaker level directly, which is how a
|
|
74
|
+
// program plays digitised sound by writing it fast enough.
|
|
75
|
+
export namespace Distortion {
|
|
76
|
+
const POLY_5_17: utinyint = 0x00; // 5-bit then 17-bit: the roughest noise
|
|
77
|
+
const POLY_5: utinyint = 0x20; // 5-bit only: a buzzy, pitched rasp
|
|
78
|
+
const POLY_5_4: utinyint = 0x40; // 5-bit then 4-bit
|
|
79
|
+
const POLY_17: utinyint = 0x80; // 17-bit: the smoothest noise, near white
|
|
80
|
+
const TONE: utinyint = 0xA0; // pure square wave — the only clean pitch
|
|
81
|
+
const POLY_4: utinyint = 0xC0; // 4-bit: a short, metallic loop
|
|
82
|
+
const VOLUME_ONLY: utinyint = 0x10; // OR this on: no oscillator, volume is the output
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// AUDCTL ($D208) bits, for `pokey.setAudctl()`. OR together what is wanted;
|
|
86
|
+
// the register is global, not per voice.
|
|
87
|
+
export namespace Audctl {
|
|
88
|
+
const CLOCK_15KHZ: utinyint = 0x01; // every voice's base clock becomes ~15.7 kHz
|
|
89
|
+
const HIGHPASS_2_4: utinyint = 0x02; // filter voice 2 by voice 4
|
|
90
|
+
const HIGHPASS_1_3: utinyint = 0x04; // filter voice 1 by voice 3
|
|
91
|
+
const JOIN_3_4: utinyint = 0x08; // voices 3+4 as one 16-bit divider
|
|
92
|
+
const JOIN_1_2: utinyint = 0x10; // voices 1+2 as one 16-bit divider
|
|
93
|
+
const FAST_3: utinyint = 0x20; // clock voice 3 at 1.79 MHz
|
|
94
|
+
const FAST_1: utinyint = 0x40; // clock voice 1 at 1.79 MHz
|
|
95
|
+
const POLY_9BIT: utinyint = 0x80; // 9-bit instead of 17-bit poly (also changes RANDOM)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Which table `play()` reads.
|
|
99
|
+
export namespace Region {
|
|
100
|
+
const PAL: utinyint = 0;
|
|
101
|
+
const NTSC: utinyint = 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Note indexes: octave * 12 + semitone, C0 = 0, A4 = 57 — the same numbering
|
|
105
|
+
// as @8bitscript/c64/sid, so a melody is the same list of names on both.
|
|
106
|
+
// CS is C sharp, and so on.
|
|
107
|
+
export namespace Note {
|
|
108
|
+
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;
|
|
109
|
+
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;
|
|
110
|
+
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;
|
|
111
|
+
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;
|
|
112
|
+
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;
|
|
113
|
+
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;
|
|
114
|
+
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;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// AUDF per note for POKEY's 63921 Hz clock (NTSC): round(clock / (2 * f)) - 1,
|
|
118
|
+
// clamped to the 8-bit register. Octaves 0-2 are all 255 — see the header:
|
|
119
|
+
// they are below what this clock reaches, not a table this file got wrong.
|
|
120
|
+
const NOTE_NTSC: array<utinyint, 84> = [
|
|
121
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // octave 0
|
|
122
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // octave 1
|
|
123
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // octave 2
|
|
124
|
+
243, 230, 217, 204, 193, 182, 172, 162, 153, 144, 136, 128, // octave 3
|
|
125
|
+
121, 114, 108, 102, 96, 91, 85, 81, 76, 72, 68, 64, // octave 4
|
|
126
|
+
60, 57, 53, 50, 47, 45, 42, 40, 37, 35, 33, 31, // octave 5
|
|
127
|
+
30, 28, 26, 25, 23, 22, 21, 19, 18, 17, 16, 15, // octave 6
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// The same for PAL's 63337 Hz clock.
|
|
131
|
+
const NOTE_PAL: array<utinyint, 84> = [
|
|
132
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // octave 0
|
|
133
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // octave 1
|
|
134
|
+
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // octave 2
|
|
135
|
+
241, 228, 215, 203, 191, 180, 170, 161, 152, 143, 135, 127, // octave 3
|
|
136
|
+
120, 113, 107, 101, 95, 90, 85, 80, 75, 71, 67, 63, // octave 4
|
|
137
|
+
60, 56, 53, 50, 47, 44, 42, 39, 37, 35, 33, 31, // octave 5
|
|
138
|
+
29, 28, 26, 24, 23, 22, 20, 19, 18, 17, 16, 15, // octave 6
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// Which table `play()` reads: PAL until `setRegion()` or `detectRegion()`
|
|
142
|
+
// says otherwise, matching @8bitscript/c64/sid's default.
|
|
143
|
+
let region: utinyint = 0;
|
|
144
|
+
|
|
145
|
+
// The AUDC byte last written per voice, since the register cannot be read
|
|
146
|
+
// back: volume and distortion are set separately and each needs the other.
|
|
147
|
+
let control: array<utinyint, 4>;
|
|
148
|
+
|
|
149
|
+
export namespace pokey {
|
|
150
|
+
const VOICE_COUNT: utinyint = 4;
|
|
151
|
+
|
|
152
|
+
// Every voice silent, every divider zero, AUDCTL cleared to the plain
|
|
153
|
+
// 64 kHz per-voice clock. Call it before the first note; a program that
|
|
154
|
+
// does not is at the mercy of whatever the OS left behind.
|
|
155
|
+
function reset(): void {
|
|
156
|
+
audctl = 0;
|
|
157
|
+
for (let voice: utinyint = 0; voice < pokey.VOICE_COUNT; voice++) {
|
|
158
|
+
control[voice] = 0;
|
|
159
|
+
memory.write(AUDIO_BASE + voice * 2, 0);
|
|
160
|
+
memory.write(AUDIO_BASE + voice * 2 + 1, 0);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Volume 0-15 for one voice, keeping its distortion.
|
|
165
|
+
function setVolume(voice: utinyint, volume: utinyint): void {
|
|
166
|
+
control[voice] = (control[voice] & 0xF0) | (volume & 0x0F);
|
|
167
|
+
memory.write(AUDIO_BASE + voice * 2 + 1, control[voice]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// One of the `Distortion.*` settings for one voice, keeping its volume.
|
|
171
|
+
function setDistortion(voice: utinyint, distortion: utinyint): void {
|
|
172
|
+
control[voice] = (control[voice] & 0x0F) | (distortion & 0xF0);
|
|
173
|
+
memory.write(AUDIO_BASE + voice * 2 + 1, control[voice]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// AUDF straight through, for a program driving pitch itself — a 16-bit
|
|
177
|
+
// pair, the 15 kHz clock, a slide, a sound effect that is not a note.
|
|
178
|
+
function setDivider(voice: utinyint, divider: utinyint): void {
|
|
179
|
+
memory.write(AUDIO_BASE + voice * 2, divider);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The AUDF this region's table gives a `Note.*`, without playing it.
|
|
183
|
+
function frequencyOf(note: utinyint): utinyint {
|
|
184
|
+
if (region == Region.NTSC) {
|
|
185
|
+
return NOTE_NTSC[note];
|
|
186
|
+
}
|
|
187
|
+
return NOTE_PAL[note];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Sound a `Note.*` on a voice, at whatever volume and distortion the
|
|
191
|
+
// voice already has. `setDistortion(voice, Distortion.TONE)` first, or
|
|
192
|
+
// it plays as noise.
|
|
193
|
+
function play(voice: utinyint, note: utinyint): void {
|
|
194
|
+
memory.write(AUDIO_BASE + voice * 2, pokey.frequencyOf(note));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Volume to zero, leaving the divider and distortion alone.
|
|
198
|
+
function silence(voice: utinyint): void {
|
|
199
|
+
pokey.setVolume(voice, 0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// AUDCTL, from `Audctl.*` bits OR-ed together. Global to all four voices.
|
|
203
|
+
function setAudctl(bits: utinyint): void {
|
|
204
|
+
audctl = bits;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Which table `play()` reads: `Region.PAL` or `Region.NTSC`.
|
|
208
|
+
function setRegion(which: utinyint): void {
|
|
209
|
+
region = which;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Ask the machine and set the region from the answer. GTIA's PAL
|
|
213
|
+
// register ($D014) reports the TV standard in its low bits: a PAL
|
|
214
|
+
// machine answers 1, an NTSC one 14 or 15 (atari800 returns $01 and
|
|
215
|
+
// $0F). One binary therefore plays in tune on both, the same way
|
|
216
|
+
// waitFrame()'s pacing adapts at start-up. Returns what it chose.
|
|
217
|
+
function detectRegion(): utinyint {
|
|
218
|
+
if ((palRegister & 0x0E) == 0) {
|
|
219
|
+
region = Region.PAL;
|
|
220
|
+
} else {
|
|
221
|
+
region = Region.NTSC;
|
|
222
|
+
}
|
|
223
|
+
return region;
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/random.8bs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @8bitscript/atari8/random — POKEY's hardware entropy.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./random"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { random } from "@8bitscript/atari8/random";
|
|
6
|
+
//
|
|
7
|
+
// let seed: utinyint = random.byte();
|
|
8
|
+
//
|
|
9
|
+
// Its own import, and not part of ./pokey.8bs, on purpose. RANDOM ($D20A) is
|
|
10
|
+
// a *hardware* source: it reads the free-running 17-bit polynomial counter
|
|
11
|
+
// POKEY uses to make noise, sampled wherever the beam happens to be, so two
|
|
12
|
+
// runs of the same program never agree. That is exactly what a program wants
|
|
13
|
+
// for a seed and exactly what it does not want anywhere else — a game that
|
|
14
|
+
// reads it inside its logic cannot be replayed, cannot be tested against a
|
|
15
|
+
// recorded input, and cannot be debugged from a screenshot. The root
|
|
16
|
+
// AGENTS.md rule is that hardware entropy is never the deterministic PRNG's
|
|
17
|
+
// seed by default and never arrives without being asked for; this file is
|
|
18
|
+
// the asking.
|
|
19
|
+
//
|
|
20
|
+
// The usual shape: read one byte at start-up, seed a deterministic generator
|
|
21
|
+
// with it, and never touch this again.
|
|
22
|
+
//
|
|
23
|
+
// ---- what the number actually is -------------------------------------------
|
|
24
|
+
//
|
|
25
|
+
// The counter is 17 bits and runs continuously off the same clock the audio
|
|
26
|
+
// channels use, so consecutive reads a few cycles apart are consecutive
|
|
27
|
+
// states of one shift register, not independent samples: reading it in a
|
|
28
|
+
// tight loop gives a walk through the polynomial, not eight fresh bytes.
|
|
29
|
+
// `bytes()` is here for a program that wants more than eight bits and knows
|
|
30
|
+
// that; anything that needs many independent values wants a PRNG seeded once
|
|
31
|
+
// from here.
|
|
32
|
+
//
|
|
33
|
+
// AUDCTL's bit 7 (`Audctl.POLY_9BIT` in ./pokey.8bs) switches the counter to
|
|
34
|
+
// 9 bits, which shortens this to a 511-state cycle. A program that sets it
|
|
35
|
+
// for the sound it makes has also changed what this returns; read the seed
|
|
36
|
+
// before touching AUDCTL, or leave that bit alone.
|
|
37
|
+
//
|
|
38
|
+
// In atari800 the register is a table indexed by a scanline counter plus the
|
|
39
|
+
// current cycle (upstream `pokey.c`), so under the emulator it is
|
|
40
|
+
// reproducible in principle and unpredictable in practice — the same as on
|
|
41
|
+
// the machine, for a program's purposes, but not a source of cryptographic
|
|
42
|
+
// randomness on either. Nothing here is.
|
|
43
|
+
import { randomRegister } from "./index.8bs";
|
|
44
|
+
|
|
45
|
+
export namespace random {
|
|
46
|
+
// One byte from the counter, 0-255.
|
|
47
|
+
function byte(): utinyint {
|
|
48
|
+
return randomRegister;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Two bytes as one 16-bit value. See the header: the two reads are
|
|
52
|
+
// consecutive states of one shift register, close together in time, so
|
|
53
|
+
// this is wider than `byte()` but not twice as random.
|
|
54
|
+
function word(): usmallint {
|
|
55
|
+
let high: usmallint = randomRegister;
|
|
56
|
+
let low: usmallint = randomRegister;
|
|
57
|
+
return high * 256 + low;
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/screen.8bs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @8bitscript/atari8/screen — the Atari 8-bit's implementation of
|
|
2
|
+
// @8bitscript/screen.
|
|
3
|
+
//
|
|
4
|
+
// Named by "8bitscript".exports["./screen"] in this package's package.json,
|
|
5
|
+
// and what
|
|
6
|
+
//
|
|
7
|
+
// import { screen, BorderColor, BackgroundColor } from "@8bitscript/screen";
|
|
8
|
+
//
|
|
9
|
+
// resolves to when the build is for the Atari: @8bitscript/screen's entry is
|
|
10
|
+
// keyed by machine and delegates here. Built on the registers
|
|
11
|
+
// @8bitscript/atari8 itself exports, one layer up from them — see that file
|
|
12
|
+
// for which GTIA register is which, and why the OS shadows matter. Every
|
|
13
|
+
// machine's screen.8bs exports this same surface, which is what lets a
|
|
14
|
+
// program import it from @8bitscript/screen and never name the hardware.
|
|
15
|
+
import {
|
|
16
|
+
borderColor, borderColorShadow, backgroundColor, backgroundColorShadow,
|
|
17
|
+
textColor, textColorShadow, cursorInhibit,
|
|
18
|
+
} from "./index.8bs";
|
|
19
|
+
|
|
20
|
+
// ---- the screen: border and background --------------------------------------
|
|
21
|
+
//
|
|
22
|
+
// GTIA colours are 8-bit (4 bits hue, 4 bits luminance) but this package
|
|
23
|
+
// doesn't interpret them — a caller passes raw GTIA bytes, the same
|
|
24
|
+
// "portable values are whatever the hardware wants" contract memory.write
|
|
25
|
+
// already has everywhere else.
|
|
26
|
+
//
|
|
27
|
+
// Shadow first, then hardware, for two different reasons: the shadow so the
|
|
28
|
+
// colour survives the OS's next vertical blank (see @8bitscript/atari8 —
|
|
29
|
+
// without this, the colour lasts one frame), the hardware register so it
|
|
30
|
+
// appears now rather than at that next blank.
|
|
31
|
+
//
|
|
32
|
+
// COLPF1 is forced to $0E (max luminance, hue ignored in GR.0) so
|
|
33
|
+
// characters stay readable against any COLPF2 the caller just set — the
|
|
34
|
+
// Atari equivalent of the Commodore packages drawing digits in white.
|
|
35
|
+
// CRSINH hides the OS cursor so it cannot invert a cell of whatever the
|
|
36
|
+
// caller writes.
|
|
37
|
+
export namespace screen {
|
|
38
|
+
function setColors(border: u8, background: u8): void {
|
|
39
|
+
borderColorShadow = border;
|
|
40
|
+
borderColor = border;
|
|
41
|
+
backgroundColorShadow = background;
|
|
42
|
+
backgroundColor = background;
|
|
43
|
+
textColorShadow = 0x0E;
|
|
44
|
+
textColor = 0x0E;
|
|
45
|
+
cursorInhibit = 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// `blank(border, background)`: every cell blank and both colours set —
|
|
49
|
+
// black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
|
|
50
|
+
// to leave one as it is. `setBorder` and `setBackground` are one
|
|
51
|
+
// colour each, `setColors` the pair; `text.setColor` owns what the
|
|
52
|
+
// next print looks like.
|
|
53
|
+
function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
|
|
54
|
+
if (border != BorderColor.KEEP) {
|
|
55
|
+
screen.setBorder(border);
|
|
56
|
+
}
|
|
57
|
+
if (background != BackgroundColor.KEEP) {
|
|
58
|
+
screen.setBackground(background);
|
|
59
|
+
}
|
|
60
|
+
cursorInhibit = 1;
|
|
61
|
+
// Screen memory is wherever the OS put it (SAVMSC, $58/$59); 0 is
|
|
62
|
+
// the blank internal code, what putChar turns a space into.
|
|
63
|
+
let base: usmallint = memory.read(0x58) + memory.read(0x59) * 256;
|
|
64
|
+
for (let cell: usmallint = 0; cell < 960; cell++) {
|
|
65
|
+
memory.write(base + cell, 0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function setBackground(background: u8): void {
|
|
70
|
+
backgroundColorShadow = background;
|
|
71
|
+
backgroundColor = background;
|
|
72
|
+
textColorShadow = 0x0E;
|
|
73
|
+
textColor = 0x0E;
|
|
74
|
+
cursorInhibit = 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function setBorder(border: u8): void {
|
|
78
|
+
borderColorShadow = border;
|
|
79
|
+
borderColor = border;
|
|
80
|
+
textColorShadow = 0x0E;
|
|
81
|
+
textColor = 0x0E;
|
|
82
|
+
cursorInhibit = 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- colour names ----------------------------------------------------------
|
|
87
|
+
//
|
|
88
|
+
// The same eight names every machine's screen.8bs exports — Black, White,
|
|
89
|
+
// Red, Cyan, Purple, Green, Blue, Yellow — so a program can write
|
|
90
|
+
// `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` through
|
|
91
|
+
// @8bitscript/screen and get blue on every target. The values are GTIA
|
|
92
|
+
// bytes: hue in the high nibble, luminance in the low (even values only;
|
|
93
|
+
// bit 0 is unused). Hues follow the SETCOLOR hue table in the Atari BASIC
|
|
94
|
+
// Reference Manual — 0 grey, 1 gold, 3 red-orange, 5 purple, 8 blue, 10
|
|
95
|
+
// turquoise, 12 green — with luminances picked so that each reads as its
|
|
96
|
+
// name and stays darker than the $0E text luminance setColors() writes. A
|
|
97
|
+
// program that wants a colour not named here passes its own GTIA byte to
|
|
98
|
+
// setColors() directly. Not visually verified under atari800 by this
|
|
99
|
+
// project; the hue table is the manual's, the luminances a judgement.
|
|
100
|
+
|
|
101
|
+
export namespace BorderColor {
|
|
102
|
+
const BLACK: utinyint = 0x00;
|
|
103
|
+
const WHITE: utinyint = 0x0E;
|
|
104
|
+
const RED: utinyint = 0x34;
|
|
105
|
+
const CYAN: utinyint = 0xA8;
|
|
106
|
+
const PURPLE: utinyint = 0x58;
|
|
107
|
+
const GREEN: utinyint = 0xC6;
|
|
108
|
+
const BLUE: utinyint = 0x84;
|
|
109
|
+
const YELLOW: utinyint = 0x1C;
|
|
110
|
+
// Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
|
|
111
|
+
// a value no register here takes — except on this machine, where
|
|
112
|
+
// every byte is a colour: hue 15 at its brightest is given up for it.
|
|
113
|
+
const KEEP: utinyint = 255;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export namespace BackgroundColor {
|
|
117
|
+
const BLACK: utinyint = 0x00;
|
|
118
|
+
const WHITE: utinyint = 0x0E;
|
|
119
|
+
const RED: utinyint = 0x34;
|
|
120
|
+
const CYAN: utinyint = 0xA8;
|
|
121
|
+
const PURPLE: utinyint = 0x58;
|
|
122
|
+
const GREEN: utinyint = 0xC6;
|
|
123
|
+
const BLUE: utinyint = 0x84;
|
|
124
|
+
const YELLOW: utinyint = 0x1C;
|
|
125
|
+
// Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
|
|
126
|
+
// a value no register here takes — except on this machine, where
|
|
127
|
+
// every byte is a colour: hue 15 at its brightest is given up for it.
|
|
128
|
+
const KEEP: utinyint = 255;
|
|
129
|
+
}
|
package/src/text.8bs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @8bitscript/atari8/text — the Atari 8-bit's implementation of
|
|
2
|
+
// @8bitscript/text.
|
|
3
|
+
//
|
|
4
|
+
// Named by "8bitscript".exports["./text"] in this package's package.json,
|
|
5
|
+
// and what
|
|
6
|
+
//
|
|
7
|
+
// import { text } from "@8bitscript/text";
|
|
8
|
+
//
|
|
9
|
+
// resolves to when the build is for the Atari. Built on the registers
|
|
10
|
+
// @8bitscript/atari8 exports. Every machine's text.8bs exports this same
|
|
11
|
+
// namespace — the same names, the same shapes, ASCII codes, and a cell 0
|
|
12
|
+
// at the top-left corner inside the border — so a program that imports it
|
|
13
|
+
// from @8bitscript/text draws the same thing on every target.
|
|
14
|
+
import { cursorInhibit } from "./index.8bs";
|
|
15
|
+
|
|
16
|
+
// ANTIC mode 2 (GR.0) is 40x24 = 960 cells, pointed to by SAVMSC at $58
|
|
17
|
+
// (lo) / $59 (hi) — not a fixed address the way the Commodore machines have
|
|
18
|
+
// it: screen memory lives wherever the OS's display list points it, so
|
|
19
|
+
// putChar() goes through memory.read of those two bytes rather than a
|
|
20
|
+
// compile-time constant. Codes are ASCII, as they are on every machine
|
|
21
|
+
// (space, '0'-'9', 'A'-'Z' and a little punctuation, upper case only) —
|
|
22
|
+
// ATASCII agrees with ASCII across that whole range, so no translation is
|
|
23
|
+
// needed on the way in; putChar converts to the internal screen code ANTIC
|
|
24
|
+
// actually fetches (space $20 -> $00, digits and letters $20-$5F minus
|
|
25
|
+
// $20). There is no colour RAM: a character's luminance is the global
|
|
26
|
+
// COLPF1 that `screen.setColors()` writes, so putColor exists only so
|
|
27
|
+
// portable code still compiles and does nothing, the same way
|
|
28
|
+
// @8bitscript/pet's putColor does nothing. putChar also inhibits the OS
|
|
29
|
+
// cursor (see @8bitscript/atari8's `cursorInhibit`), so a program that
|
|
30
|
+
// draws before its first setColors() — which is the order every program
|
|
31
|
+
// should use, for the NES's sake — does not get its first cell inverted by
|
|
32
|
+
// the cursor sitting on it.
|
|
33
|
+
|
|
34
|
+
function atasciiToInternal(code: utinyint): utinyint {
|
|
35
|
+
if (code < 32) {
|
|
36
|
+
return code + 64;
|
|
37
|
+
}
|
|
38
|
+
if (code < 96) {
|
|
39
|
+
return code - 32;
|
|
40
|
+
}
|
|
41
|
+
return code;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- print: strings and number fields ------------------------------------
|
|
45
|
+
//
|
|
46
|
+
// `text.print(cell, s)` writes a string's characters into consecutive
|
|
47
|
+
// cells from `cell`, and `text.printNumber(cell, value, width)` writes
|
|
48
|
+
// `value` as exactly `width` decimal digits, zero-padded and right-aligned,
|
|
49
|
+
// so a field on a HUD never shifts columns. Both draw in the current colour —
|
|
50
|
+
// white until `text.setColor(TextColor.CYAN)` changes it, and that one
|
|
51
|
+
// call then colours everything printed after it, on the machines that have
|
|
52
|
+
// per-cell colour. They are also the two
|
|
53
|
+
// functions the compiler's template layout targets: `text.print(0,
|
|
54
|
+
// \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
|
|
55
|
+
// (see packages/compiler/src/ir).
|
|
56
|
+
//
|
|
57
|
+
// Under them: `place()` puts one character at one cell in the current
|
|
58
|
+
// colour, and is all a run of text costs per character, with `prepare()` hiding the
|
|
59
|
+
// cursor and reading where the OS put screen memory once for the run.
|
|
60
|
+
// `text.putChar` and `text.putColor` stay the one-cell pokes a caller can
|
|
61
|
+
// build anything from.
|
|
62
|
+
let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
|
|
63
|
+
let currentReverse: bool = false; // until text.setReverse() says otherwise
|
|
64
|
+
|
|
65
|
+
// Where the OS put screen memory (SAVMSC, $58/$59), read by prepare().
|
|
66
|
+
let screenBase: usmallint = 0;
|
|
67
|
+
|
|
68
|
+
// Reverse video is bit 7 of ANTIC's internal code. A reverse space is a
|
|
69
|
+
// solid block in COLPF1, which is how a selected menu item becomes a bar.
|
|
70
|
+
function toInternal(code: utinyint): utinyint {
|
|
71
|
+
let screen: utinyint = atasciiToInternal(code);
|
|
72
|
+
if (currentReverse) {
|
|
73
|
+
screen = screen + 128;
|
|
74
|
+
}
|
|
75
|
+
return screen;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// What a run of text needs once, before its first character.
|
|
79
|
+
function prepare(): void {
|
|
80
|
+
cursorInhibit = 1;
|
|
81
|
+
screenBase = memory.read(0x58) + memory.read(0x59) * 256;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function place(cell: usmallint, code: utinyint): void {
|
|
85
|
+
memory.write(screenBase + cell, toInternal(code));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- digits -------------------------------------------------------------
|
|
89
|
+
//
|
|
90
|
+
// A number is written one place at a time, high to low, by subtracting the
|
|
91
|
+
// place value until it no longer fits: the 6502 has no divide instruction,
|
|
92
|
+
// and `value / 10` would link a 250-byte routine to do it. Places above the
|
|
93
|
+
// field are still taken off, so a field narrower than its number shows the
|
|
94
|
+
// low digits; places the number does not reach print as zeros.
|
|
95
|
+
const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
|
|
96
|
+
|
|
97
|
+
export namespace text {
|
|
98
|
+
const CELL_COUNT: usmallint = 960; // 40 columns x 24 rows
|
|
99
|
+
const COLUMNS: utinyint = 40; // cells per row, so cell = y * text.COLUMNS + x
|
|
100
|
+
|
|
101
|
+
function putChar(cell: usmallint, code: utinyint): void {
|
|
102
|
+
prepare();
|
|
103
|
+
memory.write(screenBase + cell, toInternal(code));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function putColor(cell: usmallint, color: utinyint): void {
|
|
107
|
+
// Deliberately empty — GR.0 has no per-cell colour RAM.
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setColor(color: utinyint): void {
|
|
111
|
+
currentColor = color;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function setReverse(on: bool): void {
|
|
115
|
+
currentReverse = on;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function print(cell: usmallint, s: string): void {
|
|
119
|
+
prepare();
|
|
120
|
+
for (let i: utinyint = 0; i < s.length; i++) {
|
|
121
|
+
place(cell + i, s[i]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
|
|
126
|
+
prepare();
|
|
127
|
+
let k: utinyint = width;
|
|
128
|
+
if (k < DIGIT_PLACES.length) {
|
|
129
|
+
k = DIGIT_PLACES.length;
|
|
130
|
+
}
|
|
131
|
+
while (k > 0) {
|
|
132
|
+
let digit: utinyint = 48; // '0'
|
|
133
|
+
if (k <= DIGIT_PLACES.length) {
|
|
134
|
+
while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
|
|
135
|
+
value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
|
|
136
|
+
digit++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (k <= width) {
|
|
140
|
+
place(cell + width - k, digit);
|
|
141
|
+
}
|
|
142
|
+
k--;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The colours `text.setColor()` takes. GR.0 has no per-cell colour RAM, so the
|
|
148
|
+
// names are here for a program that compiles everywhere and their values
|
|
149
|
+
// go nowhere.
|
|
150
|
+
export namespace TextColor {
|
|
151
|
+
const BLACK: utinyint = 0;
|
|
152
|
+
const WHITE: utinyint = 1;
|
|
153
|
+
const RED: utinyint = 2;
|
|
154
|
+
const CYAN: utinyint = 3;
|
|
155
|
+
const PURPLE: utinyint = 4;
|
|
156
|
+
const GREEN: utinyint = 5;
|
|
157
|
+
const BLUE: utinyint = 6;
|
|
158
|
+
const YELLOW: utinyint = 7;
|
|
159
|
+
}
|