Prove the input path end-to-end. Poll platform events in the frame loop and, on a mouse_down (UIKit touchesBegan, mapped to an Event in uikit.sx), toggle the centered quad between orange and green. The toggle marks the vertex buffer dirty; the next frame re-uploads the active color palette before drawing. Flip on the press only — a tap also delivers mouse_up (touchesEnded), so toggling on both would net to no visible change. goldens/p0_input_before.png (orange quad) and goldens/p0_input_after.png (green quad) bracket a real tap injected at the device-screen center (201,437 pt) via `idb ui tap` on the booted iOS 26.0 simulator.
192 lines
7.2 KiB
Plaintext
192 lines
7.2 KiB
Plaintext
#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;
|
||
|
||
// ── 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;
|
||
}
|
||
|
||
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) {
|
||
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) {
|
||
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; }
|
||
}
|
||
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);
|
||
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();
|
||
}
|