@8bitscript/cx16 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,153 @@
1
+ {
2
+ "name": "@8bitscript/cx16",
3
+ "version": "0.1.0",
4
+ "description": "Commander X16 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
+ "./banks": "./src/banks.8bs",
12
+ "./mouse": "./src/mouse.8bs",
13
+ "./input": "./src/input.8bs",
14
+ "./pointer": "./src/pointer.8bs"
15
+ },
16
+ "hardware": {
17
+ "facts": {
18
+ "video.columns": 76,
19
+ "video.rows": 56,
20
+ "video.cellWidth": 8,
21
+ "video.cellHeight": 8,
22
+ "video.palette": 256,
23
+ "video.cellColors": 2,
24
+ "video.colorPerCell": true,
25
+ "video.glyphs": 256,
26
+ "video.blockWidth": 2,
27
+ "video.blockHeight": 2,
28
+ "video.bitmap": true,
29
+ "video.layers": 2,
30
+ "video.scroll": true,
31
+ "video.sprites": 128,
32
+ "video.spritesPerLine": 46,
33
+ "video.spriteWidth": 64,
34
+ "video.spriteHeight": 64,
35
+ "video.spriteColors": 255,
36
+ "video.frameRate": 60,
37
+ "audio.voices": 25,
38
+ "audio.noise": true,
39
+ "audio.envelope": true,
40
+ "audio.filter": false,
41
+ "audio.pcm": true,
42
+ "audio.volume": true,
43
+ "audio.entropy": false,
44
+ "input.keyboard": true,
45
+ "input.joysticks": 0,
46
+ "input.pads": 2,
47
+ "input.mouse": true,
48
+ "input.paddles": false,
49
+ "storage.save": true,
50
+ "memory.ram": 38655,
51
+ "memory.banked": true,
52
+ "memory.bankedKib": 512,
53
+ "storage.kib": 32768
54
+ },
55
+ "run": {
56
+ "x16emu": [
57
+ "-capture"
58
+ ]
59
+ },
60
+ "options": {
61
+ "ram": {
62
+ "label": "Banked RAM",
63
+ "default": "512",
64
+ "detect": "@8bitscript/cx16/banks",
65
+ "values": {
66
+ "64": {
67
+ "label": "64 KiB of banked RAM",
68
+ "run": {
69
+ "x16emu": [
70
+ "-ram",
71
+ "64"
72
+ ]
73
+ },
74
+ "facts": {
75
+ "memory.bankedKib": 64
76
+ }
77
+ },
78
+ "128": {
79
+ "label": "128 KiB of banked RAM",
80
+ "run": {
81
+ "x16emu": [
82
+ "-ram",
83
+ "128"
84
+ ]
85
+ },
86
+ "facts": {
87
+ "memory.bankedKib": 128
88
+ }
89
+ },
90
+ "256": {
91
+ "label": "256 KiB of banked RAM",
92
+ "run": {
93
+ "x16emu": [
94
+ "-ram",
95
+ "256"
96
+ ]
97
+ },
98
+ "facts": {
99
+ "memory.bankedKib": 256
100
+ }
101
+ },
102
+ "512": {
103
+ "label": "512 KiB of banked RAM",
104
+ "run": {
105
+ "x16emu": [
106
+ "-ram",
107
+ "512"
108
+ ]
109
+ },
110
+ "facts": {
111
+ "memory.bankedKib": 512
112
+ }
113
+ },
114
+ "1024": {
115
+ "label": "1024 KiB of banked RAM",
116
+ "run": {
117
+ "x16emu": [
118
+ "-ram",
119
+ "1024"
120
+ ]
121
+ },
122
+ "facts": {
123
+ "memory.bankedKib": 1024
124
+ }
125
+ },
126
+ "2048": {
127
+ "label": "2048 KiB of banked RAM",
128
+ "run": {
129
+ "x16emu": [
130
+ "-ram",
131
+ "2048"
132
+ ]
133
+ },
134
+ "facts": {
135
+ "memory.bankedKib": 2048
136
+ }
137
+ }
138
+ }
139
+ }
140
+ },
141
+ "presets": {}
142
+ }
143
+ },
144
+ "files": [
145
+ "src"
146
+ ],
147
+ "publishConfig": {
148
+ "access": "public"
149
+ },
150
+ "scripts": {
151
+ "test": "node --test"
152
+ }
153
+ }
package/src/banks.8bs ADDED
@@ -0,0 +1,128 @@
1
+ // @8bitscript/cx16/banks — how much banked RAM this X16 has.
2
+ //
3
+ // Named by "8bitscript".exports["./banks"] in this package's package.json:
4
+ //
5
+ // import { banks } from "@8bitscript/cx16/banks";
6
+ //
7
+ // let kib: usmallint = banks.kib(); // 64 .. 2048
8
+ //
9
+ // A run-time hardware probe, like @8bitscript/c64/reu. The X16's banked
10
+ // RAM is not a fixed amount: a board carries anywhere from 64 KiB to 2
11
+ // MiB of it, `x16emu -ram <KiB>` emulates each, and one binary runs on
12
+ // all of them. The build says which the *emulator* is fitted with (the
13
+ // catalog's `ram` option; `Memory.BANKED_KIB` from @8bitscript/system),
14
+ // and this says what the machine the program is running on really has. A
15
+ // program that never imports this file carries none of it.
16
+ //
17
+ // Banked RAM is 8 KiB pages seen through the window at `$A000`–`$BFFF`,
18
+ // the page selected by a write to `$0000` (the X16 puts its two bank
19
+ // registers in the zero page, which is why the SDK's own zero-page
20
+ // allocation starts at `$0022`). A machine has a power of two of them, 8
21
+ // pages (64 KiB) to 256 (2 MiB). So the count is found by asking about
22
+ // pages 8, 16, 32, 64 and 128 in turn: the first one the machine does not
23
+ // really have is the count.
24
+ //
25
+ // "Does not really have" has two shapes, and the probe tests for both,
26
+ // because which one a machine shows is the machine's business:
27
+ //
28
+ // 1. **It is a page it does have, under another number.** A board that
29
+ // keeps fewer than 8 bits of the page register wraps, so page N is
30
+ // page (N mod count). Every page tested here is one above a power of
31
+ // two, so where it wraps it lands on page 1 — the page the KERNAL
32
+ // leaves to us — and the test is whether page N+1 shows a marker just
33
+ // written to page 1. It is confirmed with a second, different marker,
34
+ // so a page that merely happens to hold those two bytes is not
35
+ // mistaken for a mirror.
36
+ // 2. **It is nothing at all.** Then the window floats: writes do not
37
+ // stick and reads return whatever was last on the bus. Verified under
38
+ // x16emu, which is this shape — with `-ram 64`, a byte written to
39
+ // page 200 read back as `$A0`, the high byte of the window's own
40
+ // address, and page 9 read `$3E`. So the second test is the plain
41
+ // one: write two different bytes and read both back.
42
+ //
43
+ // Only pages 1 and N+1 are ever written, and page N+1 only after it has
44
+ // been shown not to be a mirror — page 0 is the KERNAL's own workspace
45
+ // and is never touched. The two bytes at `$A000`–`$A001` of page 1 are
46
+ // scribbled on, so call this at start-up, before storing anything in
47
+ // banked RAM, and keep the answer. Page 1 is left selected, which is
48
+ // where the KERNAL leaves it.
49
+ //
50
+ // (The KERNAL can answer this too — `MEMTOP` with carry set returns the
51
+ // page count — but calling it means a JSR with register conventions the
52
+ // language has no spelling for. See ../AGENTS.md.)
53
+
54
+ namespace Bank {
55
+ const SELECT: usmallint = 0x0000; // the RAM page register
56
+ const WINDOW: usmallint = 0xA000; // first byte of the 8 KiB window
57
+ const WINDOW1: usmallint = 0xA001; // and the second
58
+ const OURS: utinyint = 1; // the page the KERNAL leaves to us
59
+ }
60
+
61
+ export namespace banks {
62
+ // Put `value` and its complement in our own page's first two bytes.
63
+ function mark(value: utinyint): void {
64
+ memory.write(Bank.SELECT, Bank.OURS);
65
+ memory.write(Bank.WINDOW, value);
66
+ memory.write(Bank.WINDOW1, value ^ 0xFF);
67
+ }
68
+
69
+ // Does `page` show what mark(value) wrote — is it our own page under
70
+ // another number?
71
+ function shows(page: utinyint, value: utinyint): bool {
72
+ memory.write(Bank.SELECT, page);
73
+ if (memory.read(Bank.WINDOW) != value) {
74
+ return false;
75
+ }
76
+ return memory.read(Bank.WINDOW1) == (value ^ 0xFF);
77
+ }
78
+
79
+ // Is `page` real memory? Two different bytes, both read back: a
80
+ // floating window echoes at most one of them.
81
+ function backed(page: utinyint): bool {
82
+ memory.write(Bank.SELECT, page);
83
+ memory.write(Bank.WINDOW, 0x5A);
84
+ memory.write(Bank.WINDOW1, 0xA5);
85
+ if (memory.read(Bank.WINDOW) != 0x5A) {
86
+ return false;
87
+ }
88
+ return memory.read(Bank.WINDOW1) == 0xA5;
89
+ }
90
+
91
+ // The 8 KiB pages of banked RAM this machine has: 8, 16, 32, 64, 128
92
+ // or 256.
93
+ function count(): usmallint {
94
+ let pages: utinyint = 8;
95
+ while (pages != 0) {
96
+ let above: utinyint = pages + Bank.OURS;
97
+ banks.mark(0x5A);
98
+ if (banks.shows(above, 0x5A)) {
99
+ banks.mark(0xC3);
100
+ if (banks.shows(above, 0xC3)) {
101
+ return banks.answer(pages); // a mirror of our own page
102
+ }
103
+ }
104
+ if (!banks.backed(above)) {
105
+ return banks.answer(pages); // nothing there
106
+ }
107
+ pages = pages * 2;
108
+ }
109
+ return banks.answer(0);
110
+ }
111
+
112
+ // Leave page 1 selected, as the KERNAL does, and widen the count; 0
113
+ // pages means the register never ran out, which is all 256.
114
+ function answer(pages: utinyint): usmallint {
115
+ memory.write(Bank.SELECT, Bank.OURS);
116
+ if (pages == 0) {
117
+ return 256;
118
+ }
119
+ let found: usmallint = pages;
120
+ return found;
121
+ }
122
+
123
+ // KiB of banked RAM plugged in: 64 on the smallest board, 2048 on the
124
+ // largest. One call at start-up; keep the answer.
125
+ function kib(): usmallint {
126
+ return banks.count() * 8;
127
+ }
128
+ }
package/src/index.8bs ADDED
@@ -0,0 +1,111 @@
1
+ // @8bitscript/cx16 — Commander X16 target support: the hardware underneath.
2
+ //
3
+ // See ../AGENTS.md before extending this package: the X16 has a great deal
4
+ // of hardware, all of it behind windows, ports, and firmware, and the rules
5
+ // there are about keeping bank state, VERA state, and optional hardware out
6
+ // of game code.
7
+ //
8
+ // The module entry point named by the "8bitscript".entry field in this
9
+ // package's package.json, resolved and linked for:
10
+ //
11
+ // import { setVramAddress, locateTextMap, mapBank, mapLow } from "@8bitscript/cx16";
12
+ //
13
+ // What a program usually wants sits one layer up, in this package's own
14
+ // implementations of the portable capability packages — `./src/screen.8bs`
15
+ // behind @8bitscript/screen and `./src/text.8bs` behind @8bitscript/text,
16
+ // named by the "8bitscript".exports map in package.json — which are built
17
+ // on what this file exports and on nothing else. Where a Commodore
18
+ // package's hardware surface is a set of registers, the X16's is a PORT:
19
+ // VERA's video memory is not CPU-addressable, so what this file exports is
20
+ // the address-port sequence every VRAM access performs, and where the text
21
+ // map currently is.
22
+ //
23
+ // ---- what the program finds when main() runs ---------------------------
24
+ //
25
+ // LLVM-MOS's cx16 start-up code prints CHR$(15) through the KERNAL's CHROUT
26
+ // immediately before calling main() — verified by disassembling a linked
27
+ // build (`lda #$0f / jsr $ffd2 / jsr main`, in the SDK's start-up
28
+ // sequence after `_start`; llvm-objdump labels the block with a local
29
+ // symbol, `shift`). CHR$(15) switches the X16 KERNAL's screen editor into
30
+ // ISO mode so that C's ASCII strings print as ASCII, and the KERNAL clears
31
+ // the screen as part of that switch. Two consequences every program on this
32
+ // target inherits, and the screen and text modules are written around:
33
+ //
34
+ // 1. The screen is blank when main() starts — the boot banner is gone —
35
+ // and it is the KERNAL's default blue (palette index 6) because that
36
+ // is the background nibble the KERNAL cleared every cell with. A
37
+ // program that sets a border and nothing else therefore appears to do
38
+ // nothing: examples/borders looked like exactly that, a solid blue
39
+ // screen, before screen.8bs painted anything itself.
40
+ //
41
+ // 2. The screen is in ISO mode: the character set VERA is drawing from
42
+ // is the KERNAL's ISO-8859-15 set, whose tile index IS the character
43
+ // code — 'A' is 65, '0' is 48, space is 32 — not the PETSCII screen
44
+ // codes (A = 1) the Commodore machines' screen memory takes.
45
+ // `text.putChar` takes ASCII on every machine, so here it passes the
46
+ // code straight through, where the Commodore packages translate.
47
+ //
48
+ // ---- VRAM through VERA's port ---------------------------------------------
49
+ //
50
+ // The text layer (layer 1 in the KERNAL's boot configuration: 1bpp
51
+ // 16-colour text, a tile map 128 tiles wide by 64 high, of which 80x60 are
52
+ // on screen at boot and 76x56 inside this package's border inset —
53
+ // screen_init in the ROM's kernal/drivers/x16/screen.s) takes
54
+ // each cell's colours from that cell's own attribute byte — high nibble
55
+ // background, low nibble foreground — stored in VRAM, which the CPU reaches
56
+ // only through VERA's address/data port: write the 17-bit address into
57
+ // $9F20 (bits 7:0), $9F21 (15:8) and $9F22 (bit 16, plus the auto-increment
58
+ // step in bits 7:4), then read or write $9F23 (DATA0), which advances the
59
+ // address by that step. Cell (x, y) is char byte base + y*256 + x*2,
60
+ // attribute byte one higher.
61
+ //
62
+ // Where `base` is comes from VERA, not from this file: the KERNAL puts
63
+ // the map at $1B000 today, but that is a constant in its source
64
+ // (inc/io.inc), not a published contract, and the ROM's own documentation
65
+ // reserves the right to move it. VERA's layer registers read back what
66
+ // was written to them (verified in the emulator's video_read), so
67
+ // locateTextMap() below reads L1_MAPBASE ($9F35) — the map address
68
+ // divided by 512 — before every access and derives the 17-bit base from
69
+ // it. What this file still assumes is the shape screen_init gives the
70
+ // map: 128 tiles wide (a 256-byte row stride), and wholly within one
71
+ // 64 KiB half of VRAM so the low 16 bits of any cell fit a usmallint.
72
+ //
73
+ // Interrupts are on while main() runs (the frame driver only disables
74
+ // them afterwards), and the KERNAL's IRQ handler drives the mouse-cursor
75
+ // sprite and the cursor blink through this same port. That is safe: both
76
+ // wrap their work in the ROM's screen_save_state/screen_restore_state,
77
+ // which save VERA CTRL, FX_CTRL and ADDR0 (kernal/drivers/x16/screen.s),
78
+ // so an interrupt landing between the three address writes and the data
79
+ // write finds everything as it left it.
80
+ //
81
+ // Writing CTRL ($9F25) also clears ADDRSEL (bit 0), selecting DATA0 — the
82
+ // port everything here uses; screen.8bs sets CTRL itself both times it
83
+ // needs the other DCSEL view, and leaves it 0 (the KERNAL's default).
84
+
85
+ // The text map's VRAM address, as (bit 16, low 16 bits), refreshed from
86
+ // VERA by locateTextMap() — see above for why it is read, not known. Two
87
+ // globals rather than a return value because a function returns one
88
+ // value, and this is two.
89
+ export let mapBank: utinyint = 0;
90
+ export let mapLow: usmallint = 0;
91
+
92
+ // L1_MAPBASE ($9F35) holds the map address >> 9: bit 7 is VRAM bit 16,
93
+ // bits 6:0 are address bits 15:9.
94
+ export function locateTextMap(): void {
95
+ mapBank = memory.read(0x9F35) / 128;
96
+ // Two steps, not `(byte % 128) * 512`: the product can reach 65024,
97
+ // past a 16-bit signed int, and in the C this becomes that is
98
+ // undefined arithmetic on a promoted byte (LLVM-MOS happens to wrap;
99
+ // nothing promises it). Multiplying a usmallint keeps it unsigned.
100
+ mapLow = memory.read(0x9F35) % 128;
101
+ mapLow = mapLow * 512;
102
+ }
103
+
104
+ // Point DATA0 at a 17-bit VRAM address (bank = bit 16, low = bits 15:0),
105
+ // with the auto-increment step index in the top nibble of $9F22 (0 = no
106
+ // step, 1 = +1, 2 = +2 — VERA's table of steps, not a byte count).
107
+ export function setVramAddress(bank: utinyint, low: usmallint, stepIndex: utinyint): void {
108
+ memory.write(0x9F20, low % 256);
109
+ memory.write(0x9F21, low / 256);
110
+ memory.write(0x9F22, bank + stepIndex * 16);
111
+ }
package/src/input.8bs ADDED
@@ -0,0 +1,146 @@
1
+ // @8bitscript/cx16/input — the Commander X16 behind @8bitscript/input.
2
+ //
3
+ // Named by "8bitscript".exports["./input"] in this package's package.json,
4
+ // and by "8bitscript".entry.cx16 in @8bitscript/input's, so a portable
5
+ // program writes
6
+ //
7
+ // import { input } from "@8bitscript/input";
8
+ //
9
+ // and gets this file on an X16.
10
+ //
11
+ // ---- what this layer answers ----------------------------------------------
12
+ //
13
+ // **The pointer, through the KERNAL.** `$FF68` mouse_config, `$FF71`
14
+ // mouse_scan and `$FF6B` mouse_get, wrapped by ./mouse.8bs. A menu asks
15
+ // "did the user just press", so the button is edge-triggered: true on the
16
+ // one frame the press begins, false while it is held. `poll()` has to be
17
+ // called exactly once a frame — twice a frame and every press is seen once
18
+ // and then swallowed. See ./mouse.8bs for why mouse_scan is called from
19
+ // here rather than left to the KERNAL IRQ (the frame driver has interrupts
20
+ // off).
21
+ //
22
+ // **Nothing else yet.** The keyboard is PS/2 decoded into a KERNAL queue
23
+ // (`$FFE4` GETIN) and the SNES-style pads are another KERNAL call
24
+ // (`$FF56` joystick_get). Both are still unreachable from this file for
25
+ // the same reason the mouse was: they need `asm6502` blocks and care about
26
+ // what the KERNAL expects to still be true when they are called. They are
27
+ // the next thing this layer should grow, not a gap this file hides —
28
+ // `left()`/`right()`/`confirm()`/`cancel()` stay false, and a program
29
+ // still links.
30
+ //
31
+ // **`input.mouse` is true on this machine by default**, because the
32
+ // hardware really does have one. `HAS_MOUSE` is that fact, so on a future
33
+ // build that could take the mouse out the pointer half of this file is
34
+ // `if (false)` and LLVM deletes it. Today there is no such catalog option;
35
+ // the emulator's mouse is always there.
36
+ //
37
+ // **The pointer is reported in cells, not pixels.** `pointerCell()` is a
38
+ // flat cell index, the same number `text.print()` takes. The KERNAL's
39
+ // position is a pixel from the top-left of the *active area* — the same
40
+ // origin cell (0, 0) has after screen.8bs insets the display so VERA's
41
+ // border colour has somewhere to show. An earlier draft subtracted that
42
+ // 16-pixel inset again, and the arrow sat two cells down-right of the
43
+ // cell `pointerCell()` named: mouse_get said (319, 239) and the sprite
44
+ // was on screen at (335, 254), which is (319, 239) plus the inset, so
45
+ // the KERNAL is already in the inset space. Shift down by the 8-pixel
46
+ // cell size; a pointer past the visible 76×56 clamps to the last cell.
47
+ import { mouse } from "./mouse.8bs";
48
+
49
+ const HAS_MOUSE: bool = #fact(input.mouse);
50
+
51
+ const COLUMNS: usmallint = #fact(video.columns);
52
+ const ROWS: usmallint = #fact(video.rows);
53
+ const CELL_SHIFT: utinyint = 3; // 8 pixels a cell
54
+
55
+ namespace Edge {
56
+ const LEFT: utinyint = 1;
57
+ const RIGHT: utinyint = 2;
58
+ const UP: utinyint = 4;
59
+ const DOWN: utinyint = 8;
60
+ const CONFIRM: utinyint = 16;
61
+ const CANCEL: utinyint = 32;
62
+ const BUTTON: utinyint = 64;
63
+ }
64
+
65
+ let held: utinyint = 0;
66
+ let before: utinyint = 0;
67
+ let began: utinyint = 0;
68
+
69
+ let atCell: usmallint = 0;
70
+
71
+ // Pixel to a column or row on the visible grid: shift down by the cell
72
+ // size, clamp to the last cell. mouse_get's origin is the active area's
73
+ // top-left (see the header), so there is no inset to subtract.
74
+ function cellOn(pixel: usmallint, limit: usmallint): usmallint {
75
+ let at: usmallint = pixel >> CELL_SHIFT;
76
+ if (at >= limit) {
77
+ return limit - 1;
78
+ }
79
+ return at;
80
+ }
81
+
82
+ export namespace input {
83
+
84
+ function begin(): void {
85
+ if (HAS_MOUSE) {
86
+ mouse.begin();
87
+ }
88
+ }
89
+
90
+ // Once a frame, right after waitFrame(), and never twice.
91
+ function poll(): void {
92
+ if (HAS_MOUSE) {
93
+ mouse.poll();
94
+ }
95
+
96
+ before = held;
97
+ held = 0;
98
+
99
+ if (HAS_MOUSE) {
100
+ let column: usmallint = cellOn(mouse.x(), COLUMNS);
101
+ let row: usmallint = cellOn(mouse.y(), ROWS);
102
+ atCell = row * COLUMNS + column;
103
+ if (mouse.left()) {
104
+ held = held | Edge.BUTTON;
105
+ }
106
+ }
107
+
108
+ began = held & (before ^ 0xFF);
109
+ }
110
+
111
+ function left(): bool {
112
+ return (began & Edge.LEFT) != 0;
113
+ }
114
+
115
+ function right(): bool {
116
+ return (began & Edge.RIGHT) != 0;
117
+ }
118
+
119
+ function up(): bool {
120
+ return (began & Edge.UP) != 0;
121
+ }
122
+
123
+ function down(): bool {
124
+ return (began & Edge.DOWN) != 0;
125
+ }
126
+
127
+ function confirm(): bool {
128
+ return (began & Edge.CONFIRM) != 0;
129
+ }
130
+
131
+ function cancel(): bool {
132
+ return (began & Edge.CANCEL) != 0;
133
+ }
134
+
135
+ function pointer(): bool {
136
+ return HAS_MOUSE;
137
+ }
138
+
139
+ function pointerCell(): usmallint {
140
+ return atCell;
141
+ }
142
+
143
+ function pointerButton(): bool {
144
+ return (began & Edge.BUTTON) != 0;
145
+ }
146
+ }
package/src/mouse.8bs ADDED
@@ -0,0 +1,187 @@
1
+ // @8bitscript/cx16/mouse — the X16's PS/2 mouse, through the KERNAL.
2
+ //
3
+ // Named by "8bitscript".exports["./mouse"] in this package's package.json:
4
+ //
5
+ // import { mouse } from "@8bitscript/cx16/mouse";
6
+ //
7
+ // mouse.begin(); // once, at start-up
8
+ // ...
9
+ // waitFrame();
10
+ // mouse.poll(); // once a frame
11
+ // if (mouse.present()) { ... mouse.x() ... }
12
+ //
13
+ // The X16 has no control-port mouse and no registers a program pokes. The
14
+ // pointer is a KERNAL service: `$FF68` mouse_config turns it on (and the
15
+ // firmware draws it as VERA sprite 0), `$FF71` mouse_scan reads the PS/2
16
+ // device and moves that sprite, `$FF6B` mouse_get copies the position and
17
+ // buttons out. All three were read in the ROM this package was verified
18
+ // against — x16-rom `fbe32a60`, `kernal/drivers/x16/ps2mouse.s` — not
19
+ // recalled.
20
+ //
21
+ // **Why poll() calls ps2data_fetch then mouse_scan.** The frame driver
22
+ // disables interrupts (FRAME_SYNC.cx16's `sei`, every waitFrame(), so it
23
+ // can own VERA's VSYNC bit instead of racing the KERNAL IRQ for it). The
24
+ // KERNAL IRQ is what would have called extapi `$08` ps2data_fetch (poll
25
+ // the SMC) and then mouse_scan. mouse_scan only applies bytes already in
26
+ // that buffer — with the IRQ silenced, a scan alone is a no-op and the
27
+ // sprite stays at mouse_config's rest position. `$FEAB` with A=8 is the
28
+ // published fetch; it is not re-entrant, which is why poll() `sei`s first.
29
+ // mouse_scan then saves and restores VERA address state around the sprite
30
+ // write (the same screen_save_state/restore_state the IRQ uses), so a
31
+ // program's text port is still where it left it.
32
+ //
33
+ // **Why the buffer lives at `$80`–`$84`.** mouse_get wants a zero-page
34
+ // address in X and writes four consecutive bytes there (X lo/hi, Y lo/hi),
35
+ // returning buttons in A. The SDK's cx16 link script gives the *program*
36
+ // `$02`–`$7F` (`__basic_zp_end = $80`), and LLVM-MOS allocates that range
37
+ // for imaginary registers and `.zp.noinit` variables. An earlier draft
38
+ // parked this buffer at `$7B` and `sta $7B` for the buttons overwrote
39
+ // `there` every poll — `present()` was false on a machine whose pointer
40
+ // was drawing in the middle of the screen. `$80`–`$84` is the first byte
41
+ // the program is not given, so the linker will not put a variable there.
42
+ // The KERNAL's mouse state lives in banked KVARSB0, not here.
43
+ //
44
+ // **Presence is the fact, not a probe.** A 1351 says it is there through
45
+ // the SID's pots; the X16's KERNAL does not. `input.mouse` is true on the
46
+ // stock sheet because the hardware has a mouse (the emulator's is always
47
+ // there), and `present()` is that fact. A stored flag set before a KERNAL
48
+ // call and read after it is not safe: `asm6502` is opaque, LLVM may keep
49
+ // the flag in a register the call trashes — measured, with the pointer
50
+ // drawing at (319, 239) while `present()` was false. A board whose SMC
51
+ // reports `BAT_FAIL` ($FC) leaves the mouse off and this file will still
52
+ // say present. That case has not been seen under x16emu; *to verify* on a
53
+ // machine with the mouse unplugged.
54
+ //
55
+ // **The rest position is the centre**, not the corner. mouse_config with
56
+ // a nonzero size (this file asks screen_mode, $FF5F, carry set, for the
57
+ // current 8-pixel size — 80×60 in the KERNAL's text mode) parks the
58
+ // pointer at half that range. Under x16emu that is pixel (319, 239) of
59
+ // the 640×480 output. Movement is not exercised headlessly: nothing in a
60
+ // `--screenshot` run moves the host pointer x16emu reads.
61
+ const HAS_MOUSE: bool = #fact(input.mouse);
62
+
63
+ // mouse_get's 4-byte position (X, then Y, 16-bit little-endian each) and
64
+ // the button byte it returns in A. See the header for why these addresses.
65
+ @address(0x80)
66
+ let posX: volatile<usmallint>;
67
+ @address(0x82)
68
+ let posY: volatile<usmallint>;
69
+ @address(0x84)
70
+ let rawButtons: volatile<utinyint>;
71
+
72
+ // Copied out of the buffer during poll(), so every caller in a frame
73
+ // shares one reading — the same discipline @8bitscript/c64/mouse holds
74
+ // to, even though these locations do not run on their own the way the
75
+ // SID's converter does.
76
+ let atX: usmallint = 0;
77
+ let atY: usmallint = 0;
78
+ let heldButtons: utinyint = 0;
79
+ let started: bool = false;
80
+
81
+ export namespace Mouse {
82
+ const LEFT: utinyint = 1;
83
+ }
84
+
85
+ export namespace mouse {
86
+
87
+ // Once, at start-up, before the first poll(). Turns the KERNAL mouse
88
+ // on at the current screen's 8-pixel size, which also loads the default
89
+ // pointer into sprite 0 and parks it at the centre. Harmless to call
90
+ // twice: the second call is skipped, so input.begin() and pointer.begin()
91
+ // can both name it without re-centering.
92
+ function begin(): void {
93
+ if (HAS_MOUSE) {
94
+ if (started) {
95
+ return;
96
+ }
97
+ started = true;
98
+ asm6502 {
99
+ sec
100
+ jsr $FF5F
101
+ lda #1
102
+ jsr $FF68
103
+ ldx #$80
104
+ jsr $FF6B
105
+ sta $84
106
+ }
107
+ atX = posX;
108
+ atY = posY;
109
+ heldButtons = rawButtons;
110
+ }
111
+ }
112
+
113
+ // Once a frame, after waitFrame(). Fetch from the SMC first (the IRQ
114
+ // is off), then mouse_scan, then mouse_get into the buffer, then copy.
115
+ // One reading, kept.
116
+ function poll(): void {
117
+ if (HAS_MOUSE) {
118
+ asm6502 {
119
+ sei
120
+ lda #8
121
+ jsr $FEAB
122
+ jsr $FF71
123
+ ldx #$80
124
+ jsr $FF6B
125
+ sta $84
126
+ }
127
+ atX = posX;
128
+ atY = posY;
129
+ heldButtons = rawButtons;
130
+ }
131
+ }
132
+
133
+ // True on a build fitted with a mouse, which is every stock X16. This
134
+ // is the fact, not a stored flag: an `asm6502` block is opaque to
135
+ // LLVM, so a bool set before a KERNAL call and read after it can be
136
+ // kept in a register the call trashes. Measured: `there = true` in
137
+ // begin() then mouse_get in poll() left present() false while the
138
+ // pointer was drawing at (319, 239).
139
+ function present(): bool {
140
+ return HAS_MOUSE;
141
+ }
142
+
143
+ // Pixel position from the top-left of the active area — the same
144
+ // origin cell (0, 0) has after screen.8bs insets the display. The
145
+ // KERNAL parks a freshly configured mouse at half the current
146
+ // screen_mode range; under x16emu that is (319, 239), which is the
147
+ // centre of the 640×480 the mode names, not of the inset picture.
148
+ function x(): usmallint {
149
+ return atX;
150
+ }
151
+
152
+ function y(): usmallint {
153
+ return atY;
154
+ }
155
+
156
+ // The left button, as of the last poll(). Bit 0 of mouse_get's A,
157
+ // which ps2mouse.s documents as left (bit 1 right, bit 2 middle).
158
+ function left(): bool {
159
+ return (heldButtons & Mouse.LEFT) != 0;
160
+ }
161
+
162
+ // Hide the pointer and stop tracking. pointer.hide() is the caller;
163
+ // the next show() turns tracking back on without re-centering
164
+ // (mouse_config $FF, size left as-is).
165
+ function hide(): void {
166
+ if (HAS_MOUSE) {
167
+ asm6502 {
168
+ lda #0
169
+ ldx #0
170
+ ldy #0
171
+ jsr $FF68
172
+ }
173
+ }
174
+ }
175
+
176
+ // Undo hide(): mouse on, keep the current cursor and position.
177
+ function show(): void {
178
+ if (HAS_MOUSE) {
179
+ asm6502 {
180
+ lda #$FF
181
+ ldx #0
182
+ ldy #0
183
+ jsr $FF68
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,77 @@
1
+ // @8bitscript/cx16/pointer — the X16 behind @8bitscript/pointer: the
2
+ // KERNAL's own mouse cursor.
3
+ //
4
+ // Named by "8bitscript".exports["./pointer"] in this package's package.json,
5
+ // and by "8bitscript".entry.cx16 in @8bitscript/pointer's, so a portable
6
+ // program writes
7
+ //
8
+ // import { pointer } from "@8bitscript/pointer";
9
+ //
10
+ // and gets this file on an X16. It is the *visible* half of the pointer
11
+ // @8bitscript/input already reports: `input` says where the pointer is and
12
+ // whether its button went down, and this says what the user sees while
13
+ // they aim it.
14
+ //
15
+ // ---- the firmware draws it ------------------------------------------------
16
+ //
17
+ // This is the one machine of the nine where a program need not draw a
18
+ // cursor at all. `$FF68` mouse_config (./mouse.8bs's begin()) loads the
19
+ // KERNAL's default pointer — Susan Kare's 16×16 1bpp arrow — into VERA
20
+ // sprite 0 and the firmware moves it. `update()` here is therefore empty
21
+ // on the happy path: `input.poll()` has already called mouse_scan, which
22
+ // writes the sprite's position. A program that never polls gets an arrow
23
+ // that never moves, the same contract as on the C64.
24
+ //
25
+ // `DRAWS` is `#fact(input.mouse)`, true on the stock machine. The rest
26
+ // position is the *centre* of the 640×480 output, not the corner: that is
27
+ // where mouse_config parks it, verified in ps2mouse.s (x16-rom fbe32a60).
28
+ //
29
+ // `setColor` does nothing. The firmware pointer is 1bpp black and white,
30
+ // not a VIC-style sprite colour this layer can poke without fighting the
31
+ // KERNAL's sprite_set_image. Studio asks for white, which is already what
32
+ // the arrow's fill is.
33
+ //
34
+ // `hide()` turns the KERNAL mouse off (mouse_config 0) and `update()`
35
+ // turns it back on without re-centering (mouse_config $FF, size left
36
+ // as-is), so a program that hides for one frame — the pointer example's
37
+ // cancel — gets the arrow back on the next without a jump.
38
+ import { mouse } from "./mouse.8bs";
39
+
40
+ const HAS_MOUSE: bool = #fact(input.mouse);
41
+
42
+ let hidden: bool = false;
43
+
44
+ export namespace pointer {
45
+
46
+ const DRAWS: bool = #fact(input.mouse);
47
+
48
+ function begin(): void {
49
+ if (HAS_MOUSE) {
50
+ // input.begin() has usually already done this; mouse.begin()
51
+ // is skipped the second time so the pointer is not re-centered.
52
+ mouse.begin();
53
+ }
54
+ }
55
+
56
+ function setColor(color: utinyint): void {
57
+ if (HAS_MOUSE) {
58
+ // The firmware pointer is black and white; see the header.
59
+ }
60
+ }
61
+
62
+ function update(): void {
63
+ if (HAS_MOUSE) {
64
+ if (hidden) {
65
+ mouse.show();
66
+ hidden = false;
67
+ }
68
+ }
69
+ }
70
+
71
+ function hide(): void {
72
+ if (HAS_MOUSE) {
73
+ mouse.hide();
74
+ hidden = true;
75
+ }
76
+ }
77
+ }
package/src/screen.8bs ADDED
@@ -0,0 +1,230 @@
1
+ // @8bitscript/cx16/screen — the Commander X16'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 X16: @8bitscript/screen's entry is
10
+ // keyed by machine and delegates here. Built on the VERA port protocol
11
+ // @8bitscript/cx16 exports — read that file first for what the KERNAL
12
+ // leaves on screen and how VRAM is reached. Every machine's screen.8bs
13
+ // exports this same surface, which is what lets a program import it from
14
+ // @8bitscript/screen and never name the hardware.
15
+ import { mapBank, mapLow, locateTextMap, setVramAddress } from "./index.8bs";
16
+
17
+ // ---- where a border comes from --------------------------------------------
18
+ //
19
+ // VERA's border colour is a plain CPU-addressable register — but the
20
+ // border it colours has no size by default. VERA draws layers inside an
21
+ // "active area" whose edges are DC_HSTART/HSTOP/VSTART/VSTOP, and the
22
+ // KERNAL boots with that area set to the entire 640x480 output, so nothing
23
+ // is left over for the border colour to fill. Setting the border alone is
24
+ // invisible — the second reason examples/borders once showed only blue.
25
+ //
26
+ // So setColors() shrinks the active area by 16 pixels on every side —
27
+ // HSTART/HSTOP count in units of 4 pixels (0..160), VSTART/VSTOP in units
28
+ // of 2 (0..240), hence 4/156 and 8/232 — and VERA fills the 16-pixel ring
29
+ // outside it with the border colour. The text layer is positioned relative
30
+ // to the active area, so cell (0,0) sits at the inset's top-left corner
31
+ // and the map's rightmost 4 columns and bottom 4 rows fall outside the
32
+ // active area: the grid a program sees is the 76x56 that is drawn
33
+ // (text.COLUMNS / text.CELL_COUNT say so), not the map's 80x60. This
34
+ // is the closest thing the X16 has to the VIC/VIC-II's dedicated border
35
+ // area, and like the NES's drawn frame it is honest about what it is: a
36
+ // choice this module makes, not a register the machine has.
37
+ //
38
+ // "Relative" needs one correction, measured rather than assumed. Under
39
+ // x16emu r50 the layer's first line lands TWO lines above the active
40
+ // area's top edge whenever VSTART is nonzero: with a 16-line inset, row 0
41
+ // of text occupied lines 14-21 and its top two lines were hidden under
42
+ // the border, and the same two lines with a 32-line inset. The emulator's
43
+ // video.c shows why — its layer line counter starts accumulating on the
44
+ // VSTART line through a two-line register-history pipeline (the one that
45
+ // models VERA's own raster-effect latency), so the active area's first
46
+ // line already reads as layer line 2. setColors() compensates with layer
47
+ // 1's vertical scroll: 510, which is -2 modulo the map's 512 lines, puts
48
+ // layer line 0 on the active area's first line exactly (verified the same
49
+ // way: row 0's glyphs occupy lines 16-23 with the 16-line inset, none
50
+ // hidden). The horizontal edge needs no such correction. Real hardware
51
+ // has not been checked; if VERA silicon has no such offset, the visible
52
+ // symptom would be the map's last row showing in the top two lines — so
53
+ // the repaint below covers that row too and keeps its characters blank.
54
+ //
55
+ // The four DC_ registers live at $9F29-$9F2C under two views selected by
56
+ // DCSEL (CTRL $9F25 bit 1): DCSEL=0 is VIDEO/HSCALE/VSCALE/BORDER, DCSEL=1
57
+ // is HSTART/HSTOP/VSTART/VSTOP — confirmed against llvm-mos-sdk's cx16.h,
58
+ // which lays out `struct __vera` exactly so. setColors() sets DCSEL
59
+ // itself both times and leaves it 0 (the KERNAL's default), so nothing
60
+ // here assumes what a previous program left it at.
61
+ //
62
+ // ---- the background: painted, not set ---------------------------------------
63
+ //
64
+ // There is no screen-background register at all: each text cell's colours
65
+ // come from its own attribute byte in VRAM (see @8bitscript/cx16). So the
66
+ // background is painted: setColors() writes every one of the 4800
67
+ // on-screen cells' attribute bytes to `background` in the high nibble and
68
+ // white (1) in the low — 60 rows of 80 auto-incrementing writes — the same
69
+ // way the Commodore packages draw their digits in white against whatever
70
+ // background is chosen. Around 60,000 cycles at 8MHz, a few milliseconds:
71
+ // VERA's VRAM is safe to write at any point in the frame (a write mid-frame
72
+ // can tear for that one frame; it cannot corrupt anything, unlike the
73
+ // NES's port), so this needs no vblank budget and setColors() is safe from
74
+ // the frame loop as well as from setup. Its side effect is deliberate and
75
+ // documented: any foreground colour a program set with text.putColor() is
76
+ // reset to white by the next setColors().
77
+ //
78
+ // Colours are VERA palette indices. The KERNAL's default palette starts
79
+ // with the sixteen C64 colours in C64 order — 0 black, 1 white, 2 red,
80
+ // 3 cyan, 4 purple, 5 green, 6 blue, 7 yellow, and so on — verified on
81
+ // screen under x16emu for the values examples/borders uses, so the
82
+ // Commodore packages' colour numbers mean the same colours here.
83
+
84
+ // The border exists once the active area is inset — 16 pixels in on
85
+ // every side — and layer 1 is scrolled so cell (0,0) lands at its corner.
86
+ // Done by whichever screen call comes first, once.
87
+ let insetDone: utinyint = 0;
88
+
89
+ function insetPicture(): void {
90
+ if (insetDone == 1) {
91
+ return;
92
+ }
93
+ insetDone = 1;
94
+ // DCSEL=1: the active-area edges.
95
+ memory.write(0x9F25, 0x02);
96
+ memory.write(0x9F29, 4); // HSTART: 16px
97
+ memory.write(0x9F2A, 156); // HSTOP: 624px
98
+ memory.write(0x9F2B, 8); // VSTART: 16px
99
+ memory.write(0x9F2C, 232); // VSTOP: 464px
100
+ memory.write(0x9F25, 0x00); // back to DCSEL=0
101
+ // Layer 1's scroll: horizontal 0, vertical 510 = -2 mod the map's
102
+ // 512 lines — the two-line correction the header explains, so that
103
+ // cell (0,0) lands exactly at the active area's top-left corner.
104
+ memory.write(0x9F37, 0x00); // L1_HSCROLL_L
105
+ memory.write(0x9F38, 0x00); // L1_HSCROLL_H
106
+ memory.write(0x9F39, 0xFE); // L1_VSCROLL_L: 510 & 0xFF
107
+ memory.write(0x9F3A, 0x01); // L1_VSCROLL_H: 510 >> 8
108
+ }
109
+
110
+ export namespace screen {
111
+ function setColors(border: u8, background: u8): void {
112
+ insetPicture();
113
+ // DCSEL=0 (and ADDRSEL=0): the border colour itself.
114
+ memory.write(0x9F25, 0x00);
115
+ memory.write(0x9F2C, border);
116
+
117
+ // Every cell's attribute byte: background high, white low. All 64
118
+ // map rows, not just the 60 on screen — row 63 is what the
119
+ // vertical-scroll correction above would expose if real hardware
120
+ // lacks the offset it corrects (see the header), so it gets the
121
+ // same colours and, below, blank characters.
122
+ locateTextMap();
123
+ for (let row: usmallint = 0; row < 64; row++) {
124
+ setVramAddress(mapBank, mapLow + 1 + row * 256, 2);
125
+ for (let col: utinyint = 0; col < 80; col++) {
126
+ memory.write(0x9F23, background * 16 + 1);
127
+ }
128
+ }
129
+ setVramAddress(mapBank, mapLow + 63 * 256, 2);
130
+ for (let col: utinyint = 0; col < 80; col++) {
131
+ memory.write(0x9F23, 32);
132
+ }
133
+ }
134
+
135
+ // `blank(border, background)`: every cell blank and both colours set —
136
+ // black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
137
+ // to leave one as it is. `setBorder` and `setBackground` are one
138
+ // colour each, `setColors` the pair; `text.setColor` owns what the
139
+ // next print looks like.
140
+ function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
141
+ if (border != BorderColor.KEEP) {
142
+ screen.setBorder(border);
143
+ }
144
+ if (background != BackgroundColor.KEEP) {
145
+ screen.setBackground(background);
146
+ }
147
+ // Every character of all 64 map rows (see setColors above for why
148
+ // 64) blank; the attribute bytes between them are left alone.
149
+ locateTextMap();
150
+ for (let row: usmallint = 0; row < 64; row++) {
151
+ setVramAddress(mapBank, mapLow + row * 256, 2);
152
+ for (let col: utinyint = 0; col < 80; col++) {
153
+ memory.write(0x9F23, 32);
154
+ }
155
+ }
156
+ }
157
+
158
+ function setBackground(background: u8): void {
159
+ insetPicture();
160
+ // The background lives in every cell's attribute byte, so this is
161
+ // the same repaint setColors() does: background high, white low.
162
+ locateTextMap();
163
+ for (let row: usmallint = 0; row < 64; row++) {
164
+ setVramAddress(mapBank, mapLow + 1 + row * 256, 2);
165
+ for (let col: utinyint = 0; col < 80; col++) {
166
+ memory.write(0x9F23, background * 16 + 1);
167
+ }
168
+ }
169
+ }
170
+
171
+ function setBorder(border: u8): void {
172
+ insetPicture();
173
+ memory.write(0x9F25, 0x00); // DCSEL=0: the border colour register
174
+ memory.write(0x9F2C, border);
175
+ }
176
+ }
177
+
178
+ // ---- colour names ----------------------------------------------------------
179
+ //
180
+ // The same eight names every machine's screen.8bs exports — Black, White,
181
+ // Red, Cyan, Purple, Green, Blue, Yellow — so a program can write
182
+ // `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` through
183
+ // @8bitscript/screen and get blue on every target. The values are the first
184
+ // sixteen entries of VERA's default palette, which are the C64's sixteen
185
+ // colours in the C64's numbering (see above), so both namespaces carry all
186
+ // sixteen.
187
+
188
+ export namespace BorderColor {
189
+ const BLACK: utinyint = 0;
190
+ const WHITE: utinyint = 1;
191
+ const RED: utinyint = 2;
192
+ const CYAN: utinyint = 3;
193
+ const PURPLE: utinyint = 4;
194
+ const GREEN: utinyint = 5;
195
+ const BLUE: utinyint = 6;
196
+ const YELLOW: utinyint = 7;
197
+ const ORANGE: utinyint = 8;
198
+ const BROWN: utinyint = 9;
199
+ const LIGHT_RED: utinyint = 10;
200
+ const DARK_GREY: utinyint = 11;
201
+ const GREY: utinyint = 12;
202
+ const LIGHT_GREEN: utinyint = 13;
203
+ const LIGHT_BLUE: utinyint = 14;
204
+ const LIGHT_GREY: utinyint = 15;
205
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
206
+ // a value no register here takes.
207
+ const KEEP: utinyint = 255;
208
+ }
209
+
210
+ export namespace BackgroundColor {
211
+ const BLACK: utinyint = 0;
212
+ const WHITE: utinyint = 1;
213
+ const RED: utinyint = 2;
214
+ const CYAN: utinyint = 3;
215
+ const PURPLE: utinyint = 4;
216
+ const GREEN: utinyint = 5;
217
+ const BLUE: utinyint = 6;
218
+ const YELLOW: utinyint = 7;
219
+ const ORANGE: utinyint = 8;
220
+ const BROWN: utinyint = 9;
221
+ const LIGHT_RED: utinyint = 10;
222
+ const DARK_GREY: utinyint = 11;
223
+ const GREY: utinyint = 12;
224
+ const LIGHT_GREEN: utinyint = 13;
225
+ const LIGHT_BLUE: utinyint = 14;
226
+ const LIGHT_GREY: utinyint = 15;
227
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
228
+ // a value no register here takes.
229
+ const KEEP: utinyint = 255;
230
+ }
package/src/text.8bs ADDED
@@ -0,0 +1,173 @@
1
+ // @8bitscript/cx16/text — the Commander X16'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 X16. Built on the VERA port protocol
10
+ // @8bitscript/cx16 exports — read that file first for what the KERNAL
11
+ // leaves on screen and how VRAM is reached. Every machine's text.8bs
12
+ // exports this same namespace — the same names, the same shapes, ASCII
13
+ // codes, and a cell 0 at the top-left corner inside the border — so a
14
+ // program that imports it from @8bitscript/text draws the same thing on
15
+ // every target.
16
+ import { mapBank, mapLow, locateTextMap, setVramAddress } from "./index.8bs";
17
+
18
+ // 76x56 = 4256 cells, row-major (the map is 80x60; screen.8bs's border
19
+ // inset shows 76x56 of it, and that is the grid); codes are ASCII, as on every machine, and
20
+ // here that is also what the hardware wants (ISO mode — see
21
+ // @8bitscript/cx16). Each cell is two bytes in the map — the character,
22
+ // then its attribute byte: background colour in the high nibble,
23
+ // foreground in the low.
24
+ //
25
+ // putColor is real here, unlike on the NES and Atari: it sets a cell's
26
+ // foreground nibble and leaves the background nibble as it finds it — a
27
+ // read-modify-write through the same port, with the auto-increment step at
28
+ // 0 so the read and the write land on one byte (VERA fetches the byte when
29
+ // the address is set, so DATA0 reads it back without moving). Whose
30
+ // background that nibble holds is not this module's business: the KERNAL's
31
+ // blue before screen.setColors() has painted, the painted colour after.
32
+
33
+ // ---- print: strings and number fields ------------------------------------
34
+ //
35
+ // `text.print(cell, s)` writes a string's characters into consecutive
36
+ // cells from `cell`, and `text.printNumber(cell, value, width)` writes
37
+ // `value` as exactly `width` decimal digits, zero-padded and right-aligned,
38
+ // so a field on a HUD never shifts columns. Both draw in the current colour —
39
+ // white until `text.setColor(TextColor.CYAN)` changes it, and that one
40
+ // call then colours everything printed after it, on the machines that have
41
+ // per-cell colour. They are also the two
42
+ // functions the compiler's template layout targets: `text.print(0,
43
+ // \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
44
+ // (see packages/compiler/src/ir).
45
+ //
46
+ // Under them: `locate()` turns a cell into a
47
+ // VERA address without dividing, and a run of text lets VERA step the
48
+ // address itself.
49
+ // `text.putChar` and `text.putColor` stay the one-cell pokes a caller can
50
+ // build anything from.
51
+ let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
52
+ let currentReverse: bool = false; // until text.setReverse() says otherwise
53
+
54
+ // ISO mode has no reverse copies of the glyphs, so invert is a swap of
55
+ // the attribute nibbles: the cell fills with the text colour and the
56
+ // glyph pixels take whatever background `screen.setColors` left. Port 1
57
+ // is already sitting on this cell's colour byte after locate().
58
+ function attributeByte(): utinyint {
59
+ let attr: utinyint = memory.read(0x9F24);
60
+ if (currentReverse) {
61
+ return (currentColor * 16) + (attr / 16);
62
+ }
63
+ return (attr & 0xF0) | currentColor;
64
+ }
65
+
66
+ // Point VERA at a cell, and hand back its column for a caller that walks
67
+ // on from there. Rows are 256 bytes apart in the map, cells two bytes —
68
+ // character, then colour (background in the high nibble, text in the low).
69
+ // No division: with 76 cells to a row and 256 = 3 * 76 + 28, a cell's row
70
+ // is three per 256 plus (28 * high byte + low byte) / 76, and that last
71
+ // quotient, on a number below 704, is ((x / 4) * 54) / 1024 exactly —
72
+ // checked for every cell. Two address ports: port 0 walks character, colour,
73
+ // character... stepping by one, and is what gets written; port 1 walks the
74
+ // colour bytes stepping by two, and is read for each cell's background
75
+ // nibble so a print keeps whatever `screen.setBackground` put there.
76
+ // Leaves ADDRSEL at 0, which screen.8bs assumes.
77
+ function locate(cell: usmallint): utinyint {
78
+ locateTextMap();
79
+ let high: utinyint = cell / 256;
80
+ let x: usmallint = high * 28 + cell % 256;
81
+ let row: utinyint = high * 3 + ((x / 4) * 54) / 1024;
82
+ let col: utinyint = cell - row * 76;
83
+ let at: usmallint = mapLow + row * 256 + col * 2;
84
+ memory.write(0x9F25, 0x01); // ADDRSEL=1
85
+ setVramAddress(mapBank, at + 1, 2);
86
+ memory.write(0x9F25, 0x00); // ADDRSEL=0
87
+ setVramAddress(mapBank, at, 1);
88
+ return col;
89
+ }
90
+
91
+ // ---- digits -------------------------------------------------------------
92
+ //
93
+ // A number is written one place at a time, high to low, by subtracting the
94
+ // place value until it no longer fits: the 6502 has no divide instruction,
95
+ // and `value / 10` would link a 250-byte routine to do it. Places above the
96
+ // field are still taken off, so a field narrower than its number shows the
97
+ // low digits; places the number does not reach print as zeros.
98
+ const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
99
+
100
+ export namespace text {
101
+ const CELL_COUNT: usmallint = 4256; // 76 columns x 56 rows, inside the inset
102
+ const COLUMNS: utinyint = 76; // cells per row, so cell = y * text.COLUMNS + x
103
+
104
+ function putChar(cell: usmallint, code: utinyint): void {
105
+ locate(cell);
106
+ memory.write(0x9F23, code);
107
+ }
108
+
109
+ function putColor(cell: usmallint, color: utinyint): void {
110
+ locate(cell);
111
+ memory.read(0x9F23); // step port 0 past the character byte
112
+ memory.write(0x9F23, (memory.read(0x9F24) & 0xF0) | (color & 0x0F));
113
+ }
114
+
115
+ function setColor(color: utinyint): void {
116
+ currentColor = color;
117
+ }
118
+
119
+ function setReverse(on: bool): void {
120
+ currentReverse = on;
121
+ }
122
+
123
+ function print(cell: usmallint, s: string): void {
124
+ let col: utinyint = locate(cell);
125
+ for (let i: utinyint = 0; i < s.length; i++) {
126
+ memory.write(0x9F23, s[i]);
127
+ memory.write(0x9F23, attributeByte());
128
+ col++;
129
+ if (col == text.COLUMNS) {
130
+ col = locate(cell + i + 1); // the next row is not the next address
131
+ }
132
+ }
133
+ }
134
+
135
+ function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
136
+ let col: utinyint = locate(cell);
137
+ let k: utinyint = width;
138
+ if (k < DIGIT_PLACES.length) {
139
+ k = DIGIT_PLACES.length;
140
+ }
141
+ while (k > 0) {
142
+ let digit: utinyint = 48; // '0'
143
+ if (k <= DIGIT_PLACES.length) {
144
+ while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
145
+ value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
146
+ digit++;
147
+ }
148
+ }
149
+ if (k <= width) {
150
+ memory.write(0x9F23, digit);
151
+ memory.write(0x9F23, attributeByte());
152
+ col++;
153
+ if (col == text.COLUMNS) {
154
+ col = locate(cell + width - k + 1); // the next row is not the next address
155
+ }
156
+ }
157
+ k--;
158
+ }
159
+ }
160
+ }
161
+
162
+ // The colours `text.setColor()` takes: the eight names every machine
163
+ // shares, with this machine's colour-RAM values.
164
+ export namespace TextColor {
165
+ const BLACK: utinyint = 0;
166
+ const WHITE: utinyint = 1;
167
+ const RED: utinyint = 2;
168
+ const CYAN: utinyint = 3;
169
+ const PURPLE: utinyint = 4;
170
+ const GREEN: utinyint = 5;
171
+ const BLUE: utinyint = 6;
172
+ const YELLOW: utinyint = 7;
173
+ }