Files
sx/library/modules/ui/renderer.sx
agra df6e830bec fix(diagnostics): reject reserved type-name bindings in every module (issue 0077)
The issue-0076 reserved-type-name binding diagnostic only ran over main-file
decls, so an imported module (or the stdlib) could still declare `s2 := ...`
and reach lowering, where the address-of family loads the whole aggregate and
passes it by value to a `ptr` param — LLVM verifier abort.

Extend coverage to every compiled module: a dedicated `checkBindingNames` walk
(in semantic_diagnostics.zig) visits every var/`:=`/typed-local binding name and
function/lambda/struct-method parameter at any depth, with NO main-file filter,
descending the `namespace_decl` that a `mod :: #import` wraps so imported-module
decls are reached. It tracks each module's source_file (save/restore per node)
so the diagnostic renders against the imported module's text. Rejection still
defers to the parser's `Type.fromName` classifier; the unknown-type check (0064)
stays main-file-only. No lowering special-case; `.identifier`-only address-of
paths are unchanged.

Stdlib audit: the only reserved-name bindings under library/ were two `u1`
locals in ui/renderer.sx (UV coords) — renamed to u_min/u_max/v_min/v_max.

Regression test: examples/1120-diagnostics-imported-reserved-type-name.sx (+
companion mod.sx) — an imported `s2 := ...` now emits the clean diagnostic at
the import's declaration site (exit 1), not an LLVM abort.

Resolves issues 0076 (coverage extension) and 0077.
2026-06-03 19:32:49 +03:00

