ui: text shader uses raw atlas coverage (no SDF smoothstep)

Two small cleanups in the Metal text path on top of the buffer-offset
fix from cc71d95:

- Drop the SDF-style `smoothstep(0.5 ± ew, alpha)` from the text mode
  branch in UI_MSL_SRC. The glyph atlas stores alpha coverage straight
  from stbtt_MakeGlyphBitmap, not signed distance, so the smoothstep
  was thinning anti-aliased strokes by mapping mid-coverage values
  (0.3–0.7) toward 0/1. Use the sampled value directly as alpha.

- Drop the 16-byte alignment pad on `mtl_buf_offset` in `flush()`. Each
  batch's upload_size is already a multiple of UI_VERTEX_BYTES (48), so
  the running offset stays vertex-aligned without the extra rounding.

- After `font.shape_text` + `font.flush` in `render_text`, re-bind
  `font.texture_id`. If the atlas grew during shaping, the GPU texture
  handle changed; without this rebind the next flush samples the old
  (smaller) atlas which doesn't have the newly-rasterized glyphs.

- Use explicit s64-pointer arithmetic in `metal_update_buffer_at_ios`
  so a future regression in `[*]u8` indexing can't quietly miscompile
  the per-flush write offset.

Text at small sizes still renders dim on dark backgrounds — most glyph
pixels sit in 0.1–0.5 coverage and the linear blend doesn't push them
to bright values — tracked separately as the faint-text follow-up.
This commit is contained in:
agra
2026-05-18 10:26:31 +03:00
parent cc71d9591d
commit b43472e6ab
2 changed files with 29 additions and 12 deletions

View File

@@ -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 {

View File

@@ -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;