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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user