@8bitscript/nes 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/native/6502/font.s +212 -0
- package/package.json +85 -0
- package/src/index.8bs +185 -0
- package/src/input.8bs +141 -0
- package/src/pad.8bs +120 -0
- package/src/pointer.8bs +39 -0
- package/src/screen.8bs +213 -0
- package/src/text.8bs +185 -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.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
; @8bitscript/nes — the CHR-ROM character set.
|
|
2
|
+
;
|
|
3
|
+
; The NES has no character ROM of its own. Every Commodore machine and the
|
|
4
|
+
; Atari 8-bit ship their letters and digits in a ROM the video chip reads
|
|
5
|
+
; directly, so `text.putChar` on those targets only ever writes a code
|
|
6
|
+
; into screen memory. The NES PPU reads its 8x8 tile patterns from the
|
|
7
|
+
; CARTRIDGE (CHR-ROM on NROM), so a cartridge that shows text has to bring
|
|
8
|
+
; its own — this file is that ROM, linked into the .nes image through this
|
|
9
|
+
; package's "8bitscript".native list (see package.json and docs/packages.md).
|
|
10
|
+
;
|
|
11
|
+
; Layout: pattern table 0 ($0000-$0FFF), 256 tiles of 16 bytes, laid out so
|
|
12
|
+
; that TILE INDEX == ASCII CODE for every glyph here — space ($20), the
|
|
13
|
+
; digits ($30-$39), A-Z ($41-$5A), and ! , - . : ? — so @8bitscript/nes's
|
|
14
|
+
; `text.putChar(cell, code)` writes the ASCII code straight into the
|
|
15
|
+
; nametable with no translation table, the same codes examples/borders'
|
|
16
|
+
; Atari variant already uses. Codes in $20-$5F with no glyph drawn render
|
|
17
|
+
; blank. Tile $80 is solid colour index 2 — the tile @8bitscript/nes lays
|
|
18
|
+
; around the screen edge as the drawn "border" (see index.8bs). Reverse
|
|
19
|
+
; video of $20-$5F lives at ASCII+128 ($A0-$DF), leaving $80 alone.
|
|
20
|
+
; Everything else, including all of pattern table 1 ($1000-$1FFF), is blank.
|
|
21
|
+
;
|
|
22
|
+
; Tile format (nesdev.org/wiki/PPU_pattern_tables): 8 bytes of bitplane 0
|
|
23
|
+
; then 8 bytes of bitplane 1, one byte per row, bit 7 the leftmost pixel.
|
|
24
|
+
; A pixel's colour index is plane1:plane0, so a glyph with rows in plane 0
|
|
25
|
+
; and zeros in plane 1 is drawn entirely in colour index 1 — palette entry
|
|
26
|
+
; $3F01, the text colour screen.setColors() sets — and the solid tile, with zeros
|
|
27
|
+
; in plane 0 and $FF in plane 1, is entirely colour index 2 ($3F02).
|
|
28
|
+
; The binary literals below therefore ARE the glyph artwork: read each
|
|
29
|
+
; `tile` line's eight rows top to bottom, 1 = lit.
|
|
30
|
+
|
|
31
|
+
.section .chr_rom,"a"
|
|
32
|
+
|
|
33
|
+
.macro tile r0, r1, r2, r3, r4, r5, r6, r7
|
|
34
|
+
.byte \r0, \r1, \r2, \r3, \r4, \r5, \r6, \r7
|
|
35
|
+
.byte 0, 0, 0, 0, 0, 0, 0, 0
|
|
36
|
+
.endm
|
|
37
|
+
|
|
38
|
+
; Reverse of a glyph: plane 0 inverted, plane 1 still clear, so lit pixels
|
|
39
|
+
; become the backdrop and the rest of the cell takes colour index 1 — the
|
|
40
|
+
; text colour. A reverse space is therefore a solid block in that colour.
|
|
41
|
+
.macro itile r0, r1, r2, r3, r4, r5, r6, r7
|
|
42
|
+
.byte 0xff^\r0, 0xff^\r1, 0xff^\r2, 0xff^\r3, 0xff^\r4, 0xff^\r5, 0xff^\r6, 0xff^\r7
|
|
43
|
+
.byte 0, 0, 0, 0, 0, 0, 0, 0
|
|
44
|
+
.endm
|
|
45
|
+
|
|
46
|
+
.macro rfill
|
|
47
|
+
.byte 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
|
48
|
+
.byte 0, 0, 0, 0, 0, 0, 0, 0
|
|
49
|
+
.endm
|
|
50
|
+
|
|
51
|
+
; $00-$20: control codes and space — 33 blank tiles.
|
|
52
|
+
.space 33 * 16
|
|
53
|
+
|
|
54
|
+
; $21 '!'
|
|
55
|
+
tile 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00000000, 0b00000000, 0b00011000, 0b00000000
|
|
56
|
+
|
|
57
|
+
; $22-$2B: " # $ % & ' ( ) * + — 10 blank tiles.
|
|
58
|
+
.space 10 * 16
|
|
59
|
+
|
|
60
|
+
; $2C ','
|
|
61
|
+
tile 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b00011000, 0b00110000, 0b00000000
|
|
62
|
+
; $2D '-'
|
|
63
|
+
tile 0b00000000, 0b00000000, 0b00000000, 0b01111110, 0b00000000, 0b00000000, 0b00000000, 0b00000000
|
|
64
|
+
; $2E '.'
|
|
65
|
+
tile 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b00011000, 0b00000000
|
|
66
|
+
|
|
67
|
+
; $2F '/' — blank.
|
|
68
|
+
.space 16
|
|
69
|
+
|
|
70
|
+
; $30-$39 '0'-'9'
|
|
71
|
+
tile 0b00111100, 0b01100110, 0b01101110, 0b01110110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
72
|
+
tile 0b00011000, 0b00111000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b01111110, 0b00000000
|
|
73
|
+
tile 0b00111100, 0b01100110, 0b00000110, 0b00001100, 0b00110000, 0b01100000, 0b01111110, 0b00000000
|
|
74
|
+
tile 0b00111100, 0b01100110, 0b00000110, 0b00011100, 0b00000110, 0b01100110, 0b00111100, 0b00000000
|
|
75
|
+
tile 0b00001100, 0b00011100, 0b00111100, 0b01101100, 0b01111110, 0b00001100, 0b00001100, 0b00000000
|
|
76
|
+
tile 0b01111110, 0b01100000, 0b01111100, 0b00000110, 0b00000110, 0b01100110, 0b00111100, 0b00000000
|
|
77
|
+
tile 0b00111100, 0b01100000, 0b01111100, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
78
|
+
tile 0b01111110, 0b00000110, 0b00001100, 0b00011000, 0b00110000, 0b00110000, 0b00110000, 0b00000000
|
|
79
|
+
tile 0b00111100, 0b01100110, 0b01100110, 0b00111100, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
80
|
+
tile 0b00111100, 0b01100110, 0b01100110, 0b00111110, 0b00000110, 0b00001100, 0b00111000, 0b00000000
|
|
81
|
+
|
|
82
|
+
; $3A ':'
|
|
83
|
+
tile 0b00000000, 0b00011000, 0b00011000, 0b00000000, 0b00011000, 0b00011000, 0b00000000, 0b00000000
|
|
84
|
+
|
|
85
|
+
; $3B-$3E: ; < = > — 4 blank tiles.
|
|
86
|
+
.space 4 * 16
|
|
87
|
+
|
|
88
|
+
; $3F '?'
|
|
89
|
+
tile 0b00111100, 0b01100110, 0b00000110, 0b00001100, 0b00011000, 0b00000000, 0b00011000, 0b00000000
|
|
90
|
+
|
|
91
|
+
; $40 '@' — blank.
|
|
92
|
+
.space 16
|
|
93
|
+
|
|
94
|
+
; $41-$5A 'A'-'Z'
|
|
95
|
+
tile 0b00011000, 0b00111100, 0b01100110, 0b01111110, 0b01100110, 0b01100110, 0b01100110, 0b00000000
|
|
96
|
+
tile 0b01111100, 0b01100110, 0b01100110, 0b01111100, 0b01100110, 0b01100110, 0b01111100, 0b00000000
|
|
97
|
+
tile 0b00111100, 0b01100110, 0b01100000, 0b01100000, 0b01100000, 0b01100110, 0b00111100, 0b00000000
|
|
98
|
+
tile 0b01111000, 0b01101100, 0b01100110, 0b01100110, 0b01100110, 0b01101100, 0b01111000, 0b00000000
|
|
99
|
+
tile 0b01111110, 0b01100000, 0b01100000, 0b01111000, 0b01100000, 0b01100000, 0b01111110, 0b00000000
|
|
100
|
+
tile 0b01111110, 0b01100000, 0b01100000, 0b01111000, 0b01100000, 0b01100000, 0b01100000, 0b00000000
|
|
101
|
+
tile 0b00111100, 0b01100110, 0b01100000, 0b01101110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
102
|
+
tile 0b01100110, 0b01100110, 0b01100110, 0b01111110, 0b01100110, 0b01100110, 0b01100110, 0b00000000
|
|
103
|
+
tile 0b00111100, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00111100, 0b00000000
|
|
104
|
+
tile 0b00011110, 0b00001100, 0b00001100, 0b00001100, 0b00001100, 0b01101100, 0b00111000, 0b00000000
|
|
105
|
+
tile 0b01100110, 0b01101100, 0b01111000, 0b01110000, 0b01111000, 0b01101100, 0b01100110, 0b00000000
|
|
106
|
+
tile 0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01111110, 0b00000000
|
|
107
|
+
tile 0b01100011, 0b01110111, 0b01111111, 0b01101011, 0b01100011, 0b01100011, 0b01100011, 0b00000000
|
|
108
|
+
tile 0b01100110, 0b01110110, 0b01111110, 0b01111110, 0b01101110, 0b01100110, 0b01100110, 0b00000000
|
|
109
|
+
tile 0b00111100, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
110
|
+
tile 0b01111100, 0b01100110, 0b01100110, 0b01111100, 0b01100000, 0b01100000, 0b01100000, 0b00000000
|
|
111
|
+
tile 0b00111100, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00001110, 0b00000000
|
|
112
|
+
tile 0b01111100, 0b01100110, 0b01100110, 0b01111100, 0b01111000, 0b01101100, 0b01100110, 0b00000000
|
|
113
|
+
tile 0b00111100, 0b01100110, 0b01100000, 0b00111100, 0b00000110, 0b01100110, 0b00111100, 0b00000000
|
|
114
|
+
tile 0b01111110, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00000000
|
|
115
|
+
tile 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
116
|
+
tile 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00011000, 0b00000000
|
|
117
|
+
tile 0b01100011, 0b01100011, 0b01100011, 0b01101011, 0b01111111, 0b01110111, 0b01100011, 0b00000000
|
|
118
|
+
tile 0b01100110, 0b01100110, 0b00111100, 0b00011000, 0b00111100, 0b01100110, 0b01100110, 0b00000000
|
|
119
|
+
tile 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00011000, 0b00011000, 0b00011000, 0b00000000
|
|
120
|
+
tile 0b01111110, 0b00000110, 0b00001100, 0b00011000, 0b00110000, 0b01100000, 0b01111110, 0b00000000
|
|
121
|
+
|
|
122
|
+
; $5B-$7F: [ \ ] ^ _ ` a-z { | } ~ DEL — 37 blank tiles (no lowercase yet).
|
|
123
|
+
.space 37 * 16
|
|
124
|
+
|
|
125
|
+
; $80: the solid tile — every pixel colour index 2 (plane 1 set, plane 0
|
|
126
|
+
; clear), so it takes whatever palette entry $3F02 holds: `border`.
|
|
127
|
+
.byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
|
128
|
+
.byte 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
|
129
|
+
|
|
130
|
+
; $81-$9F: 31 blank tiles — a gap so reverse copies can sit at ASCII+128
|
|
131
|
+
; without colliding with the border tile at $80.
|
|
132
|
+
.space 31 * 16
|
|
133
|
+
|
|
134
|
+
; $A0-$DF: reverse video of $20-$5F. Tile index == ASCII + 128, the same
|
|
135
|
+
; rule Commodore screen codes use, so `text.setReverse` is `code + 128`.
|
|
136
|
+
; Inverted blanks (and the reverse space at $A0) are solid colour index 1.
|
|
137
|
+
; $A0 reverse space
|
|
138
|
+
rfill
|
|
139
|
+
; $A1 reverse '!'
|
|
140
|
+
itile 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00000000, 0b00000000, 0b00011000, 0b00000000
|
|
141
|
+
; $A2-$AB: reverse of the $22-$2B blanks
|
|
142
|
+
.rept 10
|
|
143
|
+
rfill
|
|
144
|
+
.endr
|
|
145
|
+
; $AC reverse ','
|
|
146
|
+
itile 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b00011000, 0b00110000, 0b00000000
|
|
147
|
+
; $AD reverse '-'
|
|
148
|
+
itile 0b00000000, 0b00000000, 0b00000000, 0b01111110, 0b00000000, 0b00000000, 0b00000000, 0b00000000
|
|
149
|
+
; $AE reverse '.'
|
|
150
|
+
itile 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b00011000, 0b00000000
|
|
151
|
+
; $AF reverse of blank '/'
|
|
152
|
+
rfill
|
|
153
|
+
; $B0-$B9 reverse '0'-'9'
|
|
154
|
+
itile 0b00111100, 0b01100110, 0b01101110, 0b01110110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
155
|
+
itile 0b00011000, 0b00111000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b01111110, 0b00000000
|
|
156
|
+
itile 0b00111100, 0b01100110, 0b00000110, 0b00001100, 0b00110000, 0b01100000, 0b01111110, 0b00000000
|
|
157
|
+
itile 0b00111100, 0b01100110, 0b00000110, 0b00011100, 0b00000110, 0b01100110, 0b00111100, 0b00000000
|
|
158
|
+
itile 0b00001100, 0b00011100, 0b00111100, 0b01101100, 0b01111110, 0b00001100, 0b00001100, 0b00000000
|
|
159
|
+
itile 0b01111110, 0b01100000, 0b01111100, 0b00000110, 0b00000110, 0b01100110, 0b00111100, 0b00000000
|
|
160
|
+
itile 0b00111100, 0b01100000, 0b01111100, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
161
|
+
itile 0b01111110, 0b00000110, 0b00001100, 0b00011000, 0b00110000, 0b00110000, 0b00110000, 0b00000000
|
|
162
|
+
itile 0b00111100, 0b01100110, 0b01100110, 0b00111100, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
163
|
+
itile 0b00111100, 0b01100110, 0b01100110, 0b00111110, 0b00000110, 0b00001100, 0b00111000, 0b00000000
|
|
164
|
+
; $BA reverse ':'
|
|
165
|
+
itile 0b00000000, 0b00011000, 0b00011000, 0b00000000, 0b00011000, 0b00011000, 0b00000000, 0b00000000
|
|
166
|
+
; $BB-$BE: reverse of the $3B-$3E blanks
|
|
167
|
+
.rept 4
|
|
168
|
+
rfill
|
|
169
|
+
.endr
|
|
170
|
+
; $BF reverse '?'
|
|
171
|
+
itile 0b00111100, 0b01100110, 0b00000110, 0b00001100, 0b00011000, 0b00000000, 0b00011000, 0b00000000
|
|
172
|
+
; $C0 reverse of blank '@'
|
|
173
|
+
rfill
|
|
174
|
+
; $C1-$DA reverse 'A'-'Z'
|
|
175
|
+
itile 0b00011000, 0b00111100, 0b01100110, 0b01111110, 0b01100110, 0b01100110, 0b01100110, 0b00000000
|
|
176
|
+
itile 0b01111100, 0b01100110, 0b01100110, 0b01111100, 0b01100110, 0b01100110, 0b01111100, 0b00000000
|
|
177
|
+
itile 0b00111100, 0b01100110, 0b01100000, 0b01100000, 0b01100000, 0b01100110, 0b00111100, 0b00000000
|
|
178
|
+
itile 0b01111000, 0b01101100, 0b01100110, 0b01100110, 0b01100110, 0b01101100, 0b01111000, 0b00000000
|
|
179
|
+
itile 0b01111110, 0b01100000, 0b01100000, 0b01111000, 0b01100000, 0b01100000, 0b01111110, 0b00000000
|
|
180
|
+
itile 0b01111110, 0b01100000, 0b01100000, 0b01111000, 0b01100000, 0b01100000, 0b01100000, 0b00000000
|
|
181
|
+
itile 0b00111100, 0b01100110, 0b01100000, 0b01101110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
182
|
+
itile 0b01100110, 0b01100110, 0b01100110, 0b01111110, 0b01100110, 0b01100110, 0b01100110, 0b00000000
|
|
183
|
+
itile 0b00111100, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00111100, 0b00000000
|
|
184
|
+
itile 0b00011110, 0b00001100, 0b00001100, 0b00001100, 0b00001100, 0b01101100, 0b00111000, 0b00000000
|
|
185
|
+
itile 0b01100110, 0b01101100, 0b01111000, 0b01110000, 0b01111000, 0b01101100, 0b01100110, 0b00000000
|
|
186
|
+
itile 0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01111110, 0b00000000
|
|
187
|
+
itile 0b01100011, 0b01110111, 0b01111111, 0b01101011, 0b01100011, 0b01100011, 0b01100011, 0b00000000
|
|
188
|
+
itile 0b01100110, 0b01110110, 0b01111110, 0b01111110, 0b01101110, 0b01100110, 0b01100110, 0b00000000
|
|
189
|
+
itile 0b00111100, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
190
|
+
itile 0b01111100, 0b01100110, 0b01100110, 0b01111100, 0b01100000, 0b01100000, 0b01100000, 0b00000000
|
|
191
|
+
itile 0b00111100, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00001110, 0b00000000
|
|
192
|
+
itile 0b01111100, 0b01100110, 0b01100110, 0b01111100, 0b01111000, 0b01101100, 0b01100110, 0b00000000
|
|
193
|
+
itile 0b00111100, 0b01100110, 0b01100000, 0b00111100, 0b00000110, 0b01100110, 0b00111100, 0b00000000
|
|
194
|
+
itile 0b01111110, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00000000
|
|
195
|
+
itile 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00000000
|
|
196
|
+
itile 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00011000, 0b00000000
|
|
197
|
+
itile 0b01100011, 0b01100011, 0b01100011, 0b01101011, 0b01111111, 0b01110111, 0b01100011, 0b00000000
|
|
198
|
+
itile 0b01100110, 0b01100110, 0b00111100, 0b00011000, 0b00111100, 0b01100110, 0b01100110, 0b00000000
|
|
199
|
+
itile 0b01100110, 0b01100110, 0b01100110, 0b00111100, 0b00011000, 0b00011000, 0b00011000, 0b00000000
|
|
200
|
+
itile 0b01111110, 0b00000110, 0b00001100, 0b00011000, 0b00110000, 0b01100000, 0b01111110, 0b00000000
|
|
201
|
+
; $DB-$DF: reverse of the $5B-$5F blanks
|
|
202
|
+
.rept 5
|
|
203
|
+
rfill
|
|
204
|
+
.endr
|
|
205
|
+
|
|
206
|
+
; $E0-$FF: 32 blank tiles, completing pattern table 0 at exactly 4096 bytes.
|
|
207
|
+
.space 32 * 16
|
|
208
|
+
|
|
209
|
+
; Pattern table 1 ($1000-$1FFF): unused, blank. Spelled out rather than left
|
|
210
|
+
; to the linker's padding so the section is exactly the 8 KiB the iNES
|
|
211
|
+
; header declares for NROM, whatever the linker script does with a short one.
|
|
212
|
+
.space 4096
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@8bitscript/nes",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NES 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
|
+
"./pad": "./src/pad.8bs",
|
|
12
|
+
"./input": "./src/input.8bs",
|
|
13
|
+
"./pointer": "./src/pointer.8bs"
|
|
14
|
+
},
|
|
15
|
+
"native": [
|
|
16
|
+
"./native/6502/font.s"
|
|
17
|
+
],
|
|
18
|
+
"hardware": {
|
|
19
|
+
"facts": {
|
|
20
|
+
"video.columns": 28,
|
|
21
|
+
"video.rows": 26,
|
|
22
|
+
"video.cellWidth": 8,
|
|
23
|
+
"video.cellHeight": 8,
|
|
24
|
+
"video.palette": 25,
|
|
25
|
+
"video.cellColors": 3,
|
|
26
|
+
"video.colorPerCell": false,
|
|
27
|
+
"video.glyphs": 0,
|
|
28
|
+
"video.blockWidth": 0,
|
|
29
|
+
"video.blockHeight": 0,
|
|
30
|
+
"video.bitmap": false,
|
|
31
|
+
"video.layers": 1,
|
|
32
|
+
"video.scroll": true,
|
|
33
|
+
"video.sprites": 64,
|
|
34
|
+
"video.spritesPerLine": 8,
|
|
35
|
+
"video.spriteWidth": 8,
|
|
36
|
+
"video.spriteHeight": 16,
|
|
37
|
+
"video.spriteColors": 3,
|
|
38
|
+
"video.frameRate": 60,
|
|
39
|
+
"audio.voices": 5,
|
|
40
|
+
"audio.noise": true,
|
|
41
|
+
"audio.envelope": true,
|
|
42
|
+
"audio.filter": false,
|
|
43
|
+
"audio.pcm": true,
|
|
44
|
+
"audio.volume": true,
|
|
45
|
+
"audio.entropy": false,
|
|
46
|
+
"input.keyboard": false,
|
|
47
|
+
"input.joysticks": 0,
|
|
48
|
+
"input.pads": 2,
|
|
49
|
+
"input.mouse": false,
|
|
50
|
+
"input.paddles": false,
|
|
51
|
+
"storage.save": false,
|
|
52
|
+
"memory.ram": 1536,
|
|
53
|
+
"memory.banked": false,
|
|
54
|
+
"memory.bankedKib": 0,
|
|
55
|
+
"storage.kib": 0
|
|
56
|
+
},
|
|
57
|
+
"options": {
|
|
58
|
+
"mapper": {
|
|
59
|
+
"label": "Cartridge board",
|
|
60
|
+
"default": "nrom",
|
|
61
|
+
"values": {
|
|
62
|
+
"nrom": {
|
|
63
|
+
"label": "NROM: 32K PRG, 8K CHR-ROM, no save",
|
|
64
|
+
"build": {
|
|
65
|
+
"driver": "mos-nes-nrom-clang",
|
|
66
|
+
"output": "nes"
|
|
67
|
+
},
|
|
68
|
+
"facts": {
|
|
69
|
+
"storage.save": false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"presets": {}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"files": [
|
|
79
|
+
"src",
|
|
80
|
+
"native"
|
|
81
|
+
],
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/index.8bs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// @8bitscript/nes — NES target support: the hardware underneath.
|
|
2
|
+
//
|
|
3
|
+
// See ../AGENTS.md before extending this package: the NES PPU is tile/pattern
|
|
4
|
+
// oriented rather than a framebuffer, its 64-sprite budget is really an
|
|
5
|
+
// 8-per-scanline budget, and its mapper (only NROM is wired up today) is a
|
|
6
|
+
// build profile question, not a language one — mistakes easy to bake into
|
|
7
|
+
// comments or APIs here if copied from a generic "8-bit" mental model.
|
|
8
|
+
//
|
|
9
|
+
// The module entry point named by the "8bitscript".entry field in this
|
|
10
|
+
// package's package.json, resolved and linked for:
|
|
11
|
+
//
|
|
12
|
+
// import { setVramAddress, resetScroll } from "@8bitscript/nes";
|
|
13
|
+
//
|
|
14
|
+
// What a program usually wants sits one layer up, in this package's own
|
|
15
|
+
// implementations of the portable capability packages — `./src/screen.8bs`
|
|
16
|
+
// behind @8bitscript/screen and `./src/text.8bs` behind @8bitscript/text,
|
|
17
|
+
// named by the "8bitscript".exports map in package.json — which are built
|
|
18
|
+
// on what this file exports and on nothing else. Where a Commodore
|
|
19
|
+
// package's hardware surface is a set of registers, the NES's is a
|
|
20
|
+
// PROTOCOL: the PPU's memory is not CPU-addressable at all, so the two
|
|
21
|
+
// functions here are the port sequence every VRAM access on this machine
|
|
22
|
+
// has to perform, and the two rules that come with it.
|
|
23
|
+
//
|
|
24
|
+
// ---- VRAM through the PPU's port -----------------------------------------
|
|
25
|
+
//
|
|
26
|
+
// Screen memory is not a fixed CPU address: the nametable lives in the
|
|
27
|
+
// PPU's own 16 KiB address space, reached only through an indirect port —
|
|
28
|
+
// read PPUSTATUS ($2002) to reset the shared address/scroll write toggle,
|
|
29
|
+
// write PPUADDR ($2006) twice (high byte, then low) to latch a VRAM address,
|
|
30
|
+
// then write PPUDATA ($2007), which stores through it and auto-increments.
|
|
31
|
+
// Two rules that protocol brings, both of which the screen and text
|
|
32
|
+
// modules honour so callers need not:
|
|
33
|
+
//
|
|
34
|
+
// 1. Writing PPUADDR moves the PPU's internal address register, which is
|
|
35
|
+
// also what it scrolls from when rendering resumes — leave it pointing
|
|
36
|
+
// into the middle of the nametable and the next frame draws from
|
|
37
|
+
// there. Every text.putChar() and screen.setColors() ends by resetting
|
|
38
|
+
// the scroll (PPUCTRL $2000 = 0, then PPUSCROLL $2005 = 0 twice), so
|
|
39
|
+
// the picture stays anchored at the top-left. Invisible while every
|
|
40
|
+
// tile was blank; not once there is something on screen to shift.
|
|
41
|
+
//
|
|
42
|
+
// 2. While rendering is ON, the PPU owns VRAM outside vertical blank,
|
|
43
|
+
// and a write outside it lands wherever the PPU's address happens to
|
|
44
|
+
// be — a corrupted tile and a torn frame. Vertical blank is about
|
|
45
|
+
// 2270 CPU cycles, and a program cannot be asked to know that. So
|
|
46
|
+
// the text and palette writes do not go to the PPU when a program
|
|
47
|
+
// makes them: they go into the queue below, as runs of bytes at a
|
|
48
|
+
// VRAM address, and `nesVerticalBlank()` delivers the whole queue
|
|
49
|
+
// at the start of the next vertical blank — packages/backend-6502's
|
|
50
|
+
// frame runtime calls it right after every hardware frame edge
|
|
51
|
+
// (FRAME_SYNC.nes.frameHook), before waitFrame() returns. Delivery
|
|
52
|
+
// is a tight loop, about 15 cycles a byte — but that number alone
|
|
53
|
+
// is what let this queue overrun the blank in the first place: it
|
|
54
|
+
// prices the data bytes and forgets each run's own header (a
|
|
55
|
+
// setVramAddress() call plus its two-write PPUADDR protocol), which
|
|
56
|
+
// is not free. A HUD of a few wide rows is nearly all data bytes and
|
|
57
|
+
// pays that header cost once or twice; a grid of many narrow fields
|
|
58
|
+
// (2048's 4-character tile cells, three text.print() runs per tile)
|
|
59
|
+
// pays it on almost every run. QUEUE_SIZE was 128 — "about four
|
|
60
|
+
// 28-column rows," sized against the wide-row case — until 2048's
|
|
61
|
+
// 16-tile board (packages/2048, one run per tile row, ~19 runs to
|
|
62
|
+
// fill one queue) proved by measurement that a full 128-byte queue
|
|
63
|
+
// of small runs delivers a few cycles past the 2270-cycle blank,
|
|
64
|
+
// corrupting whichever tile was mid-flight when the PPU's own
|
|
65
|
+
// rendering resumed under it (FCEUX, `8bs run nes --screenshot`,
|
|
66
|
+
// 2026-09-07: 128 corrupted two tiles, every size 120 and under
|
|
67
|
+
// across both a two-tile and a full sixteen-tile board did not, 124
|
|
68
|
+
// did). 112 is the fixed value now: comfortable under the observed
|
|
69
|
+
// failure band, not the exact edge of it, since neither this
|
|
70
|
+
// project's timing model nor FCEUX's is being trusted to a single
|
|
71
|
+
// cycle. A future program with even smaller, more numerous runs
|
|
72
|
+
// than 2048's could still find a new floor; measure again with
|
|
73
|
+
// `--screenshot` before assuming this one holds. more than a full
|
|
74
|
+
// queue in one frame is delivered as it overflows, which may tear.
|
|
75
|
+
// screen.blank() switches rendering off and writes the nametable
|
|
76
|
+
// directly, delivering the queue first so nothing printed before
|
|
77
|
+
// the blank appears after it.
|
|
78
|
+
//
|
|
79
|
+
// Nametable and attribute contents are undefined at power-on (real
|
|
80
|
+
// hardware: garbage; FCEUX: zeros, which is why a program that forgot to
|
|
81
|
+
// clear would look fine in the emulator and wrong on a cartridge) — a
|
|
82
|
+
// program should clear the screen once before screen.setColors(), as
|
|
83
|
+
// examples/borders does; the attribute table is cleared by screen.8bs,
|
|
84
|
+
// since a program has no other way to reach it.
|
|
85
|
+
//
|
|
86
|
+
// PPU warm-up: NESdev's guidance is to wait for two vertical blanks after
|
|
87
|
+
// reset before writing the PPU at all, since the chip is not stable for
|
|
88
|
+
// roughly the first 29,658 CPU cycles. LLVM-MOS's NROM start-up code does
|
|
89
|
+
// exactly that before main() — verified by disassembling mos-platform/
|
|
90
|
+
// nes-nrom/lib/crt0.o: __early_init writes $2000/$2001/$4010 = 0, clears
|
|
91
|
+
// $2002, and spins on it until bit 7 sets; __late_init spins on it once
|
|
92
|
+
// more after RAM is initialised — so by the time a program's main() runs
|
|
93
|
+
// the PPU has seen two vblanks and is safe to write. (An earlier revision
|
|
94
|
+
// of this comment recorded that wait as a gap this target still had; it
|
|
95
|
+
// was never one.)
|
|
96
|
+
|
|
97
|
+
// PPUSTATUS read resets the PPUADDR/PPUSCROLL write toggle so the two
|
|
98
|
+
// PPUADDR writes land as a fresh address rather than as a stray second
|
|
99
|
+
// half of whatever came before.
|
|
100
|
+
export function setVramAddress(address: usmallint): void {
|
|
101
|
+
memory.read(0x2002);
|
|
102
|
+
memory.write(0x2006, address / 256);
|
|
103
|
+
memory.write(0x2006, address % 256);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// PPUCTRL 0: nametable $2000, PPUDATA increments by 1, background patterns
|
|
107
|
+
// from table 0 (where the font is), NMI off — this target polls PPUSTATUS
|
|
108
|
+
// rather than taking the interrupt. Then PPUSCROLL x = 0, y = 0.
|
|
109
|
+
export function resetScroll(): void {
|
|
110
|
+
memory.write(0x2000, 0x00);
|
|
111
|
+
memory.write(0x2005, 0x00);
|
|
112
|
+
memory.write(0x2005, 0x00);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- the write queue -------------------------------------------------------
|
|
116
|
+
//
|
|
117
|
+
// Runs of bytes for the PPU, each a two-byte VRAM address (high, low), a
|
|
118
|
+
// count, and the bytes: `queueRun(address)` opens one, `queueByte(value)`
|
|
119
|
+
// adds to it, and `nesVerticalBlank()` writes them all out in order and
|
|
120
|
+
// resets the scroll. Rule 2 above says why.
|
|
121
|
+
const QUEUE_SIZE: utinyint = 112;
|
|
122
|
+
let queue: array<utinyint, QUEUE_SIZE>;
|
|
123
|
+
let queueEnd: utinyint = 0;
|
|
124
|
+
let runCount: utinyint = 0; // where the open run's count byte is
|
|
125
|
+
let runAddress: usmallint = 0; // the open run's VRAM address
|
|
126
|
+
|
|
127
|
+
export function queueRun(address: usmallint): void {
|
|
128
|
+
if (queueEnd > QUEUE_SIZE - 4) {
|
|
129
|
+
deliverAtVerticalBlank(); // no room for a header and a byte
|
|
130
|
+
}
|
|
131
|
+
queue[queueEnd] = address / 256;
|
|
132
|
+
queue[queueEnd + 1] = address % 256;
|
|
133
|
+
runCount = queueEnd + 2;
|
|
134
|
+
queue[runCount] = 0;
|
|
135
|
+
queueEnd = queueEnd + 3;
|
|
136
|
+
runAddress = address;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function queueByte(value: utinyint): void {
|
|
140
|
+
if (queueEnd == QUEUE_SIZE) {
|
|
141
|
+
// Full: deliver what there is and reopen the run where it left off.
|
|
142
|
+
queueRun(runAddress + queue[runCount]);
|
|
143
|
+
}
|
|
144
|
+
queue[queueEnd] = value;
|
|
145
|
+
queueEnd++;
|
|
146
|
+
queue[runCount]++;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The queue is full before the frame is over: wait for the next vertical
|
|
150
|
+
// blank and deliver it there. The program loses the rest of this frame —
|
|
151
|
+
// a HUD too big for one frame costs frames, it never costs the picture.
|
|
152
|
+
// Delivering mid-frame instead would corrupt it: PPUADDR is the PPU's
|
|
153
|
+
// own fetch position while it draws, and PPUDATA writes move it. The
|
|
154
|
+
// status read first clears a vertical-blank flag left over from earlier,
|
|
155
|
+
// so the loop waits for the edge and not for a stale flag.
|
|
156
|
+
function deliverAtVerticalBlank(): void {
|
|
157
|
+
memory.read(0x2002);
|
|
158
|
+
while (memory.read(0x2002) < 128) {
|
|
159
|
+
}
|
|
160
|
+
nesVerticalBlank();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Every queued run to the PPU, then the scroll back to the top-left (each
|
|
164
|
+
// run's PPUADDR write moved it). Called by the frame runtime at the start
|
|
165
|
+
// of vertical blank; by deliverAtVerticalBlank() when the queue is full;
|
|
166
|
+
// by screen.blank() with rendering off.
|
|
167
|
+
export function nesVerticalBlank(): void {
|
|
168
|
+
let i: utinyint = 0;
|
|
169
|
+
while (i < queueEnd) {
|
|
170
|
+
setVramAddress(queue[i] * 256 + queue[i + 1]);
|
|
171
|
+
let n: utinyint = queue[i + 2];
|
|
172
|
+
i = i + 3;
|
|
173
|
+
// Indexed straight off `i`, one byte at a time: an `i + k` index
|
|
174
|
+
// made LLVM-MOS walk a pointer at 23 cycles a byte; this is 15.
|
|
175
|
+
while (n > 0) {
|
|
176
|
+
memory.write(0x2007, queue[i]);
|
|
177
|
+
i++;
|
|
178
|
+
n--;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (queueEnd > 0) {
|
|
182
|
+
queueEnd = 0;
|
|
183
|
+
resetScroll();
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/input.8bs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// @8bitscript/nes/input — the NES behind @8bitscript/input.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./input"] in this package's package.json,
|
|
4
|
+
// and by "8bitscript".entry.nes 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 NES. The pad itself is ./pad.8bs, which stays
|
|
10
|
+
// available to a program that wants both controllers or the buttons this
|
|
11
|
+
// file does not map.
|
|
12
|
+
//
|
|
13
|
+
// ---- what this machine has, and what it does not -------------------------
|
|
14
|
+
//
|
|
15
|
+
// This is the machine that makes the portable input surface worth having,
|
|
16
|
+
// because it has none of what the others have. **No keyboard** — the fact
|
|
17
|
+
// sheet says so (`input.keyboard` is false) and Studio reads that fact to
|
|
18
|
+
// decide the NES is a viewer. **No pointer** — there is no mouse for a
|
|
19
|
+
// stock NES, so `pointer()` is a constant false and everything that hangs
|
|
20
|
+
// off it folds away. What there is, is a D-pad, and a D-pad is exactly
|
|
21
|
+
// what a menu bar needs.
|
|
22
|
+
//
|
|
23
|
+
// So the mapping is the whole file:
|
|
24
|
+
//
|
|
25
|
+
// - **left / right / up / down** — the D-pad on controller 1.
|
|
26
|
+
// - **confirm** — A, or START. START is included because on a console
|
|
27
|
+
// the button that opens a menu is the one people press to commit to
|
|
28
|
+
// something in it, and A is the one they press by reflex.
|
|
29
|
+
// - **cancel** — B.
|
|
30
|
+
//
|
|
31
|
+
// Controller 2 is read by ./pad.8bs and ignored here: the portable surface
|
|
32
|
+
// describes one user driving one interface. A two-player program wants the
|
|
33
|
+
// pad layer directly.
|
|
34
|
+
//
|
|
35
|
+
// ---- edges, not levels ---------------------------------------------------
|
|
36
|
+
//
|
|
37
|
+
// Every answer here is **edge-triggered**: true on the one frame the press
|
|
38
|
+
// begins, false while it is held. Holding right does not race the highlight
|
|
39
|
+
// across the bar. `poll()` must be called exactly once a frame, right after
|
|
40
|
+
// waitFrame() — twice a frame and every press is seen once and swallowed.
|
|
41
|
+
import { pad, Pad } from "./pad.8bs";
|
|
42
|
+
|
|
43
|
+
// One bit per thing a program can ask about, so a frame's input is one
|
|
44
|
+
// byte and an edge is one AND. The same seven bits as every other
|
|
45
|
+
// machine's input layer; this one never sets BUTTON.
|
|
46
|
+
namespace Edge {
|
|
47
|
+
const LEFT: utinyint = 1;
|
|
48
|
+
const RIGHT: utinyint = 2;
|
|
49
|
+
const UP: utinyint = 4;
|
|
50
|
+
const DOWN: utinyint = 8;
|
|
51
|
+
const CONFIRM: utinyint = 16;
|
|
52
|
+
const CANCEL: utinyint = 32;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let held: utinyint = 0;
|
|
56
|
+
let before: utinyint = 0;
|
|
57
|
+
let began: utinyint = 0;
|
|
58
|
+
|
|
59
|
+
export namespace input {
|
|
60
|
+
|
|
61
|
+
// Nothing to set up on this machine: the pad needs no configuration
|
|
62
|
+
// and there is no pointer to give a ceiling to. Here so that a program
|
|
63
|
+
// written for nine machines calls the same thing on all of them.
|
|
64
|
+
function begin(): void {
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Strobe the pad and work out what began this frame. Once a frame,
|
|
68
|
+
// right after waitFrame().
|
|
69
|
+
function poll(): void {
|
|
70
|
+
pad.scan();
|
|
71
|
+
|
|
72
|
+
before = held;
|
|
73
|
+
held = 0;
|
|
74
|
+
|
|
75
|
+
// The D-pad's four bits and the pad's own bit layout differ, so
|
|
76
|
+
// this is a translation rather than a mask — and it is four
|
|
77
|
+
// compares rather than a table because a table would cost a byte
|
|
78
|
+
// of RAM per entry to save nothing.
|
|
79
|
+
let buttons: utinyint = pad.bits(Pad.PORT_1);
|
|
80
|
+
if ((buttons & Pad.LEFT) != 0) {
|
|
81
|
+
held = held | Edge.LEFT;
|
|
82
|
+
}
|
|
83
|
+
if ((buttons & Pad.RIGHT) != 0) {
|
|
84
|
+
held = held | Edge.RIGHT;
|
|
85
|
+
}
|
|
86
|
+
if ((buttons & Pad.UP) != 0) {
|
|
87
|
+
held = held | Edge.UP;
|
|
88
|
+
}
|
|
89
|
+
if ((buttons & Pad.DOWN) != 0) {
|
|
90
|
+
held = held | Edge.DOWN;
|
|
91
|
+
}
|
|
92
|
+
if ((buttons & Pad.A) != 0 || (buttons & Pad.START) != 0) {
|
|
93
|
+
held = held | Edge.CONFIRM;
|
|
94
|
+
}
|
|
95
|
+
if ((buttons & Pad.B) != 0) {
|
|
96
|
+
held = held | Edge.CANCEL;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
began = held & (before ^ 0xFF);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function left(): bool {
|
|
103
|
+
return (began & Edge.LEFT) != 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function right(): bool {
|
|
107
|
+
return (began & Edge.RIGHT) != 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function up(): bool {
|
|
111
|
+
return (began & Edge.UP) != 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function down(): bool {
|
|
115
|
+
return (began & Edge.DOWN) != 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function confirm(): bool {
|
|
119
|
+
return (began & Edge.CONFIRM) != 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cancel(): bool {
|
|
123
|
+
return (began & Edge.CANCEL) != 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// There is no pointer on an NES, and this is a constant — so a
|
|
127
|
+
// component's `if (input.pointer())` costs this machine nothing at
|
|
128
|
+
// all, which is the point of answering the question rather than
|
|
129
|
+
// leaving the call out.
|
|
130
|
+
function pointer(): bool {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function pointerCell(): usmallint {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function pointerButton(): bool {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/pad.8bs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// @8bitscript/nes/pad — the two controller ports, one snapshot a frame.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./pad"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { pad, Pad } from "@8bitscript/nes/pad";
|
|
6
|
+
//
|
|
7
|
+
// pad.scan();
|
|
8
|
+
// if (pad.pressed(Pad.PORT_1, Pad.RIGHT)) { ... }
|
|
9
|
+
//
|
|
10
|
+
// Hardware-level, NES-only surface — the layer @8bitscript/nes/input sits
|
|
11
|
+
// on, the way ./text.8bs sits under @8bitscript/text. Importing it makes a
|
|
12
|
+
// program NES-specific. It keeps no history and decodes nothing; it
|
|
13
|
+
// answers "is this button down now", which is what a frame loop asks.
|
|
14
|
+
//
|
|
15
|
+
// ---- how a controller talks ----------------------------------------------
|
|
16
|
+
//
|
|
17
|
+
// A standard controller is an 8-bit parallel-to-serial shift register, not
|
|
18
|
+
// a set of addressable switches. Writing 1 then 0 to JOY1 ($4016) strobes
|
|
19
|
+
// *both* ports: the 1 loads each pad's eight buttons into its register,
|
|
20
|
+
// the 0 lets them be clocked out. After that, each read of $4016 returns
|
|
21
|
+
// pad 1's next button in bit 0 and each read of $4017 returns pad 2's, in
|
|
22
|
+
// this fixed order:
|
|
23
|
+
//
|
|
24
|
+
// A, B, SELECT, START, UP, DOWN, LEFT, RIGHT
|
|
25
|
+
//
|
|
26
|
+
// A 1 bit is a button held down. Only bit 0 carries a button — the upper
|
|
27
|
+
// bits are open bus on the NES and expansion-port data on a Famicom — so
|
|
28
|
+
// every read is masked. Eight reads of two ports is about 200 cycles of a
|
|
29
|
+
// 29780-cycle frame.
|
|
30
|
+
//
|
|
31
|
+
// `scan()` shifts each read into the top of a byte and shifts the byte
|
|
32
|
+
// down, so after eight rounds the first button read (A) has travelled to
|
|
33
|
+
// bit 0 and the last (RIGHT) sits at bit 7 — which is the `Pad.*` layout
|
|
34
|
+
// below, and why there is no bit table here the way the C64's keyboard
|
|
35
|
+
// needs one.
|
|
36
|
+
//
|
|
37
|
+
// ---- two things this file does not do, on purpose ------------------------
|
|
38
|
+
//
|
|
39
|
+
// **It does not re-read to defend against DMC corruption.** When the APU's
|
|
40
|
+
// sample channel fetches a byte it can steal a cycle from a controller
|
|
41
|
+
// read and drop or double-clock a bit — the reason commercial games read
|
|
42
|
+
// the pad twice and keep the reading that agrees. Nothing in 8BitScript
|
|
43
|
+
// drives the DMC channel today, so the defence would cost every program
|
|
44
|
+
// cycles to guard against a channel no program can turn on. **When sample
|
|
45
|
+
// playback arrives on this target, this is the file that has to grow the
|
|
46
|
+
// double read**, and that is the note to find here when it does.
|
|
47
|
+
//
|
|
48
|
+
// **It does not know about a four-player adapter, a Zapper, or a Power
|
|
49
|
+
// Pad.** Those change what the extra bits after the eighth read mean; this
|
|
50
|
+
// reads eight and stops.
|
|
51
|
+
//
|
|
52
|
+
// Written from the documented strobe-then-eight-reads protocol (NESdev's
|
|
53
|
+
// "Standard controller" and "Controller port registers"), which is the
|
|
54
|
+
// sequence every NES program has used since 1983. **Not yet confirmed on
|
|
55
|
+
// screen under a controller** — `--screenshot` builds and captures a
|
|
56
|
+
// frame but cannot press a button, so the button order below is the
|
|
57
|
+
// documented one and not a measured one. Anyone who runs
|
|
58
|
+
// examples/first-look/menubar under FCEUX with a pad should replace this
|
|
59
|
+
// paragraph with what they saw.
|
|
60
|
+
|
|
61
|
+
// The two ports. JOY2's *write* side is the APU's frame counter, which is
|
|
62
|
+
// why nothing here ever writes it — the strobe goes to JOY1 alone and
|
|
63
|
+
// latches both pads.
|
|
64
|
+
const JOY1: usmallint = 0x4016;
|
|
65
|
+
const JOY2: usmallint = 0x4017;
|
|
66
|
+
|
|
67
|
+
// The last scan: index 0 is port 1, index 1 is port 2; bits as in `Pad`.
|
|
68
|
+
let state: array<utinyint, 2>;
|
|
69
|
+
|
|
70
|
+
export namespace Pad {
|
|
71
|
+
const PORT_1: utinyint = 0;
|
|
72
|
+
const PORT_2: utinyint = 1;
|
|
73
|
+
|
|
74
|
+
// In the order the shift register clocks them out, which is the order
|
|
75
|
+
// the bits end up in after `scan()`.
|
|
76
|
+
const A: utinyint = 1;
|
|
77
|
+
const B: utinyint = 2;
|
|
78
|
+
const SELECT: utinyint = 4;
|
|
79
|
+
const START: utinyint = 8;
|
|
80
|
+
const UP: utinyint = 16;
|
|
81
|
+
const DOWN: utinyint = 32;
|
|
82
|
+
const LEFT: utinyint = 64;
|
|
83
|
+
const RIGHT: utinyint = 128;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export namespace pad {
|
|
87
|
+
// Strobe both pads and clock all sixteen buttons in. Once a frame,
|
|
88
|
+
// right after waitFrame().
|
|
89
|
+
function scan(): void {
|
|
90
|
+
memory.write(JOY1, 1);
|
|
91
|
+
memory.write(JOY1, 0);
|
|
92
|
+
// Both ports are clocked by the same eight reads, so they are read
|
|
93
|
+
// together rather than in two loops — one strobe, one pass.
|
|
94
|
+
let one: utinyint = 0;
|
|
95
|
+
let two: utinyint = 0;
|
|
96
|
+
for (let i: utinyint = 0; i < 8; i++) {
|
|
97
|
+
one = one >> 1;
|
|
98
|
+
if ((memory.read(JOY1) & 1) != 0) {
|
|
99
|
+
one = one | 128;
|
|
100
|
+
}
|
|
101
|
+
two = two >> 1;
|
|
102
|
+
if ((memory.read(JOY2) & 1) != 0) {
|
|
103
|
+
two = two | 128;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
state[Pad.PORT_1] = one;
|
|
107
|
+
state[Pad.PORT_2] = two;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The eight buttons of one port as `Pad.*` bits, 1 = held down.
|
|
111
|
+
function bits(port: utinyint): utinyint {
|
|
112
|
+
return state[port];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Is this button — one `Pad.*` bit, or several or'd together — down in
|
|
116
|
+
// the last scan? Several bits ask "any of these".
|
|
117
|
+
function pressed(port: utinyint, button: utinyint): bool {
|
|
118
|
+
return (state[port] & button) != 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/pointer.8bs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @8bitscript/nes/pointer — the NES behind @8bitscript/pointer.
|
|
2
|
+
//
|
|
3
|
+
// Every call here does nothing and `DRAWS` is false. The PPU has 64
|
|
4
|
+
// sprites and could draw an arrow without difficulty, so this is not the
|
|
5
|
+
// drawing half that is missing — **it is that a stock NES has nothing to
|
|
6
|
+
// move one with.** @8bitscript/nes/input reports a D-pad and buttons and
|
|
7
|
+
// no pointer, because the console this repository builds for has two
|
|
8
|
+
// controller ports and a cartridge, and a pad is not a pointer: a cursor
|
|
9
|
+
// driven by a D-pad is a selection moving between items, which is what
|
|
10
|
+
// `menubar.next()` and `previous()` already are, and dressing that up as
|
|
11
|
+
// an arrow would be a worse interface, not a better one.
|
|
12
|
+
//
|
|
13
|
+
// The machine that *would* answer is a Zapper or a Power Pad, and neither
|
|
14
|
+
// is a mouse either. A Famicom mouse existed and the hardware option for
|
|
15
|
+
// one does not: packages/nes's catalog has no controller axis at all yet
|
|
16
|
+
// (its mapper is still hard-wired — see packages/nes/AGENTS.md), so
|
|
17
|
+
// fitting one would be a new option there first, then an input driver, and
|
|
18
|
+
// only then this file.
|
|
19
|
+
//
|
|
20
|
+
// It costs the NES nothing: `DRAWS` is a const and every function is
|
|
21
|
+
// empty, so a program's `if (pointer.DRAWS)` folds away and LLVM deletes
|
|
22
|
+
// the calls — which matters most here, where a program has 2 KiB of RAM
|
|
23
|
+
// and every byte of the cartridge is budgeted.
|
|
24
|
+
export namespace pointer {
|
|
25
|
+
|
|
26
|
+
const DRAWS: bool = false;
|
|
27
|
+
|
|
28
|
+
function begin(): void {
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setColor(color: utinyint): void {
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function update(): void {
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hide(): void {
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/screen.8bs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// @8bitscript/nes/screen — the NES'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 NES: @8bitscript/screen's entry is
|
|
9
|
+
// keyed by machine and delegates here. Built on the PPU port protocol
|
|
10
|
+
// @8bitscript/nes exports — read that file first for the two VRAM rules
|
|
11
|
+
// this one lives by. Every machine's screen.8bs exports this same surface,
|
|
12
|
+
// which is what lets a program import it from @8bitscript/screen and never
|
|
13
|
+
// name the hardware.
|
|
14
|
+
import { setVramAddress, resetScroll, queueRun, queueByte, nesVerticalBlank } from "./index.8bs";
|
|
15
|
+
|
|
16
|
+
// ---- what "border" means on a machine that has none --------------------
|
|
17
|
+
//
|
|
18
|
+
// The NES has no border-colour register. There is no separate border area
|
|
19
|
+
// for one to colour: the PPU's 256x240 picture IS the whole frame, edge to
|
|
20
|
+
// edge, and every one of those pixels comes from a background tile or a
|
|
21
|
+
// sprite (or, where neither covers it, the universal backdrop colour). VIC,
|
|
22
|
+
// VIC-II and GTIA all draw a picture smaller than the raster and paint the
|
|
23
|
+
// margin around it from a dedicated register; the PPU simply has no
|
|
24
|
+
// margin. So the border cannot be a register write here, the way it is on
|
|
25
|
+
// every other target.
|
|
26
|
+
//
|
|
27
|
+
// It is real anyway. This module DRAWS a border: on the first setColors()
|
|
28
|
+
// call it lays a two-tile-thick frame of a solid tile around the edge of
|
|
29
|
+
// the nametable (rows 0-1 and 28-29, columns 0-1 and 30-31), and `border`
|
|
30
|
+
// is the palette entry that solid tile takes its colour from. Two tiles
|
|
31
|
+
// thick, not one, because NTSC televisions — and FCEUX, whose default NTSC
|
|
32
|
+
// view drops the top and bottom 8 lines — overscan the outer tile row
|
|
33
|
+
// away; the inner row survives. The text grid a program draws on (see
|
|
34
|
+
// text.8bs) is the area INSIDE that frame: 28 columns by 26 rows, whose
|
|
35
|
+
// cell 0 is nametable row 2, column 2 — the top-left corner, against the
|
|
36
|
+
// border, which is exactly what cell 0 is on every other target. A program
|
|
37
|
+
// never addresses the frame's own tiles, and never needs to know they
|
|
38
|
+
// exist. The four values setColors() writes are, in the PPU's own terms:
|
|
39
|
+
//
|
|
40
|
+
// $3F00 universal backdrop `background`
|
|
41
|
+
// $3F01 colour index 1 $30 (white) — every glyph in the font
|
|
42
|
+
// $3F02 colour index 2 `border` — the solid frame tile
|
|
43
|
+
// $3F03 colour index 3 unused
|
|
44
|
+
//
|
|
45
|
+
// all in background palette 0, which every tile on screen uses (the
|
|
46
|
+
// attribute table is zeroed by the same first call, so no 32x32-pixel block
|
|
47
|
+
// picks another palette). Both colours are NES palette indices — $00-$3F,
|
|
48
|
+
// the 2C02's own colour numbers; $0F is black, $30 white, row $0x dark
|
|
49
|
+
// through row $3x pale — not RGB and not the Commodore/Atari values.
|
|
50
|
+
//
|
|
51
|
+
// The CPU cannot read the PPU's mask register back, so `renderingEnabled`
|
|
52
|
+
// below is this module's own record of having switched rendering on: it
|
|
53
|
+
// is what lets setColors() lay the frame exactly once, on the call that
|
|
54
|
+
// is by construction the last moment a large VRAM write is free.
|
|
55
|
+
|
|
56
|
+
let renderingEnabled: u8 = 0;
|
|
57
|
+
|
|
58
|
+
// `count` consecutive nametable cells from `address`, one PPUADDR set and
|
|
59
|
+
// then PPUDATA's auto-increment — the cheap way to write a run.
|
|
60
|
+
function fillRun(address: usmallint, count: usmallint, value: utinyint): void {
|
|
61
|
+
setVramAddress(address);
|
|
62
|
+
for (let i: usmallint = 0; i < count; i++) {
|
|
63
|
+
memory.write(0x2007, value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The drawn border: rows 0-1 and 28-29 in full, columns 0-1 and 30-31 of
|
|
68
|
+
// every row between, all tile $80 (solid colour index 2 — see font.s).
|
|
69
|
+
// Then the 64-byte attribute table at $23C0, zeroed: palette 0 everywhere.
|
|
70
|
+
function layFrame(): void {
|
|
71
|
+
fillRun(0x2000, 64, 0x80);
|
|
72
|
+
fillRun(0x2380, 64, 0x80);
|
|
73
|
+
for (let row: usmallint = 2; row < 28; row++) {
|
|
74
|
+
fillRun(0x2000 + row * 32, 2, 0x80);
|
|
75
|
+
fillRun(0x2000 + row * 32 + 30, 2, 0x80);
|
|
76
|
+
}
|
|
77
|
+
fillRun(0x23C0, 64, 0x00);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Switch the picture on, the first time anything asks for one: the frame
|
|
81
|
+
// and attribute table go in with rendering off (a few hundred VRAM
|
|
82
|
+
// writes, no vblank budget to fit), the text palette entry is set, and
|
|
83
|
+
// rendering starts at the next vertical blank — switching it on mid-frame
|
|
84
|
+
// would draw the rest of that frame from wherever the PPU's address
|
|
85
|
+
// register sits. Every later call returns at once.
|
|
86
|
+
function showPicture(): void {
|
|
87
|
+
if (renderingEnabled == 1) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
layFrame();
|
|
91
|
+
setVramAddress(0x3F01);
|
|
92
|
+
memory.write(0x2007, 0x30); // $3F01: text — white
|
|
93
|
+
resetScroll();
|
|
94
|
+
while (memory.read(0x2002) < 128) {
|
|
95
|
+
}
|
|
96
|
+
renderingEnabled = 1;
|
|
97
|
+
// PPUMASK: bit 3 shows the background, bit 1 shows it in the leftmost
|
|
98
|
+
// 8 pixels too — otherwise column 0, the outer edge of the frame, would
|
|
99
|
+
// be clipped to the backdrop colour. Sprites stay off.
|
|
100
|
+
memory.write(0x2001, 0x0A);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// One palette entry. With the picture on it goes through the write queue
|
|
104
|
+
// and lands at the next vertical blank, like text (see index.8bs); with
|
|
105
|
+
// the picture off the PPU is free and it is written now.
|
|
106
|
+
function setPalette(entry: usmallint, color: utinyint): void {
|
|
107
|
+
if (renderingEnabled == 1) {
|
|
108
|
+
queueRun(entry);
|
|
109
|
+
queueByte(color);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
setVramAddress(entry);
|
|
113
|
+
memory.write(0x2007, color);
|
|
114
|
+
resetScroll();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export namespace screen {
|
|
118
|
+
function setColors(border: u8, background: u8): void {
|
|
119
|
+
setPalette(0x3F00, background); // $3F00: the universal backdrop
|
|
120
|
+
setPalette(0x3F02, border); // $3F02: the frame tile
|
|
121
|
+
showPicture();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `blank(border, background)`: every cell blank and both colours set —
|
|
125
|
+
// black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
|
|
126
|
+
// to leave one as it is. `setBorder` and `setBackground` are one
|
|
127
|
+
// colour each, `setColors` the pair; `text.setColor` owns what the
|
|
128
|
+
// next print looks like.
|
|
129
|
+
function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
|
|
130
|
+
// The nametable can only be filled in bulk with the picture off.
|
|
131
|
+
// Before the first call that shows it, it already is; after, this
|
|
132
|
+
// switches it off, fills, and switches it back on at the next
|
|
133
|
+
// vertical blank — one dark frame, what every NES game does on a
|
|
134
|
+
// scene change. Cells are the 28x26 area inside the frame; tile 32
|
|
135
|
+
// is the blank, as putChar's space is.
|
|
136
|
+
let wasShowing: utinyint = renderingEnabled;
|
|
137
|
+
if (wasShowing == 1) {
|
|
138
|
+
memory.write(0x2001, 0x00);
|
|
139
|
+
renderingEnabled = 0; // the PPU is free until this switches it back on
|
|
140
|
+
}
|
|
141
|
+
nesVerticalBlank(); // whatever was printed before this, with the PPU free
|
|
142
|
+
if (border != BorderColor.KEEP) {
|
|
143
|
+
setPalette(0x3F02, border);
|
|
144
|
+
}
|
|
145
|
+
if (background != BackgroundColor.KEEP) {
|
|
146
|
+
setPalette(0x3F00, background);
|
|
147
|
+
}
|
|
148
|
+
for (let row: usmallint = 2; row < 28; row++) {
|
|
149
|
+
fillRun(0x2000 + row * 32 + 2, 28, 32);
|
|
150
|
+
}
|
|
151
|
+
resetScroll();
|
|
152
|
+
if (wasShowing == 1) {
|
|
153
|
+
while (memory.read(0x2002) < 128) {
|
|
154
|
+
}
|
|
155
|
+
renderingEnabled = 1;
|
|
156
|
+
memory.write(0x2001, 0x0A);
|
|
157
|
+
} else {
|
|
158
|
+
showPicture();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function setBackground(background: u8): void {
|
|
163
|
+
setPalette(0x3F00, background);
|
|
164
|
+
showPicture();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function setBorder(border: u8): void {
|
|
168
|
+
setPalette(0x3F02, border);
|
|
169
|
+
showPicture();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---- colour names ----------------------------------------------------------
|
|
174
|
+
//
|
|
175
|
+
// The same eight names every machine's screen.8bs exports — Black, White,
|
|
176
|
+
// Red, Cyan, Purple, Green, Blue, Yellow — so a program can write
|
|
177
|
+
// `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` through
|
|
178
|
+
// @8bitscript/screen and get blue on every target. The values are 2C02
|
|
179
|
+
// palette indices, by the hue columns of NESdev's palette chart ($x2 blue,
|
|
180
|
+
// $x4 purple, $x6 red, $x8 yellow, $xA green, $xC cyan; $0F black, $30
|
|
181
|
+
// white), at the mid-brightness row $1x where the hue is unmistakable,
|
|
182
|
+
// except cyan and yellow, whose $1x entries are too dark to read as their
|
|
183
|
+
// names and so come from row $2x. A program that wants a colour not named
|
|
184
|
+
// here passes its own index to setColors() directly. Not visually verified
|
|
185
|
+
// under FCEUX by this project; the chart is NESdev's.
|
|
186
|
+
|
|
187
|
+
export namespace BorderColor {
|
|
188
|
+
const BLACK: utinyint = 0x0F;
|
|
189
|
+
const WHITE: utinyint = 0x30;
|
|
190
|
+
const RED: utinyint = 0x16;
|
|
191
|
+
const CYAN: utinyint = 0x2C;
|
|
192
|
+
const PURPLE: utinyint = 0x14;
|
|
193
|
+
const GREEN: utinyint = 0x1A;
|
|
194
|
+
const BLUE: utinyint = 0x12;
|
|
195
|
+
const YELLOW: utinyint = 0x28;
|
|
196
|
+
// Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
|
|
197
|
+
// a value no register here takes.
|
|
198
|
+
const KEEP: utinyint = 255;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export namespace BackgroundColor {
|
|
202
|
+
const BLACK: utinyint = 0x0F;
|
|
203
|
+
const WHITE: utinyint = 0x30;
|
|
204
|
+
const RED: utinyint = 0x16;
|
|
205
|
+
const CYAN: utinyint = 0x2C;
|
|
206
|
+
const PURPLE: utinyint = 0x14;
|
|
207
|
+
const GREEN: utinyint = 0x1A;
|
|
208
|
+
const BLUE: utinyint = 0x12;
|
|
209
|
+
const YELLOW: utinyint = 0x28;
|
|
210
|
+
// Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
|
|
211
|
+
// a value no register here takes.
|
|
212
|
+
const KEEP: utinyint = 255;
|
|
213
|
+
}
|
package/src/text.8bs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// @8bitscript/nes/text — the NES'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 NES. Built on the PPU port protocol
|
|
9
|
+
// @8bitscript/nes exports — read that file first for the two VRAM rules
|
|
10
|
+
// this one lives by. Every machine's text.8bs exports this same namespace —
|
|
11
|
+
// the same names, the same shapes, ASCII codes, and a cell 0 at the
|
|
12
|
+
// top-left corner inside the border — so a program that imports it from
|
|
13
|
+
// @8bitscript/text draws the same thing on every target.
|
|
14
|
+
import { queueRun, queueByte } from "./index.8bs";
|
|
15
|
+
|
|
16
|
+
// ---- text: the character set is ours to ship --------------------------
|
|
17
|
+
//
|
|
18
|
+
// A Commodore or Atari `text.putChar` writes one byte into screen memory
|
|
19
|
+
// and a character ROM in the machine turns it into pixels. The NES has no
|
|
20
|
+
// character ROM: the PPU fetches every 8x8 tile pattern from the cartridge,
|
|
21
|
+
// so a cartridge that shows text must carry its own. This package does —
|
|
22
|
+
// native/6502/font.s, an 8 KiB CHR-ROM listed in package.json's
|
|
23
|
+
// "8bitscript".native and linked into the .nes image by the 6502 backend
|
|
24
|
+
// (it rides along with every import of this package, its entry or any of
|
|
25
|
+
// its subpaths) — laid out so that tile index == ASCII code.
|
|
26
|
+
// `putChar(cell, 65)` therefore draws an 'A' with no translation table.
|
|
27
|
+
// Space, digits, A-Z and `! , - . : ?` have glyphs; other codes in $20-$5F
|
|
28
|
+
// are blank tiles. Reverse video of that range lives at ASCII+128 ($A0-$DF)
|
|
29
|
+
// so `text.setReverse` is the same `code + 128` as on the Commodores. Tile
|
|
30
|
+
// $80 is the solid frame, not a reverse copy, and +128 of a portable
|
|
31
|
+
// character never lands on it. That set — upper case only — is what every
|
|
32
|
+
// machine's putChar takes, precisely because it is what this font has: the
|
|
33
|
+
// portable character set is the smallest one any target can show.
|
|
34
|
+
//
|
|
35
|
+
// The grid is the area inside the frame screen.8bs draws (see there):
|
|
36
|
+
// 28 columns by 26 rows, 728 cells, cell 0 at nametable row 2, column 2,
|
|
37
|
+
// each row 32 bytes apart in the nametable — cell `n` is nametable address
|
|
38
|
+
// $2000 + (2 + n / 28) * 32 + 2 + n % 28. Nothing shows until
|
|
39
|
+
// screen.setColors() has switched rendering on; until then, writes here
|
|
40
|
+
// are free of any vblank budget (rule 2 in @8bitscript/nes), which is why
|
|
41
|
+
// a program clears and labels first and sets colours last. putColor exists
|
|
42
|
+
// so portable code still compiles and does nothing: the PPU has no
|
|
43
|
+
// per-cell colour — the nearest thing, the attribute table, chooses a
|
|
44
|
+
// palette per 32x32-pixel block, and every block here is palette 0.
|
|
45
|
+
|
|
46
|
+
// ---- print: strings and number fields ------------------------------------
|
|
47
|
+
//
|
|
48
|
+
// `text.print(cell, s)` writes a string's characters into consecutive
|
|
49
|
+
// cells from `cell`, and `text.printNumber(cell, value, width)` writes
|
|
50
|
+
// `value` as exactly `width` decimal digits, zero-padded and right-aligned,
|
|
51
|
+
// so a field on a HUD never shifts columns. Both draw in the current colour —
|
|
52
|
+
// white until `text.setColor(TextColor.CYAN)` changes it, and that one
|
|
53
|
+
// call then colours everything printed after it, on the machines that have
|
|
54
|
+
// per-cell colour. They are also the two
|
|
55
|
+
// functions the compiler's template layout targets: `text.print(0,
|
|
56
|
+
// \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
|
|
57
|
+
// (see packages/compiler/src/ir).
|
|
58
|
+
//
|
|
59
|
+
// Under them: `locate()` turns a cell into a
|
|
60
|
+
// nametable address without dividing, and a run of text lets the PPU step
|
|
61
|
+
// the address itself.
|
|
62
|
+
// `text.putChar` and `text.putColor` stay the one-cell pokes a caller can
|
|
63
|
+
// build anything from.
|
|
64
|
+
let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
|
|
65
|
+
let currentReverse: bool = false; // until text.setReverse() says otherwise
|
|
66
|
+
|
|
67
|
+
// Reverse video is ASCII+128: the font ships inverted copies of $20-$5F
|
|
68
|
+
// at $A0-$DF. Tile $80 is the drawn border and is not a reverse copy, so
|
|
69
|
+
// the +128 of a portable character never lands on it. A reverse space is
|
|
70
|
+
// a solid block in the text colour.
|
|
71
|
+
function toTile(code: utinyint): utinyint {
|
|
72
|
+
if (currentReverse) {
|
|
73
|
+
return code + 128;
|
|
74
|
+
}
|
|
75
|
+
return code;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The nametable address of a cell — row `cell / 28`, column the rest — and
|
|
79
|
+
// the column back, for a caller that walks on from there. Eight-bit
|
|
80
|
+
// arithmetic only, and no division or multiplication: LLVM-MOS links a
|
|
81
|
+
// 250-byte routine for a 16-bit divide, turns a counted-subtraction loop
|
|
82
|
+
// into one, and calls a 300-cycle multiply for `x * 147` — none of which
|
|
83
|
+
// fits the vertical blank this runs in. A quarter of the cell fits a byte
|
|
84
|
+
// (below 728, q < 182); q / 8 + q / 64 is q / 7 or one or two short, and
|
|
85
|
+
// what is left over says which. Verified for every cell. It opens a run
|
|
86
|
+
// in the package's write queue at that address (see index.8bs: nothing
|
|
87
|
+
// here touches the PPU; waitFrame() delivers the queue in vertical
|
|
88
|
+
// blank), and the run's bytes land in consecutive cells of the row.
|
|
89
|
+
function locate(cell: usmallint): utinyint {
|
|
90
|
+
let q: utinyint = cell / 4;
|
|
91
|
+
let row: utinyint = q / 8 + q / 64;
|
|
92
|
+
let rest: utinyint = q - row * 8 + row; // q - row * 7, without the multiply
|
|
93
|
+
if (rest >= 7) {
|
|
94
|
+
rest = rest - 7;
|
|
95
|
+
row++;
|
|
96
|
+
}
|
|
97
|
+
if (rest >= 7) {
|
|
98
|
+
rest = rest - 7;
|
|
99
|
+
row++;
|
|
100
|
+
}
|
|
101
|
+
let col: utinyint = rest * 4 + cell % 4;
|
|
102
|
+
queueRun(0x2042 + row * 32 + col);
|
|
103
|
+
return col;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- digits -------------------------------------------------------------
|
|
107
|
+
//
|
|
108
|
+
// A number is written one place at a time, high to low, by subtracting the
|
|
109
|
+
// place value until it no longer fits: the 6502 has no divide instruction,
|
|
110
|
+
// and `value / 10` would link a 250-byte routine to do it. Places above the
|
|
111
|
+
// field are still taken off, so a field narrower than its number shows the
|
|
112
|
+
// low digits; places the number does not reach print as zeros.
|
|
113
|
+
const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
|
|
114
|
+
|
|
115
|
+
export namespace text {
|
|
116
|
+
const CELL_COUNT: usmallint = 728; // 28 columns x 26 rows, inside the frame
|
|
117
|
+
const COLUMNS: utinyint = 28; // cells per row, so cell = y * text.COLUMNS + x
|
|
118
|
+
|
|
119
|
+
function putChar(cell: usmallint, code: utinyint): void {
|
|
120
|
+
locate(cell);
|
|
121
|
+
queueByte(toTile(code));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function putColor(cell: usmallint, color: utinyint): void {
|
|
125
|
+
// Deliberately empty — see above.
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function setColor(color: utinyint): void {
|
|
129
|
+
currentColor = color;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setReverse(on: bool): void {
|
|
133
|
+
currentReverse = on;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function print(cell: usmallint, s: string): void {
|
|
137
|
+
let col: utinyint = locate(cell);
|
|
138
|
+
for (let i: utinyint = 0; i < s.length; i++) {
|
|
139
|
+
queueByte(toTile(s[i]));
|
|
140
|
+
col++;
|
|
141
|
+
if (col == text.COLUMNS) {
|
|
142
|
+
col = locate(cell + i + 1); // the next row is not the next address
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
|
|
148
|
+
let col: utinyint = locate(cell);
|
|
149
|
+
let k: utinyint = width;
|
|
150
|
+
if (k < DIGIT_PLACES.length) {
|
|
151
|
+
k = DIGIT_PLACES.length;
|
|
152
|
+
}
|
|
153
|
+
while (k > 0) {
|
|
154
|
+
let digit: utinyint = 48; // '0'
|
|
155
|
+
if (k <= DIGIT_PLACES.length) {
|
|
156
|
+
while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
|
|
157
|
+
value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
|
|
158
|
+
digit++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (k <= width) {
|
|
162
|
+
queueByte(toTile(digit));
|
|
163
|
+
col++;
|
|
164
|
+
if (col == text.COLUMNS) {
|
|
165
|
+
col = locate(cell + width - k + 1); // the next row is not the next address
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
k--;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The colours `text.setColor()` takes. The NES text grid has no per-cell colour, so the
|
|
174
|
+
// names are here for a program that compiles everywhere and their values
|
|
175
|
+
// go nowhere.
|
|
176
|
+
export namespace TextColor {
|
|
177
|
+
const BLACK: utinyint = 0;
|
|
178
|
+
const WHITE: utinyint = 1;
|
|
179
|
+
const RED: utinyint = 2;
|
|
180
|
+
const CYAN: utinyint = 3;
|
|
181
|
+
const PURPLE: utinyint = 4;
|
|
182
|
+
const GREEN: utinyint = 5;
|
|
183
|
+
const BLUE: utinyint = 6;
|
|
184
|
+
const YELLOW: utinyint = 7;
|
|
185
|
+
}
|