nx_chip8.nx source
↩ module page · 261 lines · 11031 B
1// nx_chip8.nx -- Implements a CHIP-8 interpreter with framebuffer, input, timers, and memory management in a single binary.
2const O_MAGIC_4096: i64 = 4096
3const O_MAGIC_2048: i64 = 2048
4const O_MAGIC_439041101: i64 = 439041101
5const O_MAGIC_88172645463325252: i64 = 88172645463325252
6// nx_chip8.nx -- R0.3 SOVEREIGN CHIP-8 interpreter. The on-ramp emulator: a public-domain VM spec (RCA
7// COSMAC VIP, 1977) implemented from scratch -> 100% Nishi, and PD CHIP-8 games are legally spotless.
8// Base-relative fixed-offset state (the proven nx_wasmfps idiom) so the SAME code runs NATIVE (base=mmap,
9// gateable) and WASM (base=0, browser). Exercises the whole sovereign substrate end to end: CPU interp +
10// framebuffer + input + timers. The same substrate carries x86/DOS (Phase 1). license_tier: ORIGINAL
11//
12// genealogy_id: sovereign_emulator_chip8 lineage_id: nx_chip8_v1 (parents: nx_emu_probe fetch-decode-execute)
13//
14// fixed linear-memory layout (byte offsets):
15const O_MEM: i64 = 0 // 4096 guest bytes (programs load at 0x200)
16const O_V: i64 = 4096 // 16 V registers (i64 each, hold an 8-bit value) -> 4224
17const O_REG: i64 = 4224 // 6 i64: I, PC, SP, DT, ST, RNG -> 4272
18const O_STACK: i64 = 4272 // 16-level call stack (i64) -> 4400
19const O_KEYS: i64 = 4400 // 16 key states 0/1 (i64) -> 4528
20const O_DISP: i64 = 4528 // 64*32 display pixels 0/1 (i64) -> 20912
21
22const RI_I: i64 = 0
23const RI_PC: i64 = 1
24const RI_SP: i64 = 2
25const RI_DT: i64 = 3
26const RI_ST: i64 = 4
27const RI_RNG: i64 = 5
28
29// write 5 font bytes at mem offset.
30func ch8_fw(mem: *u8, off: i64, a: i64, b: i64, c: i64, d: i64, e: i64) -> i64 {
31 mem[off] = a as u8; mem[off + 1] = b as u8; mem[off + 2] = c as u8; mem[off + 3] = d as u8; mem[off + 4] = e as u8
32 return 0
33}
34// the standard 16-char hex fontset at mem[0..80]; FX29 sets I = digit*5 to point here.
35func ch8_load_font(base: i64) -> i64 {
36 let mem: *u8 = (base + O_MEM) as *u8
37 ch8_fw(mem, 0, 240, 144, 144, 144, 240) // 0
38 ch8_fw(mem, 5, 32, 96, 32, 32, 112) // 1
39 ch8_fw(mem, 10, 240, 16, 240, 128, 240) // 2
40 ch8_fw(mem, 15, 240, 16, 240, 16, 240) // 3
41 ch8_fw(mem, 20, 144, 144, 240, 16, 16) // 4
42 ch8_fw(mem, 25, 240, 128, 240, 16, 240) // 5
43 ch8_fw(mem, 30, 240, 128, 240, 144, 240) // 6
44 ch8_fw(mem, 35, 240, 16, 32, 64, 64) // 7
45 ch8_fw(mem, 40, 240, 144, 240, 144, 240) // 8
46 ch8_fw(mem, 45, 240, 144, 240, 16, 240) // 9
47 ch8_fw(mem, 50, 240, 144, 240, 144, 144) // A
48 ch8_fw(mem, 55, 224, 144, 224, 144, 224) // B
49 ch8_fw(mem, 60, 240, 128, 128, 128, 240) // C
50 ch8_fw(mem, 65, 224, 144, 144, 144, 224) // D
51 ch8_fw(mem, 70, 240, 128, 240, 128, 240) // E
52 ch8_fw(mem, 75, 240, 128, 240, 128, 128) // F
53 return 0
54}
55
56func ch8_reset(base: i64) -> i64 {
57 let mem: *u8 = (base + O_MEM) as *u8
58 var i: i64 = 0
59 while i < O_MAGIC_4096 { mem[i] = 0 as u8; i = i + 1 }
60 let V: *i64 = (base + O_V) as *i64
61 i = 0; while i < 16 { V[i] = 0; i = i + 1 }
62 let stack: *i64 = (base + O_STACK) as *i64
63 i = 0; while i < 16 { stack[i] = 0; i = i + 1 }
64 let keys: *i64 = (base + O_KEYS) as *i64
65 i = 0; while i < 16 { keys[i] = 0; i = i + 1 }
66 let disp: *i64 = (base + O_DISP) as *i64
67 i = 0; while i < O_MAGIC_2048 { disp[i] = 0; i = i + 1 }
68 let reg: *i64 = (base + O_REG) as *i64
69 reg[RI_I] = 0
70 reg[RI_PC] = 512 // 0x200 -- programs start here
71 reg[RI_SP] = 0
72 reg[RI_DT] = 0
73 reg[RI_ST] = 0
74 reg[RI_RNG] = O_MAGIC_439041101 // 0x1A2B3C4D -- fixed seed -> deterministic (testable) CXNN
75 ch8_load_font(base) // 16-char hex fontset at mem[0..80] (FX29 points here)
76 return 0
77}
78
79// load one program byte into guest memory (the gate / browser uses this to place a ROM).
80func ch8_load(base: i64, addr: i64, byte: i64) -> i64 {
81 let mem: *u8 = (base + O_MEM) as *u8
82 mem[addr] = (byte & 0xff) as u8
83 return 0
84}
85
86// xorshift64 PRNG (its low bits are fine, unlike an LCG). advances + returns new state.
87func ch8_rand(s: i64) -> i64 {
88 var x: i64 = s
89 if x == 0 { x = O_MAGIC_88172645463325252 }
90 x = x ^ (x << 13)
91 x = x ^ (x >> 7)
92 x = x ^ (x << 17)
93 return x
94}
95
96// execute ONE instruction at PC. returns 0.
97func ch8_step(base: i64) -> i64 {
98 let mem: *u8 = (base + O_MEM) as *u8
99 let V: *i64 = (base + O_V) as *i64
100 let reg: *i64 = (base + O_REG) as *i64
101 let stack: *i64 = (base + O_STACK) as *i64
102 let keys: *i64 = (base + O_KEYS) as *i64
103 let disp: *i64 = (base + O_DISP) as *i64
104
105 var pc: i64 = reg[RI_PC]
106 let hi: i64 = mem[pc] as i64
107 let lo: i64 = mem[pc + 1] as i64
108 let op: i64 = (hi << 8) | lo
109 pc = pc + 2
110 let nnn: i64 = op & 0xfff
111 let nn: i64 = lo
112 let n: i64 = op & 0xf
113 let x: i64 = hi & 0xf
114 let y: i64 = (lo >> 4) & 0xf
115 let top: i64 = (op >> 12) & 0xf
116
117 if top == 0 {
118 if op == 224 { // 00E0 clear display
119 var i: i64 = 0
120 while i < O_MAGIC_2048 { disp[i] = 0; i = i + 1 }
121 }
122 if op == 238 { // 00EE return
123 reg[RI_SP] = reg[RI_SP] - 1
124 pc = stack[reg[RI_SP]]
125 }
126 }
127 if top == 1 { pc = nnn } // 1NNN jump
128 if top == 2 { // 2NNN call
129 stack[reg[RI_SP]] = pc
130 reg[RI_SP] = reg[RI_SP] + 1
131 pc = nnn
132 }
133 if top == 3 { if V[x] == nn { pc = pc + 2 } } // 3XNN skip eq
134 if top == 4 { if V[x] != nn { pc = pc + 2 } } // 4XNN skip ne
135 if top == 5 { if V[x] == V[y] { pc = pc + 2 } } // 5XY0 skip eq reg
136 if top == 6 { V[x] = nn } // 6XNN set
137 if top == 7 { V[x] = (V[x] + nn) & 0xff } // 7XNN add
138 if top == 8 {
139 if n == 0 { V[x] = V[y] }
140 if n == 1 { V[x] = (V[x] | V[y]) & 0xff }
141 if n == 2 { V[x] = (V[x] & V[y]) & 0xff }
142 if n == 3 { V[x] = (V[x] ^ V[y]) & 0xff }
143 if n == 4 { // add with carry
144 let s: i64 = V[x] + V[y]
145 var f: i64 = 0
146 if s > 255 { f = 1 }
147 V[x] = s & 0xff
148 V[15] = f
149 }
150 if n == 5 { // sub, VF = NOT borrow
151 var f: i64 = 0
152 if V[x] >= V[y] { f = 1 }
153 V[x] = (V[x] - V[y]) & 0xff
154 V[15] = f
155 }
156 if n == 6 { // shr, VF = lsb
157 let f: i64 = V[x] & 1
158 V[x] = (V[x] >> 1) & 0xff
159 V[15] = f
160 }
161 if n == 7 { // subn, VF = NOT borrow
162 var f: i64 = 0
163 if V[y] >= V[x] { f = 1 }
164 V[x] = (V[y] - V[x]) & 0xff
165 V[15] = f
166 }
167 if n == 14 { // 8XYE shl, VF = msb
168 let f: i64 = (V[x] >> 7) & 1
169 V[x] = (V[x] << 1) & 0xff
170 V[15] = f
171 }
172 }
173 if top == 9 { if V[x] != V[y] { pc = pc + 2 } } // 9XY0 skip ne reg
174 if top == 10 { reg[RI_I] = nnn } // ANNN set I
175 if top == 11 { pc = (nnn + V[0]) & 0xfff } // BNNN jump V0+NNN
176 if top == 12 { // CXNN rand & NN
177 reg[RI_RNG] = ch8_rand(reg[RI_RNG])
178 V[x] = ((reg[RI_RNG] >> 20) & nn) & 0xff
179 }
180 if top == 13 { // DXYN draw sprite
181 let vx: i64 = V[x]
182 let vy: i64 = V[y]
183 V[15] = 0
184 var row: i64 = 0
185 while row < n {
186 let sprite: i64 = mem[reg[RI_I] + row] as i64
187 var col: i64 = 0
188 while col < 8 {
189 let bit: i64 = (sprite >> (7 - col)) & 1
190 if bit == 1 {
191 let px: i64 = (vx + col) & 63
192 let py: i64 = (vy + row) & 31
193 let idx: i64 = py * 64 + px
194 if disp[idx] == 1 { V[15] = 1 }
195 disp[idx] = disp[idx] ^ 1
196 }
197 col = col + 1
198 }
199 row = row + 1
200 }
201 }
202 if top == 14 { // EX9E / EXA1 key skips
203 if nn == 158 { if keys[V[x]] == 1 { pc = pc + 2 } }
204 if nn == 161 { if keys[V[x]] == 0 { pc = pc + 2 } }
205 }
206 if top == 15 {
207 if nn == 7 { V[x] = reg[RI_DT] } // FX07 get delay
208 if nn == 10 { // FX0A wait for key (block by re-running)
209 var got: i64 = 0 - 1
210 var k: i64 = 0
211 while k < 16 { if keys[k] == 1 { got = k } k = k + 1 }
212 if got < 0 { pc = pc - 2 } else { V[x] = got }
213 }
214 if nn == 21 { reg[RI_DT] = V[x] } // FX15 set delay
215 if nn == 24 { reg[RI_ST] = V[x] } // FX18 set sound
216 if nn == 30 { reg[RI_I] = (reg[RI_I] + V[x]) & 0xffff } // FX1E add to I
217 if nn == 41 { reg[RI_I] = V[x] * 5 } // FX29 font sprite addr (font at mem[0], 5 B/char)
218 if nn == 51 { // FX33 BCD
219 let vv: i64 = V[x]
220 mem[reg[RI_I]] = (vv / 100) as u8
221 mem[reg[RI_I] + 1] = ((vv / 10) % 10) as u8
222 mem[reg[RI_I] + 2] = (vv % 10) as u8
223 }
224 if nn == 85 { // FX55 store V0..Vx
225 var i: i64 = 0
226 while i <= x { mem[reg[RI_I] + i] = (V[i] & 0xff) as u8; i = i + 1 }
227 }
228 if nn == 101 { // FX65 load V0..Vx
229 var i: i64 = 0
230 while i <= x { V[i] = mem[reg[RI_I] + i] as i64; i = i + 1 }
231 }
232 }
233
234 reg[RI_PC] = pc
235 return 0
236}
237
238// run `cycles` instructions, then tick the 60 Hz timers once (one frame).
239func ch8_run_frame(base: i64, cycles: i64) -> i64 {
240 var c: i64 = 0
241 while c < cycles { ch8_step(base); c = c + 1 }
242 let reg: *i64 = (base + O_REG) as *i64
243 if reg[RI_DT] > 0 { reg[RI_DT] = reg[RI_DT] - 1 }
244 if reg[RI_ST] > 0 { reg[RI_ST] = reg[RI_ST] - 1 }
245 return 0
246}
247
248// ---- inspection / IO (the gate + the browser shell use these) ----
249func ch8_key(base: i64, k: i64, down: i64) -> i64 {
250 let keys: *i64 = (base + O_KEYS) as *i64
251 keys[k & 15] = down
252 return 0
253}
254func ch8_reg(base: i64, xi: i64) -> i64 { let V: *i64 = (base + O_V) as *i64; return V[xi & 15] }
255func ch8_vf(base: i64) -> i64 { let V: *i64 = (base + O_V) as *i64; return V[15] }
256func ch8_geti(base: i64) -> i64 { let reg: *i64 = (base + O_REG) as *i64; return reg[RI_I] }
257func ch8_getpc(base: i64) -> i64 { let reg: *i64 = (base + O_REG) as *i64; return reg[RI_PC] }
258func ch8_pixel(base: i64, px: i64, py: i64) -> i64 { let disp: *i64 = (base + O_DISP) as *i64; return disp[py * 64 + px] }
259func ch8_disp_off() -> i64 { return O_DISP }
260func ch8_disp_w() -> i64 { return 64 }
261func ch8_disp_h() -> i64 { return 32 }