@8bitscript/vic20 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +256 -0
- package/src/geometry.8bs +36 -0
- package/src/geometry.vic20.expanded.8bs +20 -0
- package/src/index.8bs +57 -0
- package/src/input.8bs +179 -0
- package/src/keyboard.8bs +80 -0
- package/src/keys.8bs +139 -0
- package/src/pointer.8bs +40 -0
- package/src/screen.8bs +101 -0
- package/src/text.8bs +169 -0
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,256 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@8bitscript/vic20",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VIC-20 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
|
+
"./input": "./src/input.8bs",
|
|
12
|
+
"./pointer": "./src/pointer.8bs",
|
|
13
|
+
"./keyboard": "./src/keyboard.8bs",
|
|
14
|
+
"./keys": "./src/keys.8bs"
|
|
15
|
+
},
|
|
16
|
+
"hardware": {
|
|
17
|
+
"facts": {
|
|
18
|
+
"video.columns": 22,
|
|
19
|
+
"video.rows": 23,
|
|
20
|
+
"video.cellWidth": 8,
|
|
21
|
+
"video.cellHeight": 8,
|
|
22
|
+
"video.palette": 16,
|
|
23
|
+
"video.cellColors": 2,
|
|
24
|
+
"video.colorPerCell": true,
|
|
25
|
+
"video.glyphs": 256,
|
|
26
|
+
"video.blockWidth": 2,
|
|
27
|
+
"video.blockHeight": 2,
|
|
28
|
+
"video.bitmap": false,
|
|
29
|
+
"video.layers": 1,
|
|
30
|
+
"video.scroll": false,
|
|
31
|
+
"video.sprites": 0,
|
|
32
|
+
"video.spritesPerLine": 0,
|
|
33
|
+
"video.spriteWidth": 0,
|
|
34
|
+
"video.spriteHeight": 0,
|
|
35
|
+
"video.spriteColors": 0,
|
|
36
|
+
"video.frameRate": 60,
|
|
37
|
+
"audio.voices": 4,
|
|
38
|
+
"audio.noise": true,
|
|
39
|
+
"audio.envelope": false,
|
|
40
|
+
"audio.filter": false,
|
|
41
|
+
"audio.pcm": false,
|
|
42
|
+
"audio.volume": false,
|
|
43
|
+
"audio.entropy": false,
|
|
44
|
+
"input.keyboard": true,
|
|
45
|
+
"input.joysticks": 1,
|
|
46
|
+
"input.pads": 0,
|
|
47
|
+
"input.mouse": false,
|
|
48
|
+
"input.paddles": false,
|
|
49
|
+
"storage.save": true,
|
|
50
|
+
"memory.ram": 3583,
|
|
51
|
+
"memory.banked": false,
|
|
52
|
+
"memory.bankedKib": 0,
|
|
53
|
+
"storage.kib": 164
|
|
54
|
+
},
|
|
55
|
+
"options": {
|
|
56
|
+
"ram": {
|
|
57
|
+
"label": "RAM expansion",
|
|
58
|
+
"default": "none",
|
|
59
|
+
"values": {
|
|
60
|
+
"none": {
|
|
61
|
+
"label": "Unexpanded (3583 bytes for the program)",
|
|
62
|
+
"build": {
|
|
63
|
+
"defsym": {
|
|
64
|
+
"__memory_expansion": 0
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"run": {
|
|
68
|
+
"xvic": [
|
|
69
|
+
"-memory",
|
|
70
|
+
"none"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"facts": {
|
|
74
|
+
"memory.ram": 3583
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"3k": {
|
|
78
|
+
"label": "3K (VIC-1210): $0400-$0FFF",
|
|
79
|
+
"tag": "3k",
|
|
80
|
+
"build": {
|
|
81
|
+
"defsym": {
|
|
82
|
+
"__memory_expansion": 3
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"run": {
|
|
86
|
+
"xvic": [
|
|
87
|
+
"-memory",
|
|
88
|
+
"3k"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"facts": {
|
|
92
|
+
"memory.ram": 6655
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"8k": {
|
|
96
|
+
"label": "8K: screen moves to $1000",
|
|
97
|
+
"tag": "expanded",
|
|
98
|
+
"build": {
|
|
99
|
+
"defsym": {
|
|
100
|
+
"__memory_expansion": 8
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"run": {
|
|
104
|
+
"xvic": [
|
|
105
|
+
"-memory",
|
|
106
|
+
"8k"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"facts": {
|
|
110
|
+
"memory.ram": 11775
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"16k": {
|
|
114
|
+
"label": "16K: screen moves to $1000",
|
|
115
|
+
"tag": "expanded",
|
|
116
|
+
"build": {
|
|
117
|
+
"defsym": {
|
|
118
|
+
"__memory_expansion": 16
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"run": {
|
|
122
|
+
"xvic": [
|
|
123
|
+
"-memory",
|
|
124
|
+
"16k"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"facts": {
|
|
128
|
+
"memory.ram": 19967
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"24k": {
|
|
132
|
+
"label": "24K: screen moves to $1000",
|
|
133
|
+
"tag": "expanded",
|
|
134
|
+
"build": {
|
|
135
|
+
"defsym": {
|
|
136
|
+
"__memory_expansion": 24
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"run": {
|
|
140
|
+
"xvic": [
|
|
141
|
+
"-memory",
|
|
142
|
+
"24k"
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
"facts": {
|
|
146
|
+
"memory.ram": 28159
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"port1": {
|
|
152
|
+
"label": "Control port 1",
|
|
153
|
+
"default": "joystick",
|
|
154
|
+
"values": {
|
|
155
|
+
"none": {
|
|
156
|
+
"label": "Nothing plugged in",
|
|
157
|
+
"run": {
|
|
158
|
+
"xvic": [
|
|
159
|
+
"-controlport1device",
|
|
160
|
+
"0"
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"joystick": {
|
|
165
|
+
"label": "Joystick",
|
|
166
|
+
"run": {
|
|
167
|
+
"xvic": [
|
|
168
|
+
"-controlport1device",
|
|
169
|
+
"1"
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"paddles": {
|
|
174
|
+
"label": "Paddles",
|
|
175
|
+
"run": {
|
|
176
|
+
"xvic": [
|
|
177
|
+
"-controlport1device",
|
|
178
|
+
"2"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
"facts": {
|
|
182
|
+
"input.paddles": true
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"mouse1351": {
|
|
186
|
+
"label": "Commodore 1351 mouse",
|
|
187
|
+
"run": {
|
|
188
|
+
"xvic": [
|
|
189
|
+
"-controlport1device",
|
|
190
|
+
"3",
|
|
191
|
+
"-mouse"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
"facts": {
|
|
195
|
+
"input.mouse": true
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"drive": {
|
|
201
|
+
"label": "Disk drive",
|
|
202
|
+
"default": "1541",
|
|
203
|
+
"values": {
|
|
204
|
+
"1541": {
|
|
205
|
+
"label": "1541 (170K disk)",
|
|
206
|
+
"facts": {
|
|
207
|
+
"storage.save": true,
|
|
208
|
+
"storage.kib": 164
|
|
209
|
+
},
|
|
210
|
+
"tag": null
|
|
211
|
+
},
|
|
212
|
+
"1581": {
|
|
213
|
+
"label": "1581 (800K disk)",
|
|
214
|
+
"facts": {
|
|
215
|
+
"storage.save": true,
|
|
216
|
+
"storage.kib": 783
|
|
217
|
+
},
|
|
218
|
+
"tag": null
|
|
219
|
+
},
|
|
220
|
+
"none": {
|
|
221
|
+
"label": "No drive (tape or nothing)",
|
|
222
|
+
"facts": {
|
|
223
|
+
"storage.save": false,
|
|
224
|
+
"storage.kib": 0
|
|
225
|
+
},
|
|
226
|
+
"tag": null
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
"presets": {
|
|
232
|
+
"unexpanded": {
|
|
233
|
+
"ram": "none"
|
|
234
|
+
},
|
|
235
|
+
"3k": {
|
|
236
|
+
"ram": "3k"
|
|
237
|
+
},
|
|
238
|
+
"8k": {
|
|
239
|
+
"ram": "8k"
|
|
240
|
+
},
|
|
241
|
+
"16k": {
|
|
242
|
+
"ram": "16k"
|
|
243
|
+
},
|
|
244
|
+
"24k": {
|
|
245
|
+
"ram": "24k"
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"files": [
|
|
251
|
+
"src"
|
|
252
|
+
],
|
|
253
|
+
"publishConfig": {
|
|
254
|
+
"access": "public"
|
|
255
|
+
}
|
|
256
|
+
}
|
package/src/geometry.8bs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @8bitscript/vic20 — where the screen is, for the unexpanded and 3K VIC-20.
|
|
2
|
+
//
|
|
3
|
+
// The VIC-20's screen moves with its memory. Unexpanded (and with a 3K
|
|
4
|
+
// expansion, which fills $0400-$0FFF and leaves everything above alone)
|
|
5
|
+
// the KERNAL puts the 22x23 screen matrix at $1E00 with its colour nybbles
|
|
6
|
+
// at $9600, and BASIC starts at $1001. With 8K or more the KERNAL moves the
|
|
7
|
+
// screen down to $1000 and colour RAM to $9400, so that $1200 upward is one
|
|
8
|
+
// contiguous run of BASIC RAM — which is why the SDK loads an 8K+ program at
|
|
9
|
+
// $1201 (mos-platform/vic20/lib/link.ld). This file is the unexpanded/3K
|
|
10
|
+
// geometry; `geometry.vic20.expanded.8bs` beside it is the 8K-and-up one,
|
|
11
|
+
// read by a build whose hardware carries the `expanded` tag — the tag the
|
|
12
|
+
// package's catalog gives the 8k, 16k and 24k values of its `ram` option
|
|
13
|
+
// (package.json, "8bitscript".hardware), one word for the one thing they
|
|
14
|
+
// share. text.8bs and screen.8bs are written once, against these numbers
|
|
15
|
+
// (docs/packages.md, "System-specific files").
|
|
16
|
+
//
|
|
17
|
+
// Two of these are the VIC's, not the KERNAL's, and the package writes one
|
|
18
|
+
// of them itself: MEMORY_POINTER_UPPERCASE is what text.8bs stores in $9005
|
|
19
|
+
// before every run of text — bits 4-7 the screen base, bits 0-3 the
|
|
20
|
+
// upper-case character ROM at $8000 — $F0 for a screen at $1E00, $C0 for
|
|
21
|
+
// one at $1000 (POKE 36869,240 and POKE 36869,192 in the Programmer's
|
|
22
|
+
// Reference Guide). The screen's address bit 9 and the colour RAM base
|
|
23
|
+
// both come from $9002 bit 7, which the package leaves as the KERNAL set
|
|
24
|
+
// it at boot from the RAM it found. So the profile has to match the
|
|
25
|
+
// machine: a `--profile 8k` build on an unexpanded VIC-20 would write $C0
|
|
26
|
+
// to a VIC whose $9002 still says $1E00, and draw nowhere useful. `8bs run`
|
|
27
|
+
// launches xvic with the matching `-memory`, so the emulator always agrees.
|
|
28
|
+
|
|
29
|
+
export namespace Video {
|
|
30
|
+
const SCREEN: usmallint = 0x1E00; // screen matrix: one screen code per cell
|
|
31
|
+
const COLOR: usmallint = 0x9600; // colour RAM: one nybble per cell, same cell index
|
|
32
|
+
const MEMORY_POINTER_UPPERCASE: utinyint = 0xF0; // $9005: this screen, upper-case ROM
|
|
33
|
+
const COLUMNS: utinyint = 22;
|
|
34
|
+
const ROWS: utinyint = 23;
|
|
35
|
+
const CELL_COUNT: usmallint = 506; // COLUMNS * ROWS
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @8bitscript/vic20 — where the screen is, with 8K or more of expansion.
|
|
2
|
+
//
|
|
3
|
+
// The `expanded` version of geometry.8bs (see that file for the whole
|
|
4
|
+
// story): with 8K or more the KERNAL moves the screen matrix to $1000 and
|
|
5
|
+
// colour RAM to $9400 so BASIC RAM runs unbroken from $1200, and the SDK
|
|
6
|
+
// loads the program at $1201. $9005's screen nybble becomes $C0 (POKE
|
|
7
|
+
// 36869,192). One file serves the 8k, 16k and 24k values of the `ram`
|
|
8
|
+
// option because the catalog (package.json, "8bitscript".hardware) gives
|
|
9
|
+
// all three the same `expanded` tag: where the screen is depends on
|
|
10
|
+
// whether there is 8K or more, not on how much more, and a tag names
|
|
11
|
+
// exactly the thing a file differs on.
|
|
12
|
+
|
|
13
|
+
export namespace Video {
|
|
14
|
+
const SCREEN: usmallint = 0x1000; // screen matrix: one screen code per cell
|
|
15
|
+
const COLOR: usmallint = 0x9400; // colour RAM: one nybble per cell, same cell index
|
|
16
|
+
const MEMORY_POINTER_UPPERCASE: utinyint = 0xC0; // $9005: this screen, upper-case ROM
|
|
17
|
+
const COLUMNS: utinyint = 22;
|
|
18
|
+
const ROWS: utinyint = 23;
|
|
19
|
+
const CELL_COUNT: usmallint = 506; // COLUMNS * ROWS
|
|
20
|
+
}
|
package/src/index.8bs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @8bitscript/vic20 — VIC-20 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 { vicColor, memoryPointer } from "@8bitscript/vic20";
|
|
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. A project that imports THIS
|
|
14
|
+
// package has declared itself VIC-20 specific.
|
|
15
|
+
|
|
16
|
+
// $900F, the VIC chip's screen/colour register. Bits verified against the
|
|
17
|
+
// VIC-20's own hardware map, not inferred from a BASIC POKE example:
|
|
18
|
+
//
|
|
19
|
+
// bits 4-7 background colour (16 values)
|
|
20
|
+
// bit 3 normal/inverted video (1 = normal)
|
|
21
|
+
// bits 0-2 border colour (8 values — the VIC only gives border 3 bits)
|
|
22
|
+
@address(0x900F)
|
|
23
|
+
export let vicColor: volatile<u8>;
|
|
24
|
+
|
|
25
|
+
// $9005, the VIC's memory pointer (`addr` in llvm-mos-sdk's _vic.h): bits
|
|
26
|
+
// 4-7 the screen base, bits 0-3 the character set base. $F0 is screen $1E00
|
|
27
|
+
// with the upper-case/graphics character ROM at $8000 — the power-on value
|
|
28
|
+
// (POKE 36869,240 in the Programmer's Reference Guide), where $F2 is the
|
|
29
|
+
// lower-case ROM at $8800.
|
|
30
|
+
@address(0x9005)
|
|
31
|
+
export let memoryPointer: volatile<u8>;
|
|
32
|
+
|
|
33
|
+
// ---- the VIAs ($9110, $9120): the control port and the keyboard -------------
|
|
34
|
+
//
|
|
35
|
+
// The VIC-20 splits one joystick across two chips, which is the single
|
|
36
|
+
// most surprising thing about input on this machine. VIA1's port A carries
|
|
37
|
+
// up, down, left and fire; **right is VIA2 port B bit 7** — a line that is
|
|
38
|
+
// otherwise a keyboard *column output*. So reading it means turning that
|
|
39
|
+
// one bit around to an input and back, which is what
|
|
40
|
+
// `@8bitscript/vic20/input` does and why it cannot simply read a port.
|
|
41
|
+
//
|
|
42
|
+
// The keyboard is VIA2: port B selects columns (outputs, DDRB $FF at boot)
|
|
43
|
+
// and port A reads the rows of the selected column (inputs, DDRA $00).
|
|
44
|
+
//
|
|
45
|
+
// Verified against VICE's `vic20via1.c` and `vic20via2.c` — see
|
|
46
|
+
// ../AGENTS.md, which records the port bits and the boot-time direction
|
|
47
|
+
// registers this package relies on.
|
|
48
|
+
@address(0x9111)
|
|
49
|
+
export let via1PortA: volatile<u8>;
|
|
50
|
+
@address(0x9120)
|
|
51
|
+
export let via2PortB: volatile<u8>;
|
|
52
|
+
@address(0x9121)
|
|
53
|
+
export let via2PortA: volatile<u8>;
|
|
54
|
+
@address(0x9122)
|
|
55
|
+
export let via2DirectionB: volatile<u8>;
|
|
56
|
+
@address(0x9123)
|
|
57
|
+
export let via2DirectionA: volatile<u8>;
|
package/src/input.8bs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// @8bitscript/vic20/input — the VIC-20 behind @8bitscript/input.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./input"] in this package's package.json,
|
|
4
|
+
// and by "8bitscript".entry.vic20 in @8bitscript/input's, so a portable
|
|
5
|
+
// program writes
|
|
6
|
+
//
|
|
7
|
+
// import { input } from "@8bitscript/input";
|
|
8
|
+
//
|
|
9
|
+
// and gets this file on a VIC-20. The machine-level layers underneath —
|
|
10
|
+
// ./keyboard.8bs, ./keys.8bs — stay available to a program that wants the
|
|
11
|
+
// matrix itself.
|
|
12
|
+
//
|
|
13
|
+
// ---- one joystick split across two chips ---------------------------------
|
|
14
|
+
//
|
|
15
|
+
// This is the machine where reading a joystick is not reading a port. Up,
|
|
16
|
+
// down, left and fire are VIA1 port A ($9111) bits 2, 3, 4 and 5 — a 0 bit
|
|
17
|
+
// where a switch is closed, as everywhere. **Right is VIA2 port B bit 7**
|
|
18
|
+
// ($9120), which is otherwise a keyboard *column output*. So reading right
|
|
19
|
+
// means turning that one bit around to an input, reading it, and putting
|
|
20
|
+
// it back an output before anything else touches the keyboard lines — and
|
|
21
|
+
// it has to happen *after* `keyboard.scan()` below, which is the thing
|
|
22
|
+
// actually driving those seven other columns each frame; read the joystick
|
|
23
|
+
// first and scan() would find every column selected but the last, and read
|
|
24
|
+
// "right" as though nothing else had touched the port.
|
|
25
|
+
//
|
|
26
|
+
// Verified against VICE's `vic20via1.c` and `vic20via2.c`; ../AGENTS.md
|
|
27
|
+
// records the bits and the boot-time direction registers.
|
|
28
|
+
//
|
|
29
|
+
// ---- the keyboard, and the cursor keys' two directions --------------------
|
|
30
|
+
//
|
|
31
|
+
// Like the C64, this keyboard has two cursor keys, not four:
|
|
32
|
+
// `CURSOR_LEFT_RIGHT` and `CURSOR_UP_DOWN`, with SHIFT reversing each — so
|
|
33
|
+
// left is SHIFT plus the right key, and up is SHIFT plus the down key,
|
|
34
|
+
// which is what the machine actually has rather than four keys it does
|
|
35
|
+
// not. `Key`'s table (./keys.8bs) is checked key for key against VICE's
|
|
36
|
+
// own positional keyboard map (packages/compiler/test/vic20-keys.test.mjs)
|
|
37
|
+
// rather than recalled from memory, per the root AGENTS.md's rule to
|
|
38
|
+
// verify hardware facts before writing them down.
|
|
39
|
+
//
|
|
40
|
+
// **A mouse.** `--hardware port1=mouse1351` sets `input.mouse` on this
|
|
41
|
+
// machine, but a 1351 here would read through the VIC's own analogue lines
|
|
42
|
+
// at $9008/$9009, not a SID's — a different driver from the C64's, and one
|
|
43
|
+
// nobody has written or measured. `pointer()` is false rather than
|
|
44
|
+
// hopeful.
|
|
45
|
+
//
|
|
46
|
+
// ---- edges, not levels ---------------------------------------------------
|
|
47
|
+
//
|
|
48
|
+
// Every answer is edge-triggered: true on the one frame the press begins.
|
|
49
|
+
// `poll()` once a frame, right after waitFrame().
|
|
50
|
+
import { via1PortA, via2PortB, via2DirectionB } from "./index.8bs";
|
|
51
|
+
import { keyboard } from "./keyboard.8bs";
|
|
52
|
+
import { Key } from "./keys.8bs";
|
|
53
|
+
|
|
54
|
+
// VIA1 port A's four switches, and the odd one out on VIA2.
|
|
55
|
+
const JOY_UP: utinyint = 4;
|
|
56
|
+
const JOY_DOWN: utinyint = 8;
|
|
57
|
+
const JOY_LEFT: utinyint = 16;
|
|
58
|
+
const JOY_FIRE: utinyint = 32;
|
|
59
|
+
const JOY_RIGHT: utinyint = 128; // VIA2 port B bit 7
|
|
60
|
+
|
|
61
|
+
namespace Edge {
|
|
62
|
+
const LEFT: utinyint = 1;
|
|
63
|
+
const RIGHT: utinyint = 2;
|
|
64
|
+
const UP: utinyint = 4;
|
|
65
|
+
const DOWN: utinyint = 8;
|
|
66
|
+
const CONFIRM: utinyint = 16;
|
|
67
|
+
const CANCEL: utinyint = 32;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let held: utinyint = 0;
|
|
71
|
+
let before: utinyint = 0;
|
|
72
|
+
let began: utinyint = 0;
|
|
73
|
+
|
|
74
|
+
export namespace input {
|
|
75
|
+
|
|
76
|
+
// Nothing to set up: no pointer to give a ceiling to.
|
|
77
|
+
function begin(): void {
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function poll(): void {
|
|
81
|
+
// First, so the other seven keyboard columns are already driven
|
|
82
|
+
// for this frame before the joystick-right read below borrows
|
|
83
|
+
// bit 7 of the same port.
|
|
84
|
+
keyboard.scan();
|
|
85
|
+
|
|
86
|
+
// The four on VIA1, inverted once so a 1 bit means pushed.
|
|
87
|
+
let stick: utinyint = via1PortA ^ 0xFF;
|
|
88
|
+
|
|
89
|
+
// Right, on a line that is normally a keyboard column output.
|
|
90
|
+
// Turn bit 7 around, read it, put it back — leaving the other
|
|
91
|
+
// seven columns exactly as they were, because the KERNAL is not
|
|
92
|
+
// the only thing that drives them.
|
|
93
|
+
via2DirectionB = via2DirectionB & 0x7F;
|
|
94
|
+
let rightPushed: bool = (via2PortB & JOY_RIGHT) == 0;
|
|
95
|
+
via2DirectionB = via2DirectionB | 0x80;
|
|
96
|
+
|
|
97
|
+
before = held;
|
|
98
|
+
held = 0;
|
|
99
|
+
|
|
100
|
+
if ((stick & JOY_LEFT) != 0) {
|
|
101
|
+
held = held | Edge.LEFT;
|
|
102
|
+
}
|
|
103
|
+
if (rightPushed) {
|
|
104
|
+
held = held | Edge.RIGHT;
|
|
105
|
+
}
|
|
106
|
+
if ((stick & JOY_UP) != 0) {
|
|
107
|
+
held = held | Edge.UP;
|
|
108
|
+
}
|
|
109
|
+
if ((stick & JOY_DOWN) != 0) {
|
|
110
|
+
held = held | Edge.DOWN;
|
|
111
|
+
}
|
|
112
|
+
if ((stick & JOY_FIRE) != 0) {
|
|
113
|
+
held = held | Edge.CONFIRM;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The two cursor keys and SHIFT, which is what "four directions"
|
|
117
|
+
// is on this keyboard — or'd on top of the joystick, same as the
|
|
118
|
+
// C64: a program should not care which the user reached for.
|
|
119
|
+
let shifted: bool = keyboard.pressed(Key.SHIFT_LEFT) || keyboard.pressed(Key.SHIFT_RIGHT);
|
|
120
|
+
if (keyboard.pressed(Key.CURSOR_LEFT_RIGHT)) {
|
|
121
|
+
if (shifted) {
|
|
122
|
+
held = held | Edge.LEFT;
|
|
123
|
+
} else {
|
|
124
|
+
held = held | Edge.RIGHT;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (keyboard.pressed(Key.CURSOR_UP_DOWN)) {
|
|
128
|
+
if (shifted) {
|
|
129
|
+
held = held | Edge.UP;
|
|
130
|
+
} else {
|
|
131
|
+
held = held | Edge.DOWN;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (keyboard.pressed(Key.RETURN)) {
|
|
135
|
+
held = held | Edge.CONFIRM;
|
|
136
|
+
}
|
|
137
|
+
if (keyboard.pressed(Key.RUN_STOP)) {
|
|
138
|
+
held = held | Edge.CANCEL;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
began = held & (before ^ 0xFF);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function left(): bool {
|
|
145
|
+
return (began & Edge.LEFT) != 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function right(): bool {
|
|
149
|
+
return (began & Edge.RIGHT) != 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function up(): bool {
|
|
153
|
+
return (began & Edge.UP) != 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function down(): bool {
|
|
157
|
+
return (began & Edge.DOWN) != 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function confirm(): bool {
|
|
161
|
+
return (began & Edge.CONFIRM) != 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function cancel(): bool {
|
|
165
|
+
return (began & Edge.CANCEL) != 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function pointer(): bool {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function pointerCell(): usmallint {
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function pointerButton(): bool {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/keyboard.8bs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @8bitscript/vic20/keyboard — the VIC-20's keyboard matrix, one snapshot a
|
|
2
|
+
// frame.
|
|
3
|
+
//
|
|
4
|
+
// Named by "8bitscript".exports["./keyboard"] in this package's package.json:
|
|
5
|
+
//
|
|
6
|
+
// import { keyboard } from "@8bitscript/vic20/keyboard";
|
|
7
|
+
// import { Key } from "@8bitscript/vic20/keys";
|
|
8
|
+
//
|
|
9
|
+
// This is hardware-level, VIC-20-only surface — the layer the portable
|
|
10
|
+
// input capability is built on: `@8bitscript/vic20/input` reads this file
|
|
11
|
+
// and @8bitscript/input resolves to that, the way ./text.8bs is the layer
|
|
12
|
+
// under @8bitscript/text. Importing it makes a program VIC-20-specific.
|
|
13
|
+
// It keeps no key buffer and decodes no PETSCII; it answers "is this key
|
|
14
|
+
// down now", which is what a game loop asks.
|
|
15
|
+
//
|
|
16
|
+
// ---- how the matrix works -----------------------------------------------
|
|
17
|
+
//
|
|
18
|
+
// Eight columns by eight rows, on VIA2 alone — ../index.8bs and
|
|
19
|
+
// ../AGENTS.md (verified against VICE's `vic20via2.c`): port B selects a
|
|
20
|
+
// column (DDRB $FF at boot, all eight lines outputs), port A reads that
|
|
21
|
+
// column's eight keys (DDRA $00, inputs), a 0 bit where a key is down.
|
|
22
|
+
// `scan()` writes both direction registers itself, so it does not depend
|
|
23
|
+
// on what ran before, and drives every column exactly once — the KERNAL's
|
|
24
|
+
// own IRQ scan (packages/vic20/AGENTS.md: a free-running ~60 Hz timer, not
|
|
25
|
+
// locked to the frame) would otherwise land its own column select between
|
|
26
|
+
// this scan's write and read, which is one reason the frame runtime
|
|
27
|
+
// starts a VIC-20 program with interrupts held off.
|
|
28
|
+
//
|
|
29
|
+
// `scan()` reads all eight columns into `state`, once a frame, right after
|
|
30
|
+
// waitFrame() returns; every question after that reads the snapshot. It
|
|
31
|
+
// leaves port B at $FF — no column selected — which is the state
|
|
32
|
+
// @8bitscript/vic20/input's joystick-right read needs: VIA2 port B bit 7
|
|
33
|
+
// doubles as that line, and turning it around to an input reads correctly
|
|
34
|
+
// only once the keyboard scan is no longer driving it low for some other
|
|
35
|
+
// column. Call `keyboard.scan()` first and read the joystick second, both
|
|
36
|
+
// once a frame.
|
|
37
|
+
//
|
|
38
|
+
// Ghosting is the same hazard it is on every unshifted matrix: three keys
|
|
39
|
+
// down can read as four. Two keys are always safe.
|
|
40
|
+
import { via2PortA, via2PortB, via2DirectionA, via2DirectionB } from "./index.8bs";
|
|
41
|
+
|
|
42
|
+
// The last scan, one byte per column, a 1 bit where a key is down (the
|
|
43
|
+
// port's active-low rows inverted once, here, so nothing else has to
|
|
44
|
+
// think about it). All zero until the first scan.
|
|
45
|
+
let state: array<utinyint, 8>;
|
|
46
|
+
|
|
47
|
+
// The column-select byte for each column, and the row masks — indexed
|
|
48
|
+
// rather than shifted, as `1 << n` with a variable n is a loop on the 6502.
|
|
49
|
+
const COLUMN_SELECT: array<utinyint, 8> = [254, 253, 251, 247, 239, 223, 191, 127];
|
|
50
|
+
const ROW_BIT: array<utinyint, 8> = [1, 2, 4, 8, 16, 32, 64, 128];
|
|
51
|
+
|
|
52
|
+
export namespace keyboard {
|
|
53
|
+
const COLUMNS: utinyint = 8;
|
|
54
|
+
const ROWS: utinyint = 8;
|
|
55
|
+
|
|
56
|
+
// Read all eight columns into the snapshot. Once a frame, right after
|
|
57
|
+
// waitFrame(), before reading the joystick's right line.
|
|
58
|
+
function scan(): void {
|
|
59
|
+
via2DirectionB = 0xFF;
|
|
60
|
+
via2DirectionA = 0x00;
|
|
61
|
+
for (let select: utinyint = 0; select < 8; select++) {
|
|
62
|
+
via2PortB = COLUMN_SELECT[select];
|
|
63
|
+
state[select] = via2PortA ^ 0xFF;
|
|
64
|
+
}
|
|
65
|
+
via2PortB = 0xFF;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Is this key down in the last scan? `key` is a `Key.*` value from
|
|
69
|
+
// @8bitscript/vic20/keys — `select * 8 + row` in the matrix.
|
|
70
|
+
function pressed(key: utinyint): bool {
|
|
71
|
+
return (state[key >> 3] & ROW_BIT[key & 7]) != 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The last scan's eight rows of one column, a 1 bit per key down:
|
|
75
|
+
// for code that wants a whole column at once (an "any key" test over
|
|
76
|
+
// all eight, a ghosting check).
|
|
77
|
+
function column(index: utinyint): utinyint {
|
|
78
|
+
return state[index];
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/keys.8bs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// @8bitscript/vic20/keys — the keyboard matrix, key by key.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./keys"] in this package's package.json, for
|
|
4
|
+
//
|
|
5
|
+
// import { keyboard } from "@8bitscript/vic20/keyboard";
|
|
6
|
+
// import { Key } from "@8bitscript/vic20/keys";
|
|
7
|
+
//
|
|
8
|
+
// if (keyboard.pressed(Key.SPACE)) { ... }
|
|
9
|
+
//
|
|
10
|
+
// Each value is `select * 8 + row` in VIA2's matrix (see ./keyboard.8bs):
|
|
11
|
+
// `select` is which of VIA2 port B's eight column-select lines was driven
|
|
12
|
+
// low to reach this key, `row` the bit of VIA2 port A that reads low —
|
|
13
|
+
// ../AGENTS.md, "Keyboard: VIA2 port B selects columns (output), port A
|
|
14
|
+
// reads rows against the selected columns" (verified there against
|
|
15
|
+
// vic20via2.c `read_pra`: port B is the column mask, `rev_keyarr[column]`
|
|
16
|
+
// the row bits). One matrix on every VIC-20 ever made, expanded or not.
|
|
17
|
+
//
|
|
18
|
+
// Source: the matrix diagram in VICE's own vic20/gtk3_pos.vkm (reproduced
|
|
19
|
+
// below), checked here key for key against that file,
|
|
20
|
+
// /opt/homebrew/share/vice/VIC20/gtk3_pos.vkm. Each line there is
|
|
21
|
+
// `keysym row column shiftflag`: VICE's first number is the **row** (VIA2
|
|
22
|
+
// port A bit), its second the **column** (VIA2 port B bit). `keyboard.scan()`
|
|
23
|
+
// indexes `state[column]` with row bits, so a Key is `column * 8 + row`,
|
|
24
|
+
// not `row * 8 + column`. The C64 table can use VICE's pair in file order
|
|
25
|
+
// because that machine's gtk3_pos.vkm writes the *column* first; copying
|
|
26
|
+
// that convention here looked up every key in the transposed cell
|
|
27
|
+
// (2048 on xvic, 2026-09-07: host Right lit port-B column 2 / port-A bit 7
|
|
28
|
+
// — Key 23 — while this table named that position 58).
|
|
29
|
+
// packages/compiler/test/vic20-keys.test.mjs repeats the check when the
|
|
30
|
+
// keymap is present.
|
|
31
|
+
//
|
|
32
|
+
// +------+------+------+------+------+------+------+------+
|
|
33
|
+
// |sel. 0|sel. 1|sel. 2|sel. 3|sel. 4|sel. 5|sel. 6|sel. 7|
|
|
34
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
35
|
+
// |row 0| 1 ! |A_LEFT| CTRL | R/S |SPACE | C= | Q | 2 " |
|
|
36
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
37
|
+
// |row 1| 3 # | W | A |S_LEFT| Z | S | E | 4 $ |
|
|
38
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
39
|
+
// |row 2| 5 % | R | D | X | C | F | T | 6 & |
|
|
40
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
41
|
+
// |row 3| 7 ' | Y | G | V | B | H | U | 8 ( |
|
|
42
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
43
|
+
// |row 4| 9 ) | I | J | N | M | K | O | 0 |
|
|
44
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
45
|
+
// |row 5| + | P | L | , < | . > | : [ | @ | - |
|
|
46
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
47
|
+
// |row 6|POUND | * | ; ] | / ? |S_RGHT| = |A_UP | HOME |
|
|
48
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
49
|
+
// |row 7| DEL |RETURN|C_L/R |C_U/D | F1 | F3 | F5 | F7 |
|
|
50
|
+
// +-----+------+------+------+------+------+------+------+------+
|
|
51
|
+
//
|
|
52
|
+
// Two keys are not in it: RESTORE is wired to the CPU's NMI line (VIA1's
|
|
53
|
+
// CA1, ../AGENTS.md), and SHIFT LOCK is the left SHIFT key's own wire,
|
|
54
|
+
// held down (VICE's `Caps_Lock` maps to the same position as `Shift_L`).
|
|
55
|
+
//
|
|
56
|
+
// A key that also exists on the C64 or PET has the same name here —
|
|
57
|
+
// letters, `DIGIT_n`, SPACE, RETURN, HOME, DELETE, both shifts, the arrows
|
|
58
|
+
// and the shared punctuation — so a program that sticks to those names
|
|
59
|
+
// reads the same across the Commodores it runs on. `CURSOR_LEFT_RIGHT` and
|
|
60
|
+
// `CURSOR_UP_DOWN` are named for what this keyboard actually has: one key
|
|
61
|
+
// per pair, not four, with SHIFT reversing each — the same shape as the
|
|
62
|
+
// C64's `CURSOR_RIGHT`/`CURSOR_DOWN`, under different names because they
|
|
63
|
+
// are not the same two directions unshifted (this machine's un-shifted
|
|
64
|
+
// pair is right and down, matching the C64's).
|
|
65
|
+
|
|
66
|
+
export namespace Key {
|
|
67
|
+
// select 0 — VIA2 port B bit 0
|
|
68
|
+
const DIGIT_1: utinyint = 0;
|
|
69
|
+
const DIGIT_3: utinyint = 1;
|
|
70
|
+
const DIGIT_5: utinyint = 2;
|
|
71
|
+
const DIGIT_7: utinyint = 3;
|
|
72
|
+
const DIGIT_9: utinyint = 4;
|
|
73
|
+
const PLUS: utinyint = 5;
|
|
74
|
+
const POUND: utinyint = 6; // £
|
|
75
|
+
const DELETE: utinyint = 7; // INST/DEL
|
|
76
|
+
// select 1
|
|
77
|
+
const LEFT_ARROW: utinyint = 8; // the "<-" key left of DIGIT_1, not a cursor key
|
|
78
|
+
const W: utinyint = 9;
|
|
79
|
+
const R: utinyint = 10;
|
|
80
|
+
const Y: utinyint = 11;
|
|
81
|
+
const I: utinyint = 12;
|
|
82
|
+
const P: utinyint = 13;
|
|
83
|
+
const ASTERISK: utinyint = 14;
|
|
84
|
+
const RETURN: utinyint = 15;
|
|
85
|
+
// select 2
|
|
86
|
+
const CTRL: utinyint = 16;
|
|
87
|
+
const A: utinyint = 17;
|
|
88
|
+
const D: utinyint = 18;
|
|
89
|
+
const G: utinyint = 19;
|
|
90
|
+
const J: utinyint = 20;
|
|
91
|
+
const L: utinyint = 21;
|
|
92
|
+
const SEMICOLON: utinyint = 22;
|
|
93
|
+
const CURSOR_LEFT_RIGHT: utinyint = 23; // unshifted: right; shifted: left
|
|
94
|
+
// select 3
|
|
95
|
+
const RUN_STOP: utinyint = 24;
|
|
96
|
+
const SHIFT_LEFT: utinyint = 25; // also SHIFT LOCK
|
|
97
|
+
const X: utinyint = 26;
|
|
98
|
+
const V: utinyint = 27;
|
|
99
|
+
const N: utinyint = 28;
|
|
100
|
+
const COMMA: utinyint = 29;
|
|
101
|
+
const SLASH: utinyint = 30;
|
|
102
|
+
const CURSOR_UP_DOWN: utinyint = 31; // unshifted: down; shifted: up
|
|
103
|
+
// select 4
|
|
104
|
+
const SPACE: utinyint = 32;
|
|
105
|
+
const Z: utinyint = 33;
|
|
106
|
+
const C: utinyint = 34;
|
|
107
|
+
const B: utinyint = 35;
|
|
108
|
+
const M: utinyint = 36;
|
|
109
|
+
const PERIOD: utinyint = 37;
|
|
110
|
+
const SHIFT_RIGHT: utinyint = 38;
|
|
111
|
+
const F1: utinyint = 39; // shifted: F2
|
|
112
|
+
// select 5
|
|
113
|
+
const COMMODORE: utinyint = 40; // the C= key
|
|
114
|
+
const S: utinyint = 41;
|
|
115
|
+
const F: utinyint = 42;
|
|
116
|
+
const H: utinyint = 43;
|
|
117
|
+
const K: utinyint = 44;
|
|
118
|
+
const COLON: utinyint = 45;
|
|
119
|
+
const EQUALS: utinyint = 46;
|
|
120
|
+
const F3: utinyint = 47; // shifted: F4
|
|
121
|
+
// select 6
|
|
122
|
+
const Q: utinyint = 48;
|
|
123
|
+
const E: utinyint = 49;
|
|
124
|
+
const T: utinyint = 50;
|
|
125
|
+
const U: utinyint = 51;
|
|
126
|
+
const O: utinyint = 52;
|
|
127
|
+
const AT: utinyint = 53;
|
|
128
|
+
const UP_ARROW: utinyint = 54;
|
|
129
|
+
const F5: utinyint = 55; // shifted: F6
|
|
130
|
+
// select 7
|
|
131
|
+
const DIGIT_2: utinyint = 56;
|
|
132
|
+
const DIGIT_4: utinyint = 57;
|
|
133
|
+
const DIGIT_6: utinyint = 58;
|
|
134
|
+
const DIGIT_8: utinyint = 59;
|
|
135
|
+
const DIGIT_0: utinyint = 60;
|
|
136
|
+
const MINUS: utinyint = 61;
|
|
137
|
+
const HOME: utinyint = 62; // CLR/HOME
|
|
138
|
+
const F7: utinyint = 63; // shifted: F8
|
|
139
|
+
}
|
package/src/pointer.8bs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @8bitscript/vic20/pointer — the VIC-20 behind @8bitscript/pointer.
|
|
2
|
+
//
|
|
3
|
+
// Every call here does nothing and `DRAWS` is false, and on this machine
|
|
4
|
+
// that is the hardware's answer: **the VIC has no sprites.** There are no
|
|
5
|
+
// movable objects to put an arrow in, so a cursor here would have to be a
|
|
6
|
+
// character cell — which means putting back the cell it covered, and
|
|
7
|
+
// @8bitscript/text can write a cell but not read one, so it cannot. A
|
|
8
|
+
// redefined glyph moved a cell at a time is the shape such a cursor would
|
|
9
|
+
// take, and it would be a different thing from the C64's pixel-precise
|
|
10
|
+
// arrow rather than a poorer copy of it.
|
|
11
|
+
//
|
|
12
|
+
// The other half is missing too: @8bitscript/vic20/input reports no
|
|
13
|
+
// pointer. The catalog offers `port1: mouse1351` and xvic accepts it, but
|
|
14
|
+
// this repository has no 1351 driver for the VIC-20 and the hardware note
|
|
15
|
+
// marks the 1351 on this machine *to verify* (see
|
|
16
|
+
// packages/vic20/AGENTS.md). So there is nothing to draw and nowhere to
|
|
17
|
+
// draw it from, in that order.
|
|
18
|
+
//
|
|
19
|
+
// It still has to exist: Studio and every other portable program links
|
|
20
|
+
// @8bitscript/pointer, so a missing entry is not a program without a
|
|
21
|
+
// cursor, it is a program that does not build for the VIC-20 — the machine
|
|
22
|
+
// with the least room to spare, which is exactly why this file is
|
|
23
|
+
// constants. `DRAWS` is a const and every function is empty, so a
|
|
24
|
+
// program's `if (pointer.DRAWS)` folds away and LLVM deletes the calls.
|
|
25
|
+
export namespace pointer {
|
|
26
|
+
|
|
27
|
+
const DRAWS: bool = false;
|
|
28
|
+
|
|
29
|
+
function begin(): void {
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function setColor(color: utinyint): void {
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function update(): void {
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hide(): void {
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/screen.8bs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @8bitscript/vic20/screen — the VIC-20's implementation of @8bitscript/screen.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./screen"] in this package's package.json,
|
|
4
|
+
// and what
|
|
5
|
+
//
|
|
6
|
+
// import { screen, BorderColor, BackgroundColor } from "@8bitscript/screen";
|
|
7
|
+
//
|
|
8
|
+
// resolves to when the build is for the VIC-20: @8bitscript/screen's entry is
|
|
9
|
+
// keyed by machine and delegates here. Built on the register @8bitscript/vic20
|
|
10
|
+
// itself exports, one layer up from it. Every machine's screen.8bs exports
|
|
11
|
+
// this same surface, which is what lets a program import it from
|
|
12
|
+
// @8bitscript/screen and never name the hardware.
|
|
13
|
+
import { vicColor } from "./index.8bs";
|
|
14
|
+
import { Video } from "./geometry.8bs";
|
|
15
|
+
|
|
16
|
+
// ---- the screen: border and background --------------------------------------
|
|
17
|
+
//
|
|
18
|
+
// One packed register on the VIC: border in bits 0-2 (the VIC has eight
|
|
19
|
+
// border colours, so the value is masked to them), background in bits 4-7,
|
|
20
|
+
// bit 3 held at 1 for normal video. Both colours in one call because they
|
|
21
|
+
// are one write here — and because on the NES and the X16 the two are
|
|
22
|
+
// genuinely one operation (see those packages), so the portable surface is
|
|
23
|
+
// the one every machine can honour.
|
|
24
|
+
export namespace screen {
|
|
25
|
+
function setColors(border: u8, background: u8): void {
|
|
26
|
+
vicColor = (8 | (border & 7)) | (background << 4);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// `blank(border, background)`: every cell blank and both colours set —
|
|
30
|
+
// black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
|
|
31
|
+
// to leave one as it is. `setBorder` and `setBackground` are one
|
|
32
|
+
// colour each, `setColors` the pair; `text.setColor` owns what the
|
|
33
|
+
// next print looks like.
|
|
34
|
+
function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
|
|
35
|
+
if (border != BorderColor.KEEP) {
|
|
36
|
+
screen.setBorder(border);
|
|
37
|
+
}
|
|
38
|
+
if (background != BackgroundColor.KEEP) {
|
|
39
|
+
screen.setBackground(background);
|
|
40
|
+
}
|
|
41
|
+
for (let cell: usmallint = 0; cell < Video.CELL_COUNT; cell++) {
|
|
42
|
+
memory.write(Video.SCREEN + cell, 32); // 32: the space screen code
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setBackground(background: u8): void {
|
|
47
|
+
vicColor = (vicColor & 0x0F) | (background << 4);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setBorder(border: u8): void {
|
|
51
|
+
vicColor = (vicColor & 0xF0) | 8 | (border & 7);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- colour names ----------------------------------------------------------
|
|
56
|
+
//
|
|
57
|
+
// The border field only has 3 bits, so it can only reach the first 8 of the
|
|
58
|
+
// VIC's 16 colours — `BorderColor` and `BackgroundColor` are two namespaces,
|
|
59
|
+
// not one shared `Color`, because the hardware itself draws that line, not
|
|
60
|
+
// an API design preference. Values and names match the VIC-20's own colour
|
|
61
|
+
// numbering (POKE 36879 in the Programmer's Reference Guide). Those first
|
|
62
|
+
// eight names — Black, White, Red, Cyan, Purple, Green, Blue, Yellow — are
|
|
63
|
+
// the ones every machine's screen.8bs exports in both namespaces, each with
|
|
64
|
+
// its own hardware's value, which is what lets a program say
|
|
65
|
+
// `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` on any target.
|
|
66
|
+
|
|
67
|
+
export namespace BorderColor {
|
|
68
|
+
const BLACK: utinyint = 0;
|
|
69
|
+
const WHITE: utinyint = 1;
|
|
70
|
+
const RED: utinyint = 2;
|
|
71
|
+
const CYAN: utinyint = 3;
|
|
72
|
+
const PURPLE: utinyint = 4;
|
|
73
|
+
const GREEN: utinyint = 5;
|
|
74
|
+
const BLUE: utinyint = 6;
|
|
75
|
+
const YELLOW: utinyint = 7;
|
|
76
|
+
// Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
|
|
77
|
+
// a value no register here takes.
|
|
78
|
+
const KEEP: utinyint = 255;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export namespace BackgroundColor {
|
|
82
|
+
const BLACK: utinyint = 0;
|
|
83
|
+
const WHITE: utinyint = 1;
|
|
84
|
+
const RED: utinyint = 2;
|
|
85
|
+
const CYAN: utinyint = 3;
|
|
86
|
+
const PURPLE: utinyint = 4;
|
|
87
|
+
const GREEN: utinyint = 5;
|
|
88
|
+
const BLUE: utinyint = 6;
|
|
89
|
+
const YELLOW: utinyint = 7;
|
|
90
|
+
const ORANGE: utinyint = 8;
|
|
91
|
+
const LIGHT_ORANGE: utinyint = 9;
|
|
92
|
+
const PINK_RED: utinyint = 10;
|
|
93
|
+
const LIGHT_CYAN: utinyint = 11;
|
|
94
|
+
const LIGHT_PURPLE: utinyint = 12;
|
|
95
|
+
const LIGHT_GREEN: utinyint = 13;
|
|
96
|
+
const LIGHT_BLUE: utinyint = 14;
|
|
97
|
+
const LIGHT_YELLOW: utinyint = 15;
|
|
98
|
+
// Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
|
|
99
|
+
// a value no register here takes.
|
|
100
|
+
const KEEP: utinyint = 255;
|
|
101
|
+
}
|
package/src/text.8bs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// @8bitscript/vic20/text — the VIC-20's implementation of @8bitscript/text.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./text"] in this package's package.json,
|
|
4
|
+
// and what
|
|
5
|
+
//
|
|
6
|
+
// import { text } from "@8bitscript/text";
|
|
7
|
+
//
|
|
8
|
+
// resolves to when the build is for the VIC-20. Built on the registers
|
|
9
|
+
// @8bitscript/vic20 exports. Every machine's text.8bs exports this same
|
|
10
|
+
// namespace — the same names, the same shapes, ASCII codes, and a cell 0
|
|
11
|
+
// at the top-left corner inside the border — so a program that imports it
|
|
12
|
+
// from @8bitscript/text draws the same thing on every target.
|
|
13
|
+
import { memoryPointer } from "./index.8bs";
|
|
14
|
+
import { Video } from "./geometry.8bs";
|
|
15
|
+
|
|
16
|
+
// The screen is addressed by flat cell index — a position is
|
|
17
|
+
// `y * text.COLUMNS + x` — and these are the plainest things a text API
|
|
18
|
+
// can be built from: poke one cell's character code, poke one cell's colour, and how many
|
|
19
|
+
// cells the whole screen has. Whether a caller uses these to clear the
|
|
20
|
+
// screen, lay out a label, or nothing at all is entirely up to the caller —
|
|
21
|
+
// this package draws nothing on its own.
|
|
22
|
+
//
|
|
23
|
+
// Where the 22x23 screen and its colour nybbles are depends on the RAM the
|
|
24
|
+
// machine has — $1E00/$9600 unexpanded or with 3K, $1000/$9400 with 8K or
|
|
25
|
+
// more — and that is the build's `--profile` to say (VIC20_PROFILES in
|
|
26
|
+
// packages/backend-6502). It reaches this file as ./geometry.8bs's `Video`
|
|
27
|
+
// namespace, whose 8k/16k/24k twins the profile selects; nothing here
|
|
28
|
+
// names an address.
|
|
29
|
+
//
|
|
30
|
+
// ---- character codes: ASCII in, screen codes out ----------------------------
|
|
31
|
+
//
|
|
32
|
+
// `text.putChar` takes ASCII on every machine — space (32), '0'-'9'
|
|
33
|
+
// (48-57), 'A'-'Z' (65-90) and the punctuation `! , - . : ?` — upper case
|
|
34
|
+
// only: that is the portable set, the characters every target's character
|
|
35
|
+
// set has (the NES ships its own font, and that is what it has). The VIC
|
|
36
|
+
// wants screen codes: in every Commodore character ROM codes 32-63 are
|
|
37
|
+
// those same ASCII values, and 'A'-'Z' are 1-26, so asciiToScreenCode()
|
|
38
|
+
// moves 64-95 down by 64 and leaves 32-63 alone.
|
|
39
|
+
//
|
|
40
|
+
// Whether 1-26 then LOOK like 'A'-'Z' or 'a'-'z' is the character set's
|
|
41
|
+
// choice — the ROM's boot state, whatever ran before, a user's SHIFT+C= —
|
|
42
|
+
// so this package owns the choice rather than inheriting one (LLVM-MOS's
|
|
43
|
+
// libc used to flip the machine to lower-case before main() with a hidden
|
|
44
|
+
// CHR$(14); packages/backend-6502's commodoreCharsetGuard() keeps that out
|
|
45
|
+
// of the link). putChar()
|
|
46
|
+
// selects the upper-case set, every call, through the VIC's memory pointer
|
|
47
|
+
// (see @8bitscript/vic20's `memoryPointer`): one register write — of the
|
|
48
|
+
// value that also names this profile's screen base, since $9005 holds
|
|
49
|
+
// both — and "TICK" reads as TICK here the way it does on the NES, the
|
|
50
|
+
// Atari, and the X16, none of which have a lower-case set to fall into.
|
|
51
|
+
|
|
52
|
+
function asciiToScreenCode(code: utinyint): utinyint {
|
|
53
|
+
if (code >= 64 && code < 96) {
|
|
54
|
+
return code - 64;
|
|
55
|
+
}
|
|
56
|
+
return code;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---- print: strings and number fields ------------------------------------
|
|
60
|
+
//
|
|
61
|
+
// `text.print(cell, s)` writes a string's characters into consecutive
|
|
62
|
+
// cells from `cell`, and `text.printNumber(cell, value, width)` writes
|
|
63
|
+
// `value` as exactly `width` decimal digits, zero-padded and right-aligned,
|
|
64
|
+
// so a field on a HUD never shifts columns. Both draw in the current colour —
|
|
65
|
+
// white until `text.setColor(TextColor.CYAN)` changes it, and that one
|
|
66
|
+
// call then colours everything printed after it, on the machines that have
|
|
67
|
+
// per-cell colour. They are also the two
|
|
68
|
+
// functions the compiler's template layout targets: `text.print(0,
|
|
69
|
+
// \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
|
|
70
|
+
// (see packages/compiler/src/ir).
|
|
71
|
+
//
|
|
72
|
+
// Under them: `place()` puts one character at one cell in the current
|
|
73
|
+
// colour, and is all a run of text costs per character, with `prepare()` selecting the
|
|
74
|
+
// upper-case set once for the run.
|
|
75
|
+
// `text.putChar` and `text.putColor` stay the one-cell pokes a caller can
|
|
76
|
+
// build anything from.
|
|
77
|
+
let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
|
|
78
|
+
let currentReverse: bool = false; // until text.setReverse() says otherwise
|
|
79
|
+
|
|
80
|
+
// Reverse video is a real ROM copy at codes 128-255.
|
|
81
|
+
function toScreen(code: utinyint): utinyint {
|
|
82
|
+
let screen: utinyint = asciiToScreenCode(code);
|
|
83
|
+
if (currentReverse) {
|
|
84
|
+
screen = screen + 128;
|
|
85
|
+
}
|
|
86
|
+
return screen;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// What a run of text needs once, before its first character.
|
|
90
|
+
function prepare(): void {
|
|
91
|
+
memoryPointer = Video.MEMORY_POINTER_UPPERCASE;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function place(cell: usmallint, code: utinyint): void {
|
|
95
|
+
memory.write(Video.SCREEN + cell, toScreen(code));
|
|
96
|
+
memory.write(Video.COLOR + cell, currentColor & 0x07); // bit 3 would make the cell multicolour
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- digits -------------------------------------------------------------
|
|
100
|
+
//
|
|
101
|
+
// A number is written one place at a time, high to low, by subtracting the
|
|
102
|
+
// place value until it no longer fits: the 6502 has no divide instruction,
|
|
103
|
+
// and `value / 10` would link a 250-byte routine to do it. Places above the
|
|
104
|
+
// field are still taken off, so a field narrower than its number shows the
|
|
105
|
+
// low digits; places the number does not reach print as zeros.
|
|
106
|
+
const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
|
|
107
|
+
|
|
108
|
+
export namespace text {
|
|
109
|
+
const CELL_COUNT: usmallint = Video.CELL_COUNT; // 22 columns x 23 rows
|
|
110
|
+
const COLUMNS: utinyint = Video.COLUMNS; // cells per row, so cell = y * text.COLUMNS + x
|
|
111
|
+
|
|
112
|
+
function putChar(cell: usmallint, code: utinyint): void {
|
|
113
|
+
prepare();
|
|
114
|
+
memory.write(Video.SCREEN + cell, toScreen(code));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function putColor(cell: usmallint, color: utinyint): void {
|
|
118
|
+
memory.write(Video.COLOR + cell, color & 0x07);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setColor(color: utinyint): void {
|
|
122
|
+
currentColor = color;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function setReverse(on: bool): void {
|
|
126
|
+
currentReverse = on;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function print(cell: usmallint, s: string): void {
|
|
130
|
+
prepare();
|
|
131
|
+
for (let i: utinyint = 0; i < s.length; i++) {
|
|
132
|
+
place(cell + i, s[i]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
|
|
137
|
+
prepare();
|
|
138
|
+
let k: utinyint = width;
|
|
139
|
+
if (k < DIGIT_PLACES.length) {
|
|
140
|
+
k = DIGIT_PLACES.length;
|
|
141
|
+
}
|
|
142
|
+
while (k > 0) {
|
|
143
|
+
let digit: utinyint = 48; // '0'
|
|
144
|
+
if (k <= DIGIT_PLACES.length) {
|
|
145
|
+
while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
|
|
146
|
+
value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
|
|
147
|
+
digit++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (k <= width) {
|
|
151
|
+
place(cell + width - k, digit);
|
|
152
|
+
}
|
|
153
|
+
k--;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The colours `text.setColor()` takes: the eight names every machine
|
|
159
|
+
// shares, with this machine's colour-RAM values.
|
|
160
|
+
export namespace TextColor {
|
|
161
|
+
const BLACK: utinyint = 0;
|
|
162
|
+
const WHITE: utinyint = 1;
|
|
163
|
+
const RED: utinyint = 2;
|
|
164
|
+
const CYAN: utinyint = 3;
|
|
165
|
+
const PURPLE: utinyint = 4;
|
|
166
|
+
const GREEN: utinyint = 5;
|
|
167
|
+
const BLUE: utinyint = 6;
|
|
168
|
+
const YELLOW: utinyint = 7;
|
|
169
|
+
}
|