@8bitscript/pet 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 8BitScript contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,244 @@
1
+ {
2
+ "name": "@8bitscript/pet",
3
+ "version": "0.1.0",
4
+ "description": "Commodore PET target support for 8BitScript: the hardware underneath the portable APIs.",
5
+ "license": "MIT",
6
+ "8bitscript": {
7
+ "entry": "./src/index.8bs",
8
+ "exports": {
9
+ "./screen": "./src/screen.8bs",
10
+ "./text": "./src/text.8bs",
11
+ "./keyboard": "./src/keyboard.8bs",
12
+ "./keys": "./src/keys.8bs",
13
+ "./blocks": "./src/blocks.8bs",
14
+ "./input": "./src/input.8bs",
15
+ "./pointer": "./src/pointer.8bs"
16
+ },
17
+ "hardware": {
18
+ "facts": {
19
+ "video.columns": 40,
20
+ "video.rows": 25,
21
+ "video.cellWidth": 8,
22
+ "video.cellHeight": 8,
23
+ "video.palette": 2,
24
+ "video.cellColors": 2,
25
+ "video.colorPerCell": false,
26
+ "video.glyphs": 0,
27
+ "video.blockWidth": 2,
28
+ "video.blockHeight": 2,
29
+ "video.bitmap": false,
30
+ "video.layers": 1,
31
+ "video.scroll": false,
32
+ "video.sprites": 0,
33
+ "video.spritesPerLine": 0,
34
+ "video.spriteWidth": 0,
35
+ "video.spriteHeight": 0,
36
+ "video.spriteColors": 0,
37
+ "video.frameRate": 60,
38
+ "audio.voices": 1,
39
+ "audio.noise": false,
40
+ "audio.envelope": false,
41
+ "audio.filter": false,
42
+ "audio.pcm": false,
43
+ "audio.volume": false,
44
+ "audio.entropy": false,
45
+ "input.keyboard": true,
46
+ "input.joysticks": 0,
47
+ "input.pads": 0,
48
+ "input.mouse": false,
49
+ "input.paddles": false,
50
+ "storage.save": true,
51
+ "memory.ram": 31743,
52
+ "memory.banked": false,
53
+ "memory.bankedKib": 0,
54
+ "storage.kib": 166
55
+ },
56
+ "options": {
57
+ "model": {
58
+ "label": "Model",
59
+ "default": "3032",
60
+ "values": {
61
+ "3008": {
62
+ "label": "3008: 40 columns, 8K",
63
+ "build": {
64
+ "defsym": {
65
+ "__ram_size": 8
66
+ }
67
+ },
68
+ "run": {
69
+ "xpet": [
70
+ "-model",
71
+ "3008"
72
+ ]
73
+ },
74
+ "facts": {
75
+ "video.columns": 40,
76
+ "memory.ram": 7167,
77
+ "video.frameRate": 60
78
+ }
79
+ },
80
+ "3016": {
81
+ "label": "3016: 40 columns, 16K",
82
+ "build": {
83
+ "defsym": {
84
+ "__ram_size": 16
85
+ }
86
+ },
87
+ "run": {
88
+ "xpet": [
89
+ "-model",
90
+ "3016"
91
+ ]
92
+ },
93
+ "facts": {
94
+ "video.columns": 40,
95
+ "memory.ram": 15359,
96
+ "video.frameRate": 60
97
+ }
98
+ },
99
+ "3032": {
100
+ "label": "3032: 40 columns, 32K, no CRTC, ~60 Hz",
101
+ "build": {
102
+ "defsym": {
103
+ "__ram_size": 32
104
+ }
105
+ },
106
+ "run": {
107
+ "xpet": [
108
+ "-model",
109
+ "3032"
110
+ ]
111
+ },
112
+ "facts": {
113
+ "video.columns": 40,
114
+ "memory.ram": 31743,
115
+ "video.frameRate": 60
116
+ }
117
+ },
118
+ "4016": {
119
+ "label": "4016: 40 columns, 16K, CRTC, 50 Hz",
120
+ "build": {
121
+ "defsym": {
122
+ "__ram_size": 16
123
+ }
124
+ },
125
+ "run": {
126
+ "xpet": [
127
+ "-model",
128
+ "4016"
129
+ ]
130
+ },
131
+ "facts": {
132
+ "video.columns": 40,
133
+ "memory.ram": 15359,
134
+ "video.frameRate": 50
135
+ }
136
+ },
137
+ "4032": {
138
+ "label": "4032: 40 columns, 32K, CRTC, 50 Hz",
139
+ "build": {
140
+ "defsym": {
141
+ "__ram_size": 32
142
+ }
143
+ },
144
+ "run": {
145
+ "xpet": [
146
+ "-model",
147
+ "4032"
148
+ ]
149
+ },
150
+ "facts": {
151
+ "video.columns": 40,
152
+ "memory.ram": 31743,
153
+ "video.frameRate": 50
154
+ }
155
+ },
156
+ "8032": {
157
+ "label": "8032: 80 columns, 32K, business keyboard, 50 Hz",
158
+ "build": {
159
+ "defsym": {
160
+ "__ram_size": 32
161
+ }
162
+ },
163
+ "run": {
164
+ "xpet": [
165
+ "-model",
166
+ "8032"
167
+ ]
168
+ },
169
+ "facts": {
170
+ "video.columns": 80,
171
+ "memory.ram": 31743,
172
+ "video.frameRate": 50
173
+ }
174
+ }
175
+ }
176
+ },
177
+ "drive": {
178
+ "label": "Disk drive",
179
+ "default": "4040",
180
+ "values": {
181
+ "4040": {
182
+ "label": "2040/4040 (170K disk)",
183
+ "facts": {
184
+ "storage.save": true,
185
+ "storage.kib": 166
186
+ },
187
+ "tag": null
188
+ },
189
+ "8050": {
190
+ "label": "8050 (520K disk)",
191
+ "facts": {
192
+ "storage.save": true,
193
+ "storage.kib": 508
194
+ },
195
+ "tag": null
196
+ },
197
+ "8250": {
198
+ "label": "8250 (1M disk)",
199
+ "facts": {
200
+ "storage.save": true,
201
+ "storage.kib": 1025
202
+ },
203
+ "tag": null
204
+ },
205
+ "none": {
206
+ "label": "No drive (tape or nothing)",
207
+ "facts": {
208
+ "storage.save": false,
209
+ "storage.kib": 0
210
+ },
211
+ "tag": null
212
+ }
213
+ }
214
+ }
215
+ },
216
+ "presets": {
217
+ "3008": {
218
+ "model": "3008"
219
+ },
220
+ "3016": {
221
+ "model": "3016"
222
+ },
223
+ "3032": {
224
+ "model": "3032"
225
+ },
226
+ "4016": {
227
+ "model": "4016"
228
+ },
229
+ "4032": {
230
+ "model": "4032"
231
+ },
232
+ "8032": {
233
+ "model": "8032"
234
+ }
235
+ }
236
+ }
237
+ },
238
+ "files": [
239
+ "src"
240
+ ],
241
+ "publishConfig": {
242
+ "access": "public"
243
+ }
244
+ }
package/src/blocks.8bs ADDED
@@ -0,0 +1,288 @@
1
+ // @8bitscript/pet/blocks — quadrant-block pseudo-pixels, and the big digit
2
+ // tiles built from them.
3
+ //
4
+ // The PET has no bitmap and no redefinable glyphs (`video.glyphs` is 0),
5
+ // but its ROM has sixteen "quadrant block" characters — the fact sheet's
6
+ // `video.blockWidth`/`blockHeight` of 2: eight are ROM glyphs at screen
7
+ // codes 96-127 (blank, a half, a quadrant alone, or two diagonal
8
+ // quadrants), and the other eight are their hardware-inverted complements
9
+ // at 224-255 — bit 7 flips every pixel, so a pattern this ROM does not draw
10
+ // directly is its opposite, reversed. Together the sixteen cover every
11
+ // possible 2x2 on/off pattern one cell can show, turning the 40x25 (or
12
+ // 80x25) text grid into an 80x50 (or 160x50) grid of pseudo-pixels — see
13
+ // `docs/systems.md`'s "Blocks" canvas rung. Verified against the actual
14
+ // ROM: `characters-2.901447-10.bin`'s screen codes 96-127 are byte-for-byte
15
+ // identical whether the PCR selects the graphics or text set (both are the
16
+ // shared "$60-$7F graphics" range packages/pet/AGENTS.md's character-set
17
+ // table describes), so unlike `text.8bs`, nothing here ever needs to touch
18
+ // `viaPeripheralControl` — a block or a digit tile draws the same regardless
19
+ // of which set is selected, and can sit on screen next to either kind of text.
20
+ //
21
+ // `blocks.quad(topLeft, topRight, bottomLeft, bottomRight)` is the whole
22
+ // primitive: four bits in, one screen code out — enough to draw any
23
+ // pattern a cell can show. `digits` is one thing built from it: a 2x3-cell
24
+ // (4x6 pseudo-pixel) tile per digit 0-9, laid out as `const` data so the
25
+ // font costs only the bytes it draws — `draw()` for one digit, `print()`
26
+ // for a zero-padded field the way `text.printNumber` lays one out, and
27
+ // `printCentered()` for a number that grows and shrinks (a game tile: "2",
28
+ // then "16", then "2048") centered in a box instead of padded to a fixed
29
+ // width. Every one of them takes `invert`: drawn over a solid reverse-video
30
+ // tile, an inverted digit reads as dark ink cut into the tile's colour
31
+ // rather than a fresh glyph punching a black rectangle through it.
32
+ //
33
+ // 2 cells is the font's size, not a preference: four side-by-side tiles on
34
+ // a 40-column screen leave 9 cells per tile at most (4*9 + 3 gaps = 39), and
35
+ // a 3-wide font's own widest number ("2048", 4 digits) alone needs 12 — a
36
+ // 2-wide font needs 8, which fits with a column to spare. It also makes
37
+ // every digit count center exactly: leftover space is (cellWidth − digits×2),
38
+ // always the same parity as cellWidth, so an even tile width splits evenly
39
+ // on both sides for 1, 2, 3 or 4 digits alike — a scoreboard-size font (3
40
+ // wide) cannot do that for every digit count no matter which tile width is
41
+ // picked, the parity always favours some counts over others.
42
+ //
43
+ // The pixel art above each row of the table is the font's real source —
44
+ // redraw it there, then re-derive that row's two codes from `quad()`'s
45
+ // table below (or a quick script over it) to change how a digit looks.
46
+ import { Video } from "./geometry.8bs";
47
+
48
+ const COLUMNS: utinyint = Video.COLUMNS; // cells per row, so cell = y * COLUMNS + x
49
+
50
+ // Screen code for each of the sixteen 2x2 patterns, indexed by
51
+ // (topLeft<<3 | topRight<<2 | bottomLeft<<1 | bottomRight). The first eight
52
+ // (indexes 0-8, one bit set at most plus the all-off and the TL+BR
53
+ // diagonal) are ROM glyphs; the rest are those same bits inverted, which is
54
+ // exactly what reverse video already does in hardware, so the table just
55
+ // names the +128 codes directly rather than setting a reverse flag.
56
+ const QUAD: array<utinyint, 16> = [
57
+ 32, 108, 123, 98, // 0000 0001 0010 0011
58
+ 124, 225, 255, 254, // 0100 0101 0110 0111
59
+ 126, 127, 97, 252, // 1000 1001 1010 1011
60
+ 226, 251, 236, 160, // 1100 1101 1110 1111
61
+ ];
62
+
63
+ function patternIndex(topLeft: bool, topRight: bool, bottomLeft: bool, bottomRight: bool): utinyint {
64
+ let index: utinyint = 0;
65
+ if (topLeft) {
66
+ index = index + 8;
67
+ }
68
+ if (topRight) {
69
+ index = index + 4;
70
+ }
71
+ if (bottomLeft) {
72
+ index = index + 2;
73
+ }
74
+ if (bottomRight) {
75
+ index = index + 1;
76
+ }
77
+ return index;
78
+ }
79
+
80
+ function place(cell: usmallint, code: utinyint): void {
81
+ memory.write(0x8000 + cell, code);
82
+ }
83
+
84
+ export namespace blocks {
85
+ // The screen code that draws one cell's four pseudo-pixels: `true` lights
86
+ // a quadrant, `false` leaves it off. Anything a 2x2 block can show is one
87
+ // call away — a checkerboard, a diagonal, a custom glyph of your own.
88
+ function quad(topLeft: bool, topRight: bool, bottomLeft: bool, bottomRight: bool): utinyint {
89
+ return QUAD[patternIndex(topLeft, topRight, bottomLeft, bottomRight)];
90
+ }
91
+
92
+ // Writes one cell's worth of pseudo-pixels at `cell`, so a caller who
93
+ // does not want to hand-roll `memory.write` can still work in blocks.
94
+ function put(cell: usmallint, topLeft: bool, topRight: bool, bottomLeft: bool, bottomRight: bool): void {
95
+ place(cell, QUAD[patternIndex(topLeft, topRight, bottomLeft, bottomRight)]);
96
+ }
97
+ }
98
+
99
+ // ---- digits ---------------------------------------------------------------
100
+ //
101
+ // Each digit is 2 cells wide, 3 tall — 4x6 pseudo-pixels, a classic 3x5
102
+ // dot-matrix numeral elongated by one row (its own row 1 repeated) to fill
103
+ // all 6 rows with real strokes, plus one blank pixel-column of margin (the
104
+ // right column) so adjacent digits do not touch. There is no blank *row*:
105
+ // earlier drafts padded the 5-row numeral with one, and that row landing
106
+ // only at the bottom made a tile's margin above a digit come out twice its
107
+ // margin below (see @8bitscript/pet's AGENTS.md and this file's own
108
+ // digits.HEIGHT comment — a whole spare cell can only sit on one side of a
109
+ // digit, so any asymmetry inside the font itself compounds whichever side
110
+ // that cell lands on). A digit with no internal blank row leaves margin
111
+ // entirely to whoever places it, which is what makes a symmetric split
112
+ // possible at all. Drawn left to right, top to bottom, 6 codes per digit,
113
+ // one array line per tile row of 2 cells. `#` below is a lit pseudo-pixel,
114
+ // `.` unlit; every 2x2 block of picture cells is one QUAD code in the line
115
+ // beneath it, in the same (topLeft, topRight, bottomLeft, bottomRight)
116
+ // order `patternIndex` reads.
117
+ //
118
+ // 0 1 2 3 4 5 6 7 8 9
119
+ // ###. .#.. ###. ###. #.#. ###. ###. ###. ###. ###.
120
+ // #.#. ##.. ..#. ..#. #.#. #... #... ..#. #.#. #.#.
121
+ // #.#. ##.. ..#. ..#. #.#. #... #... ..#. #.#. #.#.
122
+ // #.#. .#.. ###. ###. ###. ###. ###. ..#. ###. ###.
123
+ // #.#. .#.. #... ..#. ..#. ..#. #.#. ..#. #.#. ..#.
124
+ // ###. ###. ###. ###. ..#. ###. ###. ..#. ###. ###.
125
+ //
126
+ const DIGIT_TILES: array<utinyint, 60> = [
127
+ // 0
128
+ 236, 97,
129
+ 97, 97,
130
+ 252, 97,
131
+ // 1
132
+ 254, 32,
133
+ 251, 32,
134
+ 254, 123,
135
+ // 2
136
+ 226, 97,
137
+ 98, 97,
138
+ 252, 123,
139
+ // 3
140
+ 226, 97,
141
+ 98, 97,
142
+ 98, 97,
143
+ // 4
144
+ 97, 97,
145
+ 252, 97,
146
+ 32, 97,
147
+ // 5
148
+ 236, 126,
149
+ 252, 123,
150
+ 98, 97,
151
+ // 6
152
+ 236, 126,
153
+ 252, 123,
154
+ 252, 97,
155
+ // 7
156
+ 226, 97,
157
+ 32, 97,
158
+ 32, 97,
159
+ // 8
160
+ 236, 97,
161
+ 252, 97,
162
+ 252, 97,
163
+ // 9
164
+ 236, 97,
165
+ 252, 97,
166
+ 98, 97,
167
+ ];
168
+
169
+ // Cell offset into DIGIT_TILES per digit (value * 6), read off instead of
170
+ // multiplied — the 6502 has no multiply instruction, and every value here
171
+ // is small and fixed.
172
+ const DIGIT_BASE: array<usmallint, 10> = [0, 6, 12, 18, 24, 30, 36, 42, 48, 54];
173
+
174
+ // Places above a field are still taken off, so a field narrower than its
175
+ // number shows the low digits — the same rule text.printNumber follows.
176
+ const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
177
+
178
+ // How many cells `count` digit tiles take, read off instead of multiplied
179
+ // by WIDTH (2) — the same reason DIGIT_BASE exists. Index 0 is unused (a
180
+ // number always has at least one digit) and kept so `count` indexes it
181
+ // directly.
182
+ const DIGIT_TILE_WIDTHS: array<utinyint, 6> = [0, 2, 4, 6, 8, 10];
183
+
184
+ export namespace digits {
185
+ const WIDTH: utinyint = 2; // cells per digit tile
186
+ const HEIGHT: utinyint = 3; // cells per digit tile
187
+
188
+ // How many decimal digits `value` has with no leading zeros — "16" is
189
+ // 2, "2048" is 4 — the width printCentered() lays out around, as
190
+ // opposed to print()'s fixed, zero-padded width.
191
+ function count(value: usmallint): utinyint {
192
+ let digitCount: utinyint = 1;
193
+ if (value >= 10) {
194
+ digitCount = 2;
195
+ }
196
+ if (value >= 100) {
197
+ digitCount = 3;
198
+ }
199
+ if (value >= 1000) {
200
+ digitCount = 4;
201
+ }
202
+ if (value >= 10000) {
203
+ digitCount = 5;
204
+ }
205
+ return digitCount;
206
+ }
207
+
208
+ // Draws one digit (0-9) as a WIDTH x HEIGHT tile whose top-left cell is
209
+ // `cell`. `base` and `rowCell` walk forward by WIDTH and COLUMNS each
210
+ // row instead of multiplying row by either, for the same reason
211
+ // DIGIT_BASE exists.
212
+ //
213
+ // `invert` swaps every cell's on and off quadrants (screen code bit 7,
214
+ // the same bit reverse video always was) — the digit's ink and its gaps
215
+ // trade places. Drawn over a solid reverse-video tile (a game board
216
+ // square, say), an inverted digit reads as dark ink cut into the tile's
217
+ // colour instead of a fresh glyph punching a black rectangle through
218
+ // it. QUAD's sixteen codes were chosen to be closed under this exact
219
+ // flip (every pattern's complement is also in the table), so inverting
220
+ // costs one add-or-subtract of 128 per cell, no second font.
221
+ function draw(cell: usmallint, value: utinyint, invert: bool = false): void {
222
+ let base: usmallint = DIGIT_BASE[value];
223
+ let rowCell: usmallint = cell;
224
+ let row: utinyint = 0;
225
+ while (row < digits.HEIGHT) {
226
+ let col: utinyint = 0;
227
+ while (col < digits.WIDTH) {
228
+ let code: utinyint = DIGIT_TILES[base + col];
229
+ if (invert) {
230
+ if (code < 128) {
231
+ code = code + 128;
232
+ } else {
233
+ code = code - 128;
234
+ }
235
+ }
236
+ place(rowCell + col, code);
237
+ col++;
238
+ }
239
+ base = base + digits.WIDTH;
240
+ rowCell = rowCell + COLUMNS;
241
+ row++;
242
+ }
243
+ }
244
+
245
+ // Writes `value` as exactly `width` block-digit tiles, zero-padded and
246
+ // right-aligned, laid out left to right starting at `cell` — a scoreboard
247
+ // line built from digits instead of text.printNumber's characters. `at`
248
+ // advances by WIDTH cells per digit instead of multiplying width by
249
+ // WIDTH, the same reason draw() steps rather than multiplies.
250
+ function print(cell: usmallint, value: usmallint, width: utinyint, invert: bool = false): void {
251
+ let k: utinyint = width;
252
+ if (k < DIGIT_PLACES.length) {
253
+ k = DIGIT_PLACES.length;
254
+ }
255
+ let at: usmallint = cell;
256
+ while (k > 0) {
257
+ let digit: utinyint = 0;
258
+ if (k <= DIGIT_PLACES.length) {
259
+ while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
260
+ value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
261
+ digit++;
262
+ }
263
+ }
264
+ if (k <= width) {
265
+ digits.draw(at, digit, invert);
266
+ at = at + digits.WIDTH;
267
+ }
268
+ k--;
269
+ }
270
+ }
271
+
272
+ // Writes `value` with no leading zeros, centered in a `widthCells`-wide
273
+ // box whose left edge is `left` — a number that grows from one digit to
274
+ // several (a 2048 tile: "2", then "16", then "2048") without print()'s
275
+ // fixed field or its zero-padding. Odd leftover space rounds to the
276
+ // left, the same way a centered thing usually does when it cannot split
277
+ // evenly. If the digits are wider than the box, they simply start at
278
+ // `left` and run past its right edge rather than negative into `pad`.
279
+ function printCentered(left: usmallint, widthCells: utinyint, value: usmallint, invert: bool = false): void {
280
+ let digitCount: utinyint = digits.count(value);
281
+ let used: utinyint = DIGIT_TILE_WIDTHS[digitCount];
282
+ let pad: utinyint = 0;
283
+ if (widthCells > used) {
284
+ pad = (widthCells - used) >> 1;
285
+ }
286
+ digits.print(left + pad, value, digitCount, invert);
287
+ }
288
+ }
@@ -0,0 +1,25 @@
1
+ // @8bitscript/pet — the screen's geometry, for the 40-column PETs.
2
+ //
3
+ // The one thing about the PET's display that changes between models is how
4
+ // wide it is: 40 columns on the 2001/3xxx/4xxx machines, 80 on the 8xxx
5
+ // business machines. Everything else this package does with the screen —
6
+ // screen RAM at $8000, one byte per cell, 25 rows, no colour — is the same
7
+ // on every PET ever made, so it is written once, in screen.8bs and text.8bs,
8
+ // against these three numbers. This file is the 40-column version, and
9
+ // what every PET profile reads except the 8032, which has its own
10
+ // `geometry.pet.8032.8bs` beside it: a build for that profile resolves the
11
+ // import of `./geometry.8bs` to that file instead (docs/packages.md,
12
+ // "System-specific files"), and text.COLUMNS, text.CELL_COUNT, and
13
+ // screen.blank()'s extent follow, with no second copy of the surface.
14
+ //
15
+ // Why a file, not a runtime probe: `cell = y * text.COLUMNS + x` is compile-
16
+ // time arithmetic in every program that uses it, and a 40-column constant on
17
+ // an 80-column screen does not draw narrow — row 1 lands in the middle of row
18
+ // 0. The width is a property of the build, chosen once, like Studio's tier.
19
+ // See packages/pet/AGENTS.md ("Models are profiles").
20
+
21
+ export namespace Video {
22
+ const COLUMNS: utinyint = 40;
23
+ const ROWS: utinyint = 25;
24
+ const CELL_COUNT: usmallint = 1000; // COLUMNS * ROWS: exactly the 1K of screen RAM at $8000
25
+ }
@@ -0,0 +1,15 @@
1
+ // @8bitscript/pet — the screen's geometry, for the 80-column 8032.
2
+ //
3
+ // The 8032 profile's version of geometry.8bs (see that file for why the
4
+ // geometry is a file of its own): a build with `--profile 8032` resolves
5
+ // every import of `./geometry.8bs` in this package to this one. The 8032's
6
+ // CRTC reads two bytes of screen RAM per character clock, so the screen is
7
+ // 80 x 25 = 2000 cells, still at $8000 ($8000-$87CF); nothing else about
8
+ // the display differs from the 40-column machines. Verified on screen under
9
+ // `xpet -model 8032` — see packages/pet/AGENTS.md.
10
+
11
+ export namespace Video {
12
+ const COLUMNS: utinyint = 80;
13
+ const ROWS: utinyint = 25;
14
+ const CELL_COUNT: usmallint = 2000; // COLUMNS * ROWS: $8000-$87CF
15
+ }
package/src/index.8bs ADDED
@@ -0,0 +1,48 @@
1
+ // @8bitscript/pet — Commodore PET target support: the hardware underneath.
2
+ //
3
+ // The module entry point named by the "8bitscript".entry field in this
4
+ // package's package.json, resolved and linked for:
5
+ //
6
+ // import { viaPeripheralControl } from "@8bitscript/pet";
7
+ //
8
+ // The surface is the hardware, one register at a time. What a program
9
+ // usually wants sits one layer up, in this package's own implementations of
10
+ // the portable capability packages — `./src/screen.8bs` behind
11
+ // @8bitscript/screen and `./src/text.8bs` behind @8bitscript/text, named by
12
+ // the "8bitscript".exports map in package.json — which import these
13
+ // registers and are built on nothing else.
14
+ //
15
+ // The PET is a monochrome machine — green or white phosphor, whatever the
16
+ // monitor is, with no colour hardware at all — so there is no colour
17
+ // register here to export; see screen.8bs for what that means for the
18
+ // portable surface. What it does have is a character set to select.
19
+
20
+ // $E84C, the VIA's peripheral control register (VIA at $E840 in
21
+ // llvm-mos-sdk's pet.h, `pcr` at offset 12 in its _6522.h). Bit 1 chooses
22
+ // the character set: $0C for graphics (upper case), $0E for text (lower
23
+ // case) — the POKE 59468,12 / POKE 59468,14 every PET manual gives.
24
+ @address(0xE84C)
25
+ export let viaPeripheralControl: volatile<u8>;
26
+
27
+ // PIA1 at $E810 (pet.h's `pia1`, a 6520): the keyboard is here, not on the
28
+ // VIA. PA0-3 select one of the ten matrix rows through a 4-to-10 decoder
29
+ // (write 0-9; 10-15 select nothing), PA4-7 are inputs (cassette switches,
30
+ // IEEE EOI in, the diagnostic sense line) that the ROM's data-direction
31
+ // setup leaves as inputs, so a bare row number is what to store. PB0-7 read
32
+ // the eight columns of the selected row, ACTIVE LOW: a pressed key is a 0
33
+ // bit. ./keyboard.8bs turns the pair into a per-frame snapshot.
34
+ //
35
+ // One thing to know before reading $E812 anywhere else: on a 6520, reading
36
+ // the port B data register also clears the CB1 interrupt flag in CRB
37
+ // ($E813 bit 7), and that flag is the vertical-retrace edge the frame
38
+ // runtime waits for (FRAME_SYNC.pet in packages/backend-6502 polls it and
39
+ // acknowledges it by reading this register). A read here between a retrace
40
+ // and the next waitFrame() eats that frame. Scan once, right after
41
+ // waitFrame() returns. $E811 (CRA / DDRA) and $E813 are not exported: CA2
42
+ // is IEEE EOI out on every board after the first, and the retrace flag is
43
+ // the runtime's.
44
+ @address(0xE810)
45
+ export let pia1PortA: volatile<u8>;
46
+
47
+ @address(0xE812)
48
+ export let pia1PortB: volatile<u8>;