P4.3: render seeded board with real gem sprites (sx, iOS sim)
Adopt the modules/ui UIPipeline framework (as the chess reference app does) and replace the P0 placeholder quad with a BoardView (View protocol, modeled on chess/board_view.sx): - background.png fills the screen; an 8x8 cell.png grid is centered in the safe area; each cell's gem is sampled from gems.png by UV column = gem index (0=red .. 5=purple). - Drive it from board.sx seeded with 1337 (the board_init golden's seed), so the on-screen layout matches that snapshot gem-for-gem. main.sx now hosts the view via UIPipeline (Metal on iOS, GL on desktop) and heap-allocates the board/asset state behind pointers (UFCS method calls on a value-typed global mutate a copy, so mutable state must live behind a pointer as the reference app does). Vendor the C deps the UI module's image/font path needs (stb_image, stb_truetype, kb_text_shape, file_utils); their #include "vendors/..." paths resolve relative to the project root. Evidence: ios-sim build links clean; tools/run_tests.sh 11/11 pass; running app captured at goldens/p4_board.png.
This commit is contained in:
171
main.sx
171
main.sx
@@ -3,113 +3,60 @@
|
||||
#import "modules/compiler.sx";
|
||||
#import "modules/opengl.sx";
|
||||
#import "modules/sdl3.sx";
|
||||
#import "modules/math";
|
||||
#import "modules/stb.sx";
|
||||
#import "modules/stb_truetype.sx";
|
||||
#import "modules/gpu/api.sx";
|
||||
#import "modules/gpu/types.sx";
|
||||
#import "modules/gpu/metal.sx";
|
||||
#import "modules/ui";
|
||||
#import "modules/platform/api.sx";
|
||||
#import "modules/platform/sdl3.sx";
|
||||
#import "modules/platform/uikit.sx";
|
||||
#import "board.sx";
|
||||
#import "board_view.sx";
|
||||
|
||||
#run configure_build();
|
||||
|
||||
// Fixed seed for the rendered board — the same seed tests/board_init.sx locks
|
||||
// as a snapshot, so the on-screen layout matches that golden gem-for-gem.
|
||||
BOARD_SEED :: 1337;
|
||||
|
||||
g_plat : Platform = ---;
|
||||
g_pipeline : *UIPipeline = ---;
|
||||
g_viewport_w : f32 = 800.0;
|
||||
g_viewport_h : f32 = 600.0;
|
||||
g_safe_insets : EdgeInsets = .{};
|
||||
|
||||
// iOS-only concrete handles kept alongside the boxed `g_plat` so the frame
|
||||
// loop can reach the CAMetalLayer pointer / pixel dims without going through
|
||||
// the protocol box.
|
||||
// iOS-only concrete handles kept alongside the boxed `g_plat` so the frame loop
|
||||
// can reach the CAMetalLayer pointer / pixel dims without going through the
|
||||
// protocol box.
|
||||
g_uikit_plat : *UIKitPlatform = null;
|
||||
g_metal_gpu : *MetalGPU = null;
|
||||
|
||||
// ── Render-path proof (P0.2) ─────────────────────────────────────────────
|
||||
// Clear to a solid blue, then draw one centered orange quad covering the
|
||||
// central 50%×50% of the drawable. Geometry is in NDC ([-0.5, 0.5]²) so the
|
||||
// quad stays screen-size independent across simulator devices, which keeps
|
||||
// the screenshot golden unambiguous. This is the GPU protocol's
|
||||
// clear+quad path: an MSL pipeline state plus a 6-vertex (2-triangle)
|
||||
// buffer, created lazily once the MTLDevice exists.
|
||||
g_quad_shader : ShaderHandle = 0;
|
||||
g_quad_vbuf : BufferHandle = 0;
|
||||
// The pure-sx model (board.sx) and its sprites, seeded once in main() and
|
||||
// rendered every frame. Heap-allocated so the view holds stable pointers to
|
||||
// the mutable state across frames.
|
||||
g_board : *Board = null;
|
||||
g_assets : *BoardAssets = null;
|
||||
|
||||
// ── Input-path proof (P0.3) ──────────────────────────────────────────────
|
||||
// A tap toggles the quad between two distinct colors, proving a real touch
|
||||
// reaches sx and changes the rendered frame. UIKit touchesBegan is mapped to
|
||||
// a `mouse_down` Event in uikit.sx; the frame loop's poll flips
|
||||
// `g_quad_flipped` and marks `g_quad_dirty`, which re-uploads the matching
|
||||
// vertex colors before the next draw.
|
||||
g_quad_flipped : bool = false;
|
||||
g_quad_dirty : bool = true;
|
||||
|
||||
BG_CLEAR :: ClearColor.{ r = 0.10, g = 0.20, b = 0.55, a = 1.0 };
|
||||
|
||||
// Vertex layout matches QUAD_MSL's `Vertex`: packed_float2 pos +
|
||||
// packed_float4 color = 24 bytes. `packed_*` avoids the 16-byte alignment
|
||||
// padding a plain `float4` would force. 6 vertices = 2 triangles. Two
|
||||
// arrays share the same geometry and differ only in color so the buffer
|
||||
// re-upload is a flat memcpy of the active palette.
|
||||
QUAD_VERTS_ORANGE : [36]f32 = .[
|
||||
-0.5, 0.5, 1.0, 0.6, 0.0, 1.0,
|
||||
0.5, 0.5, 1.0, 0.6, 0.0, 1.0,
|
||||
-0.5, -0.5, 1.0, 0.6, 0.0, 1.0,
|
||||
0.5, 0.5, 1.0, 0.6, 0.0, 1.0,
|
||||
0.5, -0.5, 1.0, 0.6, 0.0, 1.0,
|
||||
-0.5, -0.5, 1.0, 0.6, 0.0, 1.0,
|
||||
];
|
||||
|
||||
QUAD_VERTS_GREEN : [36]f32 = .[
|
||||
-0.5, 0.5, 0.15, 0.85, 0.35, 1.0,
|
||||
0.5, 0.5, 0.15, 0.85, 0.35, 1.0,
|
||||
-0.5, -0.5, 0.15, 0.85, 0.35, 1.0,
|
||||
0.5, 0.5, 0.15, 0.85, 0.35, 1.0,
|
||||
0.5, -0.5, 0.15, 0.85, 0.35, 1.0,
|
||||
-0.5, -0.5, 0.15, 0.85, 0.35, 1.0,
|
||||
];
|
||||
|
||||
// Pass-through shader: the vertex stage emits NDC positions directly (no
|
||||
// projection), the fragment stage returns the interpolated vertex color.
|
||||
// Entry-point names vmain / fmain are what MetalGPU.create_shader looks up.
|
||||
QUAD_MSL :: #string MSL
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct Vertex {
|
||||
packed_float2 pos;
|
||||
packed_float4 color;
|
||||
};
|
||||
|
||||
struct RasterizerData {
|
||||
float4 position [[position]];
|
||||
float4 color;
|
||||
};
|
||||
|
||||
vertex RasterizerData vmain(uint vid [[vertex_id]],
|
||||
constant Vertex* vertices [[buffer(0)]]) {
|
||||
RasterizerData out;
|
||||
out.position = float4(vertices[vid].pos, 0.0, 1.0);
|
||||
out.color = float4(vertices[vid].color);
|
||||
return out;
|
||||
// Rebuilt each frame inside the pipeline's arena; carries the current safe-area
|
||||
// insets so the grid stays inside the notch / home-indicator region.
|
||||
build_ui :: () -> View {
|
||||
BoardView.{ board = g_board, assets = g_assets, safe = g_safe_insets }
|
||||
}
|
||||
|
||||
fragment float4 fmain(RasterizerData in [[stage_in]]) {
|
||||
return in.color;
|
||||
}
|
||||
MSL;
|
||||
|
||||
frame :: () {
|
||||
fc := g_plat.begin_frame();
|
||||
g_viewport_w = fc.viewport_w;
|
||||
g_viewport_h = fc.viewport_h;
|
||||
g_safe_insets = g_plat.safe_insets();
|
||||
|
||||
if fc.viewport_w != g_pipeline.screen_width or fc.viewport_h != g_pipeline.screen_height {
|
||||
g_pipeline.resize(fc.viewport_w, fc.viewport_h);
|
||||
}
|
||||
|
||||
for g_plat.poll_events(): (*ev) {
|
||||
if ev == {
|
||||
// Flip on the press only. A tap also produces mouse_up
|
||||
// (touchesEnded); toggling on both would net to no change.
|
||||
case .mouse_down: {
|
||||
g_quad_flipped = !g_quad_flipped;
|
||||
g_quad_dirty = true;
|
||||
}
|
||||
}
|
||||
inline if OS != .ios {
|
||||
if ev == {
|
||||
case .key_up: (e) {
|
||||
@@ -117,13 +64,14 @@ frame :: () {
|
||||
}
|
||||
}
|
||||
}
|
||||
g_pipeline.dispatch_event(ev);
|
||||
}
|
||||
|
||||
inline if OS == .ios {
|
||||
// Lazy-attach Metal once -[SxAppDelegate didFinishLaunching:] has
|
||||
// installed the SxMetalView and its bounds have been measured; both
|
||||
// can lag the first CADisplayLink tick, and a zero-sized drawable
|
||||
// aborts via XPC.
|
||||
// installed the SxMetalView and its bounds have been measured; both can
|
||||
// lag the first CADisplayLink tick, and a zero-sized drawable aborts
|
||||
// via XPC.
|
||||
if g_uikit_plat.gl_layer == null { return; }
|
||||
if g_uikit_plat.pixel_w <= 0 or g_uikit_plat.pixel_h <= 0 { return; }
|
||||
if g_metal_gpu.layer == null {
|
||||
@@ -131,31 +79,15 @@ frame :: () {
|
||||
} else if g_metal_gpu.pixel_w != g_uikit_plat.pixel_w or g_metal_gpu.pixel_h != g_uikit_plat.pixel_h {
|
||||
g_metal_gpu.resize(g_uikit_plat.pixel_w, g_uikit_plat.pixel_h);
|
||||
}
|
||||
// Compile the quad pipeline + upload its vertices once. The MTLDevice
|
||||
// was created eagerly in main(), so both only need a valid device.
|
||||
if g_quad_shader == 0 {
|
||||
g_quad_shader = g_metal_gpu.create_shader(QUAD_MSL, "");
|
||||
if g_quad_shader == 0 { return; }
|
||||
}
|
||||
if g_quad_vbuf == 0 {
|
||||
g_quad_vbuf = g_metal_gpu.create_buffer(size_of([36]f32));
|
||||
if g_quad_vbuf == 0 { return; }
|
||||
}
|
||||
if g_quad_dirty {
|
||||
verts := if g_quad_flipped then @QUAD_VERTS_GREEN else @QUAD_VERTS_ORANGE;
|
||||
g_metal_gpu.update_buffer(g_quad_vbuf, xx verts, size_of([36]f32));
|
||||
g_quad_dirty = false;
|
||||
}
|
||||
|
||||
if !g_metal_gpu.begin_frame(BG_CLEAR) { return; }
|
||||
g_metal_gpu.set_shader(g_quad_shader);
|
||||
g_metal_gpu.set_vertex_buffer(g_quad_vbuf);
|
||||
g_metal_gpu.draw_triangles(0, 6);
|
||||
clear : ClearColor = .{ r = 0.05, g = 0.06, b = 0.10, a = 1.0 };
|
||||
if !g_metal_gpu.begin_frame(clear) { return; }
|
||||
g_pipeline.tick();
|
||||
g_metal_gpu.end_frame(fc.target_present_time);
|
||||
} else {
|
||||
glViewport(0, 0, fc.pixel_w, fc.pixel_h);
|
||||
glClearColor(0.10, 0.20, 0.55, 1.0);
|
||||
glClearColor(0.05, 0.06, 0.10, 1.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
g_pipeline.tick();
|
||||
}
|
||||
g_plat.end_frame();
|
||||
}
|
||||
@@ -168,9 +100,10 @@ main :: () -> void {
|
||||
g_plat = xx u;
|
||||
g_uikit_plat = u;
|
||||
|
||||
// The CAMetalLayer doesn't exist until didFinishLaunching: runs after
|
||||
// we return into UIApplicationMain, so attach lazily on the first
|
||||
// frame. init(null, 0, 0) only needs the MTLDevice.
|
||||
// The CAMetalLayer doesn't exist until didFinishLaunching: runs after we
|
||||
// return into UIApplicationMain, so attach lazily on the first frame.
|
||||
// init(null, 0, 0) only needs the MTLDevice, which is enough for the
|
||||
// texture uploads below.
|
||||
g_metal_gpu = xx context.allocator.alloc(size_of(MetalGPU));
|
||||
// alloc returns uninitialized memory; struct field defaults are NOT
|
||||
// applied, so List caps/lens would be garbage without this memset.
|
||||
@@ -185,6 +118,26 @@ main :: () -> void {
|
||||
fc := g_plat.begin_frame();
|
||||
g_viewport_w = fc.viewport_w;
|
||||
g_viewport_h = fc.viewport_h;
|
||||
g_safe_insets = g_plat.safe_insets();
|
||||
|
||||
g_pipeline = xx context.allocator.alloc(size_of(UIPipeline));
|
||||
// Same alloc caveat as above: zero so the optional `gpu` reads as null on
|
||||
// the desktop path (where set_gpu is not called) and the Lists start empty.
|
||||
memset(xx g_pipeline, 0, size_of(UIPipeline));
|
||||
inline if OS == .ios {
|
||||
g_pipeline.set_gpu(xx g_metal_gpu);
|
||||
}
|
||||
g_pipeline.init(fc.viewport_w, fc.viewport_h);
|
||||
g_pipeline.init_font("assets/fonts/default.ttf", 32.0, fc.dpi_scale);
|
||||
|
||||
g_board = xx context.allocator.alloc(size_of(Board));
|
||||
g_board.init(BOARD_SEED);
|
||||
|
||||
g_assets = xx context.allocator.alloc(size_of(BoardAssets));
|
||||
g_assets.init();
|
||||
g_assets.load(g_pipeline.gpu);
|
||||
|
||||
g_pipeline.set_body(closure(build_ui));
|
||||
|
||||
g_plat.run_frame_loop(closure(frame));
|
||||
g_plat.shutdown();
|
||||
|
||||
Reference in New Issue
Block a user