metal: pause step 3b pending sx-side fixes (filed 0024-0030)

Step 3b code is wired across UIRenderer + GlyphCache + UIPipeline +
chess game (gpu_mode = .metal on iOS, MetalGPU bound via the GPU
protocol). macOS GL chess, iOS-sim GLES chess, and iOS-sim Metal
triangle (63-metal-clear.sx) all still render.

iOS-sim Metal chess crashes inside replaceRegion uploading the 1MB
font atlas. Bisecting that crash exposed several sx-language issues
where mid-bisect tracers (NSLog inside if/else branch bodies) didn't
produce output, blocking further investigation.

Filing each finding as examples/issue-NNNN.sx rather than working
around piecemeal:

Bugs:
- 0024 NSLog/foreign-call inside if/else body not producing output
- 0025 C-ABI param coercion incomplete for composites >16B
       (combined direct-call abiCoerceParamType TODO + call_indirect
        path that doesn't apply C-ABI coercion at all)
- 0026 replaceRegion 1MB upload crash (likely downstream of 0025)

Features needed for step 4 + cleanup:
- 0027 Obj-C block bridge (^{...}) for animateWithDuration:
- 0028 Optional protocol box (?GPU = null) replaces T = ---; has_T: bool
- 0029 destroy_texture/buffer/shader on GPU protocol
- 0030 extern cross-file globals

Library-side: renderer.sx + glyph_cache.sx + pipeline.sx gain a
`gpu: GPU = ---; has_gpu: bool` field pair + branches that route every
GL touchpoint through the protocol when has_gpu. glyph_cache.init
saves/restores those fields around its memset. pipeline.set_gpu()
propagates to renderer + font. Renderer's MSL shader source added as
UI_MSL_SRC using packed_float2/packed_float4 to keep the 12-float
interleaved vertex layout tight (48 bytes).

metal.sx: dual-phase init (init(null, 0, 0) for eager device+queue,
re-init with the layer once UIKit installs the SxMetalView).
setStorageMode:.shared on every texture descriptor to ensure CPU-
writable atlas pixels on Apple Silicon iOS-sim.

Regression suite: 68 passing, 0 failed. WASM chess build currently
broken under step 3b state (silent compiler crash); documented in
CHECKPOINT.md, likely fallout from one of the filed issues (probably
0028 — the verbose protocol-box pattern). Step 3b resumes after
0024-0030 land.
This commit is contained in:
agra
2026-05-17 21:17:17 +03:00
parent a938c4f900
commit a1647eab9b
11 changed files with 783 additions and 97 deletions

View File

@@ -1,5 +1,7 @@
#import "modules/std.sx";
#import "modules/opengl.sx";
#import "modules/gpu/types.sx";
#import "modules/gpu/api.sx";
#import "modules/stb_truetype.sx";
#import "modules/ui/types.sx";
@@ -176,9 +178,20 @@ GlyphCache :: struct {
last_shape_len: s64;
last_shape_size_q: u16;
// GPU protocol backend. When `has_gpu`, atlas creation + dirty uploads
// route through `gpu` instead of raw GL.
gpu: GPU = ---;
has_gpu: bool = false;
init :: (self: *GlyphCache, path: [:0]u8, default_size: f32) {
// Preserve any pre-set GPU dispatch across the zero-out — the
// surrounding struct memset would otherwise wipe it.
saved_gpu := self.gpu;
saved_has_gpu := self.has_gpu;
// Zero out the entire struct first (parent may be uninitialized with = ---)
memset(self, 0, size_of(GlyphCache));
self.gpu = saved_gpu;
self.has_gpu = saved_has_gpu;
// Load font file
file_size : s32 = 0;
@@ -245,15 +258,25 @@ GlyphCache :: struct {
val_bytes : s64 = self.hash_cap * 8; // s64 per slot (s32 would suffice but alignment)
self.hash_vals = xx context.allocator.alloc(val_bytes);
// Create OpenGL texture
glGenTextures(1, @self.texture_id);
glBindTexture(GL_TEXTURE_2D, self.texture_id);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, self.atlas_width, self.atlas_height, 0, GL_RED, GL_UNSIGNED_BYTE, self.bitmap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
// Create the atlas texture. In GPU-protocol mode we create empty and
// let the first `flush()` push the (zero-initialized) bitmap via
// update_texture_region — same result as the GL path's glTexImage2D
// with the zeroed bitmap, but works whether or not the backend
// accepts CPU pixel pointers at create time.
if self.has_gpu {
self.texture_id = self.gpu.create_texture(
self.atlas_width, self.atlas_height, .r8, null);
self.dirty = true;
} else {
glGenTextures(1, @self.texture_id);
glBindTexture(GL_TEXTURE_2D, self.texture_id);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, self.atlas_width, self.atlas_height, 0, GL_RED, GL_UNSIGNED_BYTE, self.bitmap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
}
out("GlyphCache initialized: ");
out(path);
@@ -406,9 +429,14 @@ GlyphCache :: struct {
// Upload dirty atlas to GPU
flush :: (self: *GlyphCache) {
if self.dirty == false { return; }
glBindTexture(GL_TEXTURE_2D, self.texture_id);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, self.atlas_width, self.atlas_height, GL_RED, GL_UNSIGNED_BYTE, self.bitmap);
if self.has_gpu {
self.gpu.update_texture_region(self.texture_id, 0, 0,
self.atlas_width, self.atlas_height, xx self.bitmap);
} else {
glBindTexture(GL_TEXTURE_2D, self.texture_id);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, self.atlas_width, self.atlas_height, GL_RED, GL_UNSIGNED_BYTE, self.bitmap);
}
self.dirty = false;
}
@@ -464,16 +492,23 @@ GlyphCache :: struct {
self.atlas_width = new_w;
self.atlas_height = new_h;
// Recreate GL texture
glDeleteTextures(1, @self.texture_id);
glGenTextures(1, @self.texture_id);
glBindTexture(GL_TEXTURE_2D, self.texture_id);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, new_w, new_h, 0, GL_RED, GL_UNSIGNED_BYTE, new_bitmap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
// Recreate atlas at the new size.
if self.has_gpu {
// No destroy_texture in the GPU protocol yet — old atlas
// leaks in the backend table until process exit. Atlas grow
// is rare so this is acceptable for now.
self.texture_id = self.gpu.create_texture(new_w, new_h, .r8, xx new_bitmap);
} else {
glDeleteTextures(1, @self.texture_id);
glGenTextures(1, @self.texture_id);
glBindTexture(GL_TEXTURE_2D, self.texture_id);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, new_w, new_h, 0, GL_RED, GL_UNSIGNED_BYTE, new_bitmap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
}
// Recompute UV coordinates for all cached glyphs
atlas_wf : f32 = xx new_w;