652 lines
24 KiB
Plaintext
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import "modules/std.sx";
#import "modules/compiler.sx";
#import "modules/opengl.sx";
#import "modules/math";
#import "modules/gpu/types.sx";
#import "modules/gpu/api.sx";
#import "modules/ui/types.sx";
#import "modules/ui/render.sx";
#import "modules/ui/glyph_cache.sx";
#import "modules/ui/font.sx";
// Vertex: pos(2) + uv(2) + color(4) + params(4) = 12 floats
UI_VERTEX_FLOATS :s64: 12;
UI_VERTEX_BYTES :s64: 48;
MAX_UI_VERTICES :s64: 16384;
UIRenderer :: struct {
// GL-side handles. Used when `gpu == null` (every non-iOS target today).
vao: u32;
vbo: u32;
shader: u32;
proj_loc: s32;
tex_loc: s32;
// CPU-side vertex scratch buffer — same for both backends.
vertices: [*]f32;
vertex_count: s64;
screen_width: f32;
screen_height: f32;
dpi_scale: f32;
white_texture: u32; // GL name OR TextureHandle (both are u32-shaped)
current_texture: u32;
draw_calls: s64;
// GPU protocol backend. When set, the renderer routes shader / buffer /
// texture / draw calls through `gpu` instead of raw GL. The chess game
// sets this on iOS to a boxed `*MetalGPU`.
gpu: ?GPU = null;
mtl_shader: ShaderHandle = 0;
mtl_vbuf: BufferHandle = 0;
// Per-frame byte offset into the Metal vertex buffer. Each flush writes
// to a fresh slice so concurrent in-flight draws don't trample each
// other's data — Metal's shared-storage buffer is read at GPU execution
// time, not at draw-call submission, so re-using offset 0 across flushes
// would let the last writer win and earlier batches would render as
// whatever was uploaded last. Reset to 0 in `begin()`.
mtl_buf_offset: s64 = 0;
mtl_buf_capacity: s64 = 0;
init :: (self: *UIRenderer) {
// Allocate vertex scratch (CPU side) — same for both backends.
buf_size := MAX_UI_VERTICES * UI_VERTEX_BYTES;
self.vertices = xx context.allocator.alloc(buf_size);
memset(self.vertices, 0, buf_size);
self.vertex_count = 0;
self.dpi_scale = 1.0;
if self.gpu != null {
// ── Metal backend (via GPU protocol) ───────────────────────
// Oversize the GPU buffer enough to hold many sub-batches per
// frame without wrapping. With per-flush offset advance, each
// draw reads from its own slice and can outlive earlier in-
// flight draws without corruption.
metal_buf_size := buf_size * 4;
// Backend-specific shader sources: Metal takes a combined MSL
// library with `vmain` + `fmain` entry points; GLES3 takes a
// separate vertex / fragment pair. Caller selects via OS gate.
inline if OS == .android {
self.mtl_shader = self.gpu.create_shader(UI_VERT_SRC_ES, UI_FRAG_SRC_ES);
}
inline if OS == .ios {
self.mtl_shader = self.gpu.create_shader(UI_MSL_SRC, "");
}
self.mtl_vbuf = self.gpu.create_buffer(metal_buf_size);
self.mtl_buf_capacity = metal_buf_size;
white_px : [4]u8 = .[255, 255, 255, 255];
self.white_texture = self.gpu.create_texture(1, 1, .rgba8, xx @white_px[0]);
} else {
// ── GL backend ─────────────────────────────────────────────
// Create shader (ES for WASM/WebGL2 + iOS GLES3, Core for desktop GL 3.3)
inline if OS == .wasm or OS == .ios {
self.shader = create_program(UI_VERT_SRC_ES, UI_FRAG_SRC_ES);
} else {
self.shader = create_program(UI_VERT_SRC_CORE, UI_FRAG_SRC_CORE);
}
self.proj_loc = glGetUniformLocation(self.shader, "uProj");
self.tex_loc = glGetUniformLocation(self.shader, "uTex");
// Create VAO/VBO
glGenVertexArrays(1, @self.vao);
glGenBuffers(1, @self.vbo);
glBindVertexArray(self.vao);
glBindBuffer(GL_ARRAY_BUFFER, self.vbo);
glBufferData(GL_ARRAY_BUFFER, xx buf_size, null, GL_DYNAMIC_DRAW);
// pos (2 floats)
glVertexAttribPointer(0, 2, GL_FLOAT, 0, xx UI_VERTEX_BYTES, xx 0);
glEnableVertexAttribArray(0);
// uv (2 floats)
glVertexAttribPointer(1, 2, GL_FLOAT, 0, xx UI_VERTEX_BYTES, xx 8);
glEnableVertexAttribArray(1);
// color (4 floats)
glVertexAttribPointer(2, 4, GL_FLOAT, 0, xx UI_VERTEX_BYTES, xx 16);
glEnableVertexAttribArray(2);
// params: corner_radius, border_width, rect_w, rect_h
glVertexAttribPointer(3, 4, GL_FLOAT, 0, xx UI_VERTEX_BYTES, xx 32);
glEnableVertexAttribArray(3);
glBindVertexArray(0);
// 1x1 white texture for solid rects
self.white_texture = create_white_texture();
}
}
begin :: (self: *UIRenderer, width: f32, height: f32, font_texture: u32) {
self.screen_width = width;
self.screen_height = height;
self.vertex_count = 0;
self.current_texture = font_texture;
self.draw_calls = 0;
proj := Mat4.ortho(0.0, width, height, 0.0, -1.0, 1.0);
if self.gpu != null {
// Reset the per-frame ring offset; this frame's flushes start at 0.
self.mtl_buf_offset = 0;
// Pipeline state + vertex buffer + projection + initial texture.
// Metal blend mode + scissor-cleared defaults are baked into
// the pipeline state, so no per-frame glEnable/glDisable.
self.gpu.set_shader(self.mtl_shader);
self.gpu.set_vertex_buffer(self.mtl_vbuf);
self.gpu.set_vertex_constants(1, xx proj.data, 64);
self.gpu.set_texture(0, font_texture);
} else {
// GL: bind everything for the frame.
glUseProgram(self.shader);
glUniformMatrix4fv(self.proj_loc, 1, 0, proj.data);
glUniform1i(self.tex_loc, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, font_texture);
glBindVertexArray(self.vao);
glBindBuffer(GL_ARRAY_BUFFER, self.vbo);
}
}
bind_texture :: (self: *UIRenderer, tex: u32) {
if tex != self.current_texture {
self.flush();
self.current_texture = tex;
}
}
// Emit a quad (2 triangles = 6 vertices)
push_quad :: (self: *UIRenderer, frame: Frame, color: Color, radius: f32, border_w: f32) {
if self.vertex_count + 6 > MAX_UI_VERTICES {
self.flush();
}
x0 := frame.origin.x;
y0 := frame.origin.y;
x1 := x0 + frame.size.width;
y1 := y0 + frame.size.height;
r := color.rf();
g := color.gf();
b := color.bf();
a := color.af();
w := frame.size.width;
h := frame.size.height;
// 6 vertices for quad: TL, TR, BL, TR, BR, BL
self.write_vertex(x0, y0, 0.0, 0.0, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x1, y0, 1.0, 0.0, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x0, y1, 0.0, 1.0, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x1, y0, 1.0, 0.0, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x1, y1, 1.0, 1.0, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x0, y1, 0.0, 1.0, r, g, b, a, radius, border_w, w, h);
}
// Emit a quad with custom UV coordinates (for sprite sheet sub-textures)
push_quad_uv :: (self: *UIRenderer, frame: Frame, color: Color, radius: f32, border_w: f32, uv_min: Point, uv_max: Point) {
if self.vertex_count + 6 > MAX_UI_VERTICES {
self.flush();
}
x0 := frame.origin.x;
y0 := frame.origin.y;
x1 := x0 + frame.size.width;
y1 := y0 + frame.size.height;
r := color.rf();
g := color.gf();
b := color.bf();
a := color.af();
w := frame.size.width;
h := frame.size.height;
u_min := uv_min.x;
v_min := uv_min.y;
u_max := uv_max.x;
v_max := uv_max.y;
self.write_vertex(x0, y0, u_min, v_min, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x1, y0, u_max, v_min, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x0, y1, u_min, v_max, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x1, y0, u_max, v_min, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x1, y1, u_max, v_max, r, g, b, a, radius, border_w, w, h);
self.write_vertex(x0, y1, u_min, v_max, r, g, b, a, radius, border_w, w, h);
}
write_vertex :: (self: *UIRenderer, x: f32, y: f32, u: f32, v: f32, r: f32, g: f32, b: f32, a: f32, cr: f32, bw: f32, rw: f32, rh: f32) {
off := self.vertex_count * UI_VERTEX_FLOATS;
self.vertices[off + 0] = x;
self.vertices[off + 1] = y;
self.vertices[off + 2] = u;
self.vertices[off + 3] = v;
self.vertices[off + 4] = r;
self.vertices[off + 5] = g;
self.vertices[off + 6] = b;
self.vertices[off + 7] = a;
self.vertices[off + 8] = cr;
self.vertices[off + 9] = bw;
self.vertices[off + 10] = rw;
self.vertices[off + 11] = rh;
self.vertex_count += 1;
}
// Walk the render tree and emit quads
process :: (self: *UIRenderer, tree: *RenderTree) {
i := 0;
while i < tree.nodes.len {
node := tree.nodes.items[i];
if node.type == {
case .rect: {
self.push_quad(node.frame, node.fill_color, 0.0, 0.0);
}
case .rounded_rect: {
self.push_quad(node.frame, node.fill_color, node.corner_radius, node.stroke_width);
}
case .text: {
if g_font != null {
self.render_text(node);
}
}
case .image: {
self.bind_texture(node.texture_id);
neg2 : f32 = 0.0 - 2.0;
self.push_quad_uv(node.frame, COLOR_WHITE, neg2, 0.0, node.uv_min, node.uv_max);
// Re-bind font atlas after image
font := g_font;
if font != null {
self.bind_texture(font.texture_id);
}
}
case .clip_push: {
self.flush();
dpi := self.dpi_scale;
if self.gpu != null {
// Metal: pixel coords, top-left origin (no Y flip).
self.gpu.set_scissor(
xx (node.frame.origin.x * dpi),
xx (node.frame.origin.y * dpi),
xx (node.frame.size.width * dpi),
xx (node.frame.size.height * dpi),
);
} else {
// GL: pixel coords, bottom-left origin — flip Y.
glEnable(GL_SCISSOR_TEST);
glScissor(
xx (node.frame.origin.x * dpi),
xx ((self.screen_height - node.frame.origin.y - node.frame.size.height) * dpi),
xx (node.frame.size.width * dpi),
xx (node.frame.size.height * dpi)
);
}
}
case .clip_pop: {
self.flush();
if self.gpu != null {
self.gpu.disable_scissor();
} else {
glDisable(GL_SCISSOR_TEST);
}
}
case .opacity_push: {}
case .opacity_pop: {}
}
i += 1;
}
}
flush :: (self: *UIRenderer) {
if self.vertex_count == 0 { return; }
upload_size : s64 = self.vertex_count * UI_VERTEX_BYTES;
if self.gpu != null {
// Mirror the GL path: bind current texture before drawing.
// current_texture may have changed since the last flush.
self.gpu.set_texture(0, self.current_texture);
// Write this batch to a fresh slice of the GPU buffer and draw
// it from there. Re-using offset 0 would race against earlier
// still-in-flight draws (see `mtl_buf_offset` comment in the
// struct).
if self.mtl_buf_offset + upload_size > self.mtl_buf_capacity {
// Frame overflowed the GPU buffer; wrap to 0. Previous in-
// flight batches from this frame will likely render wrong,
// but the alternative (skipping the draw) would render
// even less. Practical UIs should never hit this.
self.mtl_buf_offset = 0;
}
byte_off := self.mtl_buf_offset;
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;
} else {
// Only re-bind the current texture (program, projection, VAO
// already bound in begin()). glBufferData orphans the old buffer
// to avoid GPU sync stalls.
glBindTexture(GL_TEXTURE_2D, self.current_texture);
glBufferData(GL_ARRAY_BUFFER, xx upload_size, self.vertices, GL_DYNAMIC_DRAW);
glDrawArrays(GL_TRIANGLES, 0, xx self.vertex_count);
}
self.vertex_count = 0;
self.draw_calls += 1;
}
render_text :: (self: *UIRenderer, node: RenderNode) {
font := g_font;
if font == null { return; }
// 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
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();
a := node.text_color.af();
ascent := font.get_ascent(node.font_size);
raster_size := node.font_size * font.dpi_scale;
inv_dpi := font.inv_dpi;
i : s64 = 0;
while i < font.shaped_buf.len {
shaped := font.shaped_buf.items[i];
cached := font.get_or_rasterize(shaped.glyph_index, raster_size);
if cached != null {
if cached.width > 0.0 {
// Scale physical pixel dimensions back to logical units
gx0 := node.frame.origin.x + shaped.x + cached.offset_x * inv_dpi;
gy0 := node.frame.origin.y + ascent + shaped.y + cached.offset_y * inv_dpi;
gx1 := gx0 + cached.width * inv_dpi;
gy1 := gy0 + cached.height * inv_dpi;
u_min := cached.uv_x;
v_min := cached.uv_y;
u_max := cached.uv_x + cached.uv_w;
v_max := cached.uv_y + cached.uv_h;
if self.vertex_count + 6 > MAX_UI_VERTICES {
self.flush();
}
// corner_radius = -1.0 signals "text mode" to the fragment shader
neg1 : f32 = 0.0 - 1.0;
self.write_vertex(gx0, gy0, u_min, v_min, r, g, b, a, neg1, 0.0, 0.0, 0.0);
self.write_vertex(gx1, gy0, u_max, v_min, r, g, b, a, neg1, 0.0, 0.0, 0.0);
self.write_vertex(gx0, gy1, u_min, v_max, r, g, b, a, neg1, 0.0, 0.0, 0.0);
self.write_vertex(gx1, gy0, u_max, v_min, r, g, b, a, neg1, 0.0, 0.0, 0.0);
self.write_vertex(gx1, gy1, u_max, v_max, r, g, b, a, neg1, 0.0, 0.0, 0.0);
self.write_vertex(gx0, gy1, u_min, v_max, r, g, b, a, neg1, 0.0, 0.0, 0.0);
}
}
i += 1;
}
// Flush any glyphs rasterized during this text draw
font.flush();
}
}
create_white_texture :: () -> u32 {
tex : u32 = 0;
glGenTextures(1, @tex);
glBindTexture(GL_TEXTURE_2D, tex);
pixel : [4]u8 = .[255, 255, 255, 255];
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, @pixel);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_NEAREST);
tex
}
// --- UI Shaders ---
// --- Desktop (Core Profile 3.3) shaders ---
UI_VERT_SRC_CORE :: #string GLSL
#version 330 core
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 2) in vec4 aColor;
layout(location = 3) in vec4 aParams;
uniform mat4 uProj;
out vec2 vUV;
out vec4 vColor;
out vec4 vParams;
void main() {
gl_Position = uProj * vec4(aPos, 0.0, 1.0);
vUV = aUV;
vColor = aColor;
vParams = aParams;
}
GLSL;
UI_FRAG_SRC_CORE :: #string GLSL
#version 330 core
in vec2 vUV;
in vec4 vColor;
in vec4 vParams;
uniform sampler2D uTex;
out vec4 FragColor;
float roundedBoxSDF(vec2 center, vec2 half_size, float radius) {
vec2 q = abs(center) - half_size + vec2(radius);
return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - radius;
}
void main() {
float mode = vParams.x;
float border = vParams.y;
vec2 rectSize = vParams.zw;
if (mode < -1.5) {
// Image mode (mode == -2.0): sample texture
FragColor = texture(uTex, vUV) * vColor;
} else if (mode < 0.0) {
// Text mode (mode == -1.0): sample glyph atlas .r as alpha
float alpha = texture(uTex, vUV).r;
float ew = fwidth(alpha) * 0.7;
alpha = smoothstep(0.5 - ew, 0.5 + ew, alpha);
FragColor = vec4(vColor.rgb, vColor.a * pow(alpha, 0.9));
} else if (mode > 0.0 || border > 0.0) {
// Rounded rect: SDF alpha, vertex color only (no texture sample)
vec2 half_size = rectSize * 0.5;
vec2 center = (vUV - vec2(0.5)) * rectSize;
float dist = roundedBoxSDF(center, half_size, mode);
float aa = fwidth(dist);
float alpha = 1.0 - smoothstep(-aa, aa, dist);
if (border > 0.0) {
float inner = roundedBoxSDF(center, half_size - vec2(border), max(mode - border, 0.0));
float border_alpha = smoothstep(-aa, aa, inner);
alpha = alpha * max(border_alpha, 0.0);
}
FragColor = vec4(vColor.rgb, vColor.a * alpha);
} else {
// Plain rect: vertex color only (no texture sample)
FragColor = vColor;
}
}
GLSL;
// --- WASM (ES 3.0 / WebGL2) shaders ---
UI_VERT_SRC_ES :: #string GLSL
#version 300 es
precision mediump float;
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 2) in vec4 aColor;
layout(location = 3) in vec4 aParams;
uniform mat4 uProj;
out vec2 vUV;
out vec4 vColor;
out vec4 vParams;
void main() {
gl_Position = uProj * vec4(aPos, 0.0, 1.0);
vUV = aUV;
vColor = aColor;
vParams = aParams;
}
GLSL;
UI_FRAG_SRC_ES :: #string GLSL
#version 300 es
precision mediump float;
in vec2 vUV;
in vec4 vColor;
in vec4 vParams;
uniform sampler2D uTex;
out vec4 FragColor;
float roundedBoxSDF(vec2 center, vec2 half_size, float radius) {
vec2 q = abs(center) - half_size + vec2(radius);
return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - radius;
}
void main() {
float mode = vParams.x;
float border = vParams.y;
vec2 rectSize = vParams.zw;
if (mode < -1.5) {
// Image mode (mode == -2.0): sample texture
FragColor = texture(uTex, vUV) * vColor;
} else if (mode < 0.0) {
// Text mode (mode == -1.0): sample glyph atlas .r as alpha
float alpha = texture(uTex, vUV).r;
float ew = fwidth(alpha) * 0.7;
alpha = smoothstep(0.5 - ew, 0.5 + ew, alpha);
FragColor = vec4(vColor.rgb, vColor.a * pow(alpha, 0.9));
} else if (mode > 0.0 || border > 0.0) {
// Rounded rect: SDF alpha, vertex color only
vec2 half_size = rectSize * 0.5;
vec2 center = (vUV - vec2(0.5)) * rectSize;
float dist = roundedBoxSDF(center, half_size, mode);
float aa = fwidth(dist);
float alpha = 1.0 - smoothstep(-aa, aa, dist);
if (border > 0.0) {
float inner = roundedBoxSDF(center, half_size - vec2(border), max(mode - border, 0.0));
float border_alpha = smoothstep(-aa, aa, inner);
alpha = alpha * max(border_alpha, 0.0);
}
FragColor = vec4(vColor.rgb, vColor.a * alpha);
} else {
// Plain rect: vertex color only
FragColor = vColor;
}
}
GLSL;
// --- Metal (MSL) — single library with vmain/fmain entry points ---
//
// `packed_float2 / packed_float4` keep the 12-float interleaved vertex
// layout (pos2 / uv2 / color4 / params4 = 48 bytes) without padding —
// MSL's default `float4` has 16-byte alignment and would force a 64-byte
// struct (see examples/63-metal-clear.sx for the gotcha).
//
// Uniform passing: GL uses `glUniformMatrix4fv("uProj", proj)`; Metal
// receives the projection via `setVertexBytes:length:atIndex:1` (slot 0
// is the vertex buffer). Texture binding goes through
// `setFragmentTexture:atIndex:0`.
UI_MSL_SRC :: #string MSL
#include <metal_stdlib>
using namespace metal;
struct UIVertex {
packed_float2 pos;
packed_float2 uv;
packed_float4 color;
packed_float4 params;
};
struct VOut {
float4 position [[position]];
float2 uv;
float4 color;
float4 params;
};
vertex VOut vmain(uint vid [[vertex_id]],
constant UIVertex* verts [[buffer(0)]],
constant float4x4& proj [[buffer(1)]]) {
UIVertex v = verts[vid];
VOut o;
o.position = proj * float4(v.pos, 0.0, 1.0);
o.uv = float2(v.uv);
o.color = float4(v.color);
o.params = float4(v.params);
return o;
}
static float roundedBoxSDF(float2 center, float2 half_size, float radius) {
float2 q = abs(center) - half_size + float2(radius);
return length(max(q, float2(0.0))) + min(max(q.x, q.y), 0.0) - radius;
}
fragment float4 fmain(VOut in [[stage_in]],
texture2d<float> tex [[texture(0)]]) {
constexpr sampler s(coord::normalized, address::clamp_to_edge, filter::linear);
float mode = in.params.x;
float border = in.params.y;
float2 rectSize = in.params.zw;
if (mode < -1.5) {
// 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): the glyph atlas stores R8 alpha coverage.
float alpha = tex.sample(s, in.uv).r;
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;
float2 center = (in.uv - float2(0.5)) * rectSize;
float dist = roundedBoxSDF(center, half_size, mode);
float aa = fwidth(dist);
float alpha = 1.0 - smoothstep(-aa, aa, dist);
if (border > 0.0) {
float inner = roundedBoxSDF(center, half_size - float2(border), max(mode - border, 0.0));
float border_alpha = smoothstep(-aa, aa, inner);
alpha = alpha * max(border_alpha, 0.0);
}
return float4(in.color.rgb, in.color.a * alpha);
} else {
// Plain rect: vertex color only
return in.color;
}
}
MSL;