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.
This commit is contained in:
swipelab
2026-06-04 18:21:35 +03:00
parent 888814b9ec
commit df6cb2161d
2 changed files with 71 additions and 3 deletions

74
main.sx
View File

@@ -22,6 +22,60 @@ g_viewport_h : f32 = 600.0;
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;
@@ -49,12 +103,26 @@ 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);
}
clear : ClearColor = .{ r = 0.12, g = 0.12, b = 0.15, a = 1.0 };
if !g_metal_gpu.begin_frame(clear) { return; }
// 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.12, 0.12, 0.15, 1.0);
glClearColor(0.10, 0.20, 0.55, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
}
g_plat.end_frame();