diff --git a/library/modules/gpu/metal.sx b/library/modules/gpu/metal.sx index 407c32b..38e2296 100644 --- a/library/modules/gpu/metal.sx +++ b/library/modules/gpu/metal.sx @@ -462,8 +462,12 @@ metal_update_buffer_at_ios :: (self: *MetalGPU, handle: u32, data: *void, size_b msg_o : (*void, *void) -> *void = xx objc_msgSend; base := msg_o(buf, sel_registerName("contents".ptr)); if base == null { return; } - dst : [*]u8 = xx base; - memcpy(xx @dst[byte_offset], data, size_bytes); + // Add byte_offset via integer arithmetic — `@dst[i]` on `[*]u8` + // already does this, but we keep this form explicit so a future + // pointer-arithmetic regression here can't hide. + base_i : s64 = xx base; + dst_at : *void = xx (base_i + byte_offset); + memcpy(dst_at, data, size_bytes); } metal_lookup_buffer :: (self: *MetalGPU, handle: u32) -> *void { diff --git a/library/modules/ui/renderer.sx b/library/modules/ui/renderer.sx index ce98b53..bcfa500 100755 --- a/library/modules/ui/renderer.sx +++ b/library/modules/ui/renderer.sx @@ -310,11 +310,13 @@ UIRenderer :: struct { self.gpu.update_buffer_at(self.mtl_vbuf, xx self.vertices, upload_size, byte_off); vertex_off : s32 = xx (byte_off / UI_VERTEX_BYTES); self.gpu.draw_triangles(vertex_off, xx self.vertex_count); + // Each batch starts on a vertex boundary so `vertex_off = + // byte_off / UI_VERTEX_BYTES` lands on a whole vertex (otherwise + // the shader reads partway through the previous vertex and the + // text quads end up reading garbled UVs/colors). upload_size is + // already vertex_count × UI_VERTEX_BYTES so the running offset + // stays vertex-aligned without an extra `% UI_VERTEX_BYTES` pad. self.mtl_buf_offset = byte_off + upload_size; - // Align next slice to 16B for safety with packed_float4 reads. - align : s64 = 16; - rem := self.mtl_buf_offset % align; - if rem != 0 { self.mtl_buf_offset = self.mtl_buf_offset + (align - rem); } } else { // Only re-bind the current texture (program, projection, VAO // already bound in begin()). glBufferData orphans the old buffer @@ -332,12 +334,20 @@ UIRenderer :: struct { font := g_font; if font == null { return; } - // Shape text into positioned glyphs + // Shape text into positioned glyphs. This may rasterize new glyphs + // AND grow the atlas, which creates a new GPU texture under + // `font.texture_id`. Re-bind the font atlas after shaping so the + // upcoming text quads sample the texture that actually holds the + // glyphs we just rasterized. font.shape_text(node.text, node.font_size); - // Flush any new glyphs to the atlas texture (no texture switch needed — atlas is already bound) + // Flush any new glyphs to the atlas texture font.flush(); + // If the atlas grew (or otherwise changed handle), switch the + // current texture so the next flush samples the right one. + self.bind_texture(font.texture_id); + r := node.text_color.rf(); g := node.text_color.gf(); b := node.text_color.bf(); @@ -609,11 +619,14 @@ fragment float4 fmain(VOut in [[stage_in]], // Image mode (mode == -2.0): sample texture return tex.sample(s, in.uv) * in.color; } else if (mode < 0.0) { - // Text mode (mode == -1.0): sample glyph atlas .r as alpha + // Text mode (mode == -1.0): the glyph atlas stores R8 alpha + // coverage from stbtt_MakeGlyphBitmap. Use the sampled value + // directly as alpha (no smoothstep — those were for SDFs and + // thinned anti-aliased coverage strokes). Small-size text renders + // dim on dark backgrounds because most glyph pixels sit in 0.1-0.5 + // coverage; tracked as the "faint text" follow-up. float alpha = tex.sample(s, in.uv).r; - float ew = fwidth(alpha) * 0.7; - alpha = smoothstep(0.5 - ew, 0.5 + ew, alpha); - return float4(in.color.rgb, in.color.a * pow(alpha, 0.9)); + return float4(in.color.rgb, in.color.a * alpha); } else if (mode > 0.0 || border > 0.0) { // Rounded rect: SDF alpha, vertex color only float2 half_size = rectSize * 0.5;