@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/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
+ }