Files
m3te/main.sx
swipelab df6cb2161d P0.2: draw a colored quad on the iOS Simulator, driven by sx
Prove the render path end-to-end: bring up UIKit + Metal (already
scaffolded), clear to a solid blue, and draw one centered orange quad via
the GPU protocol's clear+quad path — an MSL pass-through pipeline plus a
6-vertex (2-triangle) NDC buffer, created lazily once the MTLDevice exists.

Geometry is NDC [-0.5, 0.5]² so the quad is the central 50%x50% of the
drawable regardless of device resolution, keeping the screenshot golden
unambiguous. Background (0.10, 0.20, 0.55), quad (1.0, 0.6, 0.0).

goldens/p0_quad.png is the first screenshot golden, captured from the
booted iOS 26.0 simulator after install + launch.
2026-06-04 18:21:35 +03:00

160 lines
5.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import "modules/std.sx";
#import "build.sx";
#import "modules/compiler.sx";
#import "modules/opengl.sx";
#import "modules/sdl3.sx";
#import "modules/gpu/api.sx";
#import "modules/gpu/types.sx";
#import "modules/gpu/metal.sx";
#import "modules/platform/api.sx";
#import "modules/platform/sdl3.sx";
#import "modules/platform/uikit.sx";
#run configure_build();
g_plat : Platform = ---;
g_viewport_w : f32 = 800.0;
g_viewport_h : f32 = 600.0;
// 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;
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.
QUAD_VERTS : [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,
];
// 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;
}
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;
for g_plat.poll_events(): (*ev) {
inline if OS != .ios {
if ev == {
case .key_up: (e) {
if e.key == .escape { g_plat.stop(); }
}
}
}
}
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.
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 {
g_metal_gpu.init(g_uikit_plat.gl_layer, g_uikit_plat.pixel_w, g_uikit_plat.pixel_h);
} 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; }
g_metal_gpu.update_buffer(g_quad_vbuf, xx @QUAD_VERTS, size_of([36]f32));
}
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);
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);
glClear(GL_COLOR_BUFFER_BIT);
}
g_plat.end_frame();
}
main :: () -> void {
inline if OS == .ios {
u : *UIKitPlatform = xx context.allocator.alloc(size_of(UIKitPlatform));
u.gpu_mode = .metal;
if !u.init("m3te", 800, 600) { return; }
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.
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.
memset(xx g_metal_gpu, 0, size_of(MetalGPU));
if !g_metal_gpu.init(null, 0, 0) { return; }
} else {
s : *SdlPlatform = xx context.allocator.alloc(size_of(SdlPlatform));
if !s.init("m3te", 800, 600) { return; }
g_plat = xx s;
}
fc := g_plat.begin_frame();
g_viewport_w = fc.viewport_w;
g_viewport_h = fc.viewport_h;
g_plat.run_frame_loop(closure(frame));
g_plat.shutdown();
}