Something went wrong. Try again.
Windows, a GPU renderer and text for Zig programs that draw their own interface.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The renderer's instances, drawn on the CPU into premultiplied ARGB.//!//! For the few small images that go somewhere the GPU is not drawing — a drag icon,//! picture of a tab that follows the pointer while it is dragged — rather//! than a second Vulkan target for a surface that lives a second or two.//! Instances are axis-aligned and unscaled, so this is a fill per solid,//! a copy-and-blend per glyph, and a coverage per pixel for each shape, and//! matches what the shaders draw.
const std = @import("std");
const Atlas = @import("../text/Atlas.zig");const Renderer = @import("Renderer.zig");const Instance = Renderer.Instance;
/// Draws `instances`, in order, over `out`: `width * height` pixels,/// premultiplied ARGB as Wayland's `argb8888` has it in a `u32`. Glyphs/// are read from `alpha` and `color`, the renderer's two atlases; images/// are skipped, and whatever falls outside `out` is cut off.pub fn draw(instances: []const Instance, alpha: *const Atlas, color: *const Atlas, out: []u32, width: u32, height: u32) void { for (instances) |in| { // Nothing drawn here has images, which are textures of their own. const kind = in.kind.base(); if (kind == .image) continue; const x0: i64 = @intFromFloat(@round(in.x)); const y0: i64 = @intFromFloat(@round(in.y)); const w: i64 = @intFromFloat(@round(in.width)); const h: i64 = @intFromFloat(@round(in.height)); const tu: i64 = @intFromFloat(in.u); const tv: i64 = @intFromFloat(in.v); var y = @max(y0, 0); while (y < @min(y0 + h, height)) : (y += 1) { var x = @max(x0, 0); while (x < @min(x0 + w, width)) : (x += 1) { // The pixel's center, for the shapes. const px = @as(f32, @floatFromInt(x)) + 0.5; const py = @as(f32, @floatFromInt(y)) + 0.5; const src: [4]u32 = switch (kind) { .solid => premultiply(in.color, 255), .alpha => blk: { const cov = texel(alpha, tu + (x - x0), tv + (y - y0), 1)[0]; break :blk premultiply(in.color, cov); }, .color => blk: { // Already premultiplied; the instance's alpha fades // it as a whole. const t = texel(color, tu + (x - x0), tv + (y - y0), 4); const a = in.color[3]; break :blk .{ mul(t[0], a), mul(t[1], a), mul(t[2], a), mul(t[3], a) }; }, .line => premultiply(in.color, coverageByte(Renderer.lineCoverage(px, py, in.u, in.v, in.uw, in.uh, in.kind.lineWidth()))), .disc => premultiply(in.color, coverageByte(Renderer.discCoverage(px, py, in.x + in.width / 2, in.y + in.height / 2, @min(in.width, in.height) / 2))), .triangle => premultiply(in.color, coverageByte(Renderer.triangleCoverage(px, py, in.u, in.v, in.uw, in.uh, in.ex, in.ey, in.kind.smoothEdges()))), .image => unreachable, // Nothing else is drawn by anyone. _ => continue, }; const i: usize = @intCast(y * width + x); out[i] = over(src, out[i]); } } }}
test draw { // Atlases are only read for glyphs; these hold nothing. var alpha_data: [4]u8 = @splat(0); var color_data: [16]u8 = @splat(0); const alpha: Atlas = .{ .data = &alpha_data, .size = 2, .format = .alpha8 }; const color: Atlas = .{ .data = &color_data, .size = 2, .format = .rgba };
var out: [4 * 4]u32 = @splat(0); draw(&.{ .{ .x = 1, .y = 1, .width = 2, .height = 2, .color = .{ 255, 0, 0, 255 }, .kind = .solid }, }, &alpha, &color, &out, 4, 4); try std.testing.expectEqual(@as(u32, 0), out[0]); try std.testing.expectEqual(@as(u32, 0xffff0000), out[1 * 4 + 1]);}
fn coverageByte(c: f32) u8 { return @intFromFloat(@round(c * 255));}
fn texel(a: *const Atlas, u: i64, v: i64, comptime depth: u32) [depth]u8 { if (u < 0 or v < 0 or u >= a.size or v >= a.size) return @splat(0); const i: usize = @intCast((v * a.size + u) * depth); return a.data[i..][0..depth].*;}
fn mul(c: anytype, a: u8) u32 { return (@as(u32, c) * a + 127) / 255;}
/// Straight RGBA with coverage `cov`, as premultiplied R, G, B, A.fn premultiply(c: [4]u8, cov: u8) [4]u32 { const a = mul(c[3], cov); return .{ mul(c[0], @intCast(a)), mul(c[1], @intCast(a)), mul(c[2], @intCast(a)), a };}
/// `src` over the premultiplied ARGB pixel `dst`.fn over(src: [4]u32, dst: u32) u32 { const inv = 255 - src[3]; const da = dst >> 24; const dr = (dst >> 16) & 0xff; const dg = (dst >> 8) & 0xff; const db = dst & 0xff; const a = src[3] + mul(da, @intCast(inv)); const r = src[0] + mul(dr, @intCast(inv)); const g = src[1] + mul(dg, @intCast(inv)); const b = src[2] + mul(db, @intCast(inv)); const c = struct { fn clamp(v: u32) u32 { return @min(v, 255); } }; return (c.clamp(a) << 24) | (c.clamp(r) << 16) | (c.clamp(g) << 8) | c.clamp(b);}
const testing = std.testing;
test "solids fill, and a translucent one blends over what is there" { var alpha_data: [4]u8 = @splat(0); var color_data: [16]u8 = @splat(0); const alpha: Atlas = .{ .data = &alpha_data, .size = 2, .format = .alpha8 }; const color: Atlas = .{ .data = &color_data, .size = 2, .format = .rgba }; var out: [16]u32 = @splat(0); draw(&.{ .{ .x = 0, .y = 0, .width = 4, .height = 4, .color = .{ 0x10, 0x20, 0x30, 255 }, .kind = .solid }, .{ .x = 1, .y = 1, .width = 2, .height = 1, .color = .{ 255, 255, 255, 128 }, .kind = .solid }, }, &alpha, &color, &out, 4, 4); try testing.expectEqual(@as(u32, 0xff102030), out[0]); // White at half over the dark fill. try testing.expectEqual(@as(u32, 0xff889098), out[5]); try testing.expectEqual(@as(u32, 0xff102030), out[7]);}
test "a glyph's coverage scales its color, clipped to the image" { var alpha_data = [_]u8{ 255, 0, 128, 0 }; var color_data: [16]u8 = @splat(0); const alpha: Atlas = .{ .data = &alpha_data, .size = 2, .format = .alpha8 }; const color: Atlas = .{ .data = &color_data, .size = 2, .format = .rgba }; var out: [4]u32 = @splat(0); draw(&.{ .{ .x = 1, .y = 0, .width = 2, .height = 2, .color = .{ 255, 0, 0, 255 }, .kind = .alpha }, }, &alpha, &color, &out, 2, 2); try testing.expectEqual(@as(u32, 0), out[0]); try testing.expectEqual(@as(u32, 0xffff0000), out[1]); try testing.expectEqual(@as(u32, 0x80800000), out[3]);}
test "a line and a disc cover what they should" { var alpha = try Atlas.init(testing.allocator, 4, .alpha8); defer alpha.deinit(testing.allocator); var color = try Atlas.init(testing.allocator, 4, .rgba); defer color.deinit(testing.allocator); var out: [16 * 16]u32 = @splat(0); const instances = [_]Instance{ // A horizontal line along y = 4.5, two pixels wide, from x 2 to 12. .line(2, 4.5, 12, 4.5, 2, .{ 255, 255, 255, 255 }), // A disc of radius 3 centered at 8, 11. .disc(8, 11, 3, .{ 255, 0, 0, 255 }), }; draw(&instances, &alpha, &color, &out, 16, 16); // On the line, fully; a pixel off it, not at all. try testing.expectEqual(@as(u32, 0xffffffff), out[4 * 16 + 6]); try testing.expectEqual(@as(u32, 0), out[7 * 16 + 6]); // The disc's center is red; its corner, outside the circle, empty. try testing.expectEqual(@as(u32, 0xffff0000), out[10 * 16 + 7]); try testing.expectEqual(@as(u32, 0), out[8 * 16 + 5]);}
test "a triangle covers what it should, whichever way it is wound" { var alpha = try Atlas.init(testing.allocator, 4, .alpha8); defer alpha.deinit(testing.allocator); var color = try Atlas.init(testing.allocator, 4, .rgba); defer color.deinit(testing.allocator); for ([_][6]f32{ .{ 2, 2, 14, 2, 2, 14 }, .{ 2, 2, 2, 14, 14, 2 } }) |t| { var out: [16 * 16]u32 = @splat(0); draw(&.{.triangle(t[0], t[1], t[2], t[3], t[4], t[5], 0b111, .{ 255, 255, 255, 255 })}, &alpha, &color, &out, 16, 16); // Inside, fully; across the long edge, not at all; centered on // it, half. try testing.expectEqual(@as(u32, 0xffffffff), out[4 * 16 + 4]); try testing.expectEqual(@as(u32, 0), out[10 * 16 + 10]); try testing.expectEqual(@as(u32, 0x80808080), out[8 * 16 + 7]); // Outside the corner, nothing. try testing.expectEqual(@as(u32, 0), out[1 * 16 + 1]); }}
test "two triangles sharing a hard edge cover it once, with no seam" { var alpha = try Atlas.init(testing.allocator, 4, .alpha8); defer alpha.deinit(testing.allocator); var color = try Atlas.init(testing.allocator, 4, .rgba); defer color.deinit(testing.allocator); var out: [16 * 16]u32 = @splat(0); // A square cut along its diagonal, which passes through the centers // of the pixels on it; translucent, so that a pixel drawn twice shows. const c: [4]u8 = .{ 255, 255, 255, 128 }; draw(&.{ .triangle(2, 2, 14, 2, 14, 14, 0b011, c), .triangle(14, 14, 2, 14, 2, 2, 0b011, c), }, &alpha, &color, &out, 16, 16); for (2..14) |y| for (2..14) |x| try testing.expectEqual(@as(u32, 0x80808080), out[y * 16 + x]);}
test "a diagonal at no particular angle is covered once" { var alpha = try Atlas.init(testing.allocator, 4, .alpha8); defer alpha.deinit(testing.allocator); var color = try Atlas.init(testing.allocator, 4, .rgba); defer color.deinit(testing.allocator); var out: [32 * 32]u32 = @splat(0); // A quadrilateral cut along the diagonal from 29.3, 2.1 to 3.7, 30.9, // whose sums round differently from either end; nothing it covers is // more than the color's own half. const c: [4]u8 = .{ 255, 255, 255, 128 }; draw(&.{ .triangle(1.1, 1.3, 29.3, 2.1, 3.7, 30.9, 0b101, c), .triangle(29.3, 2.1, 30.7, 29.9, 3.7, 30.9, 0b011, c), }, &alpha, &color, &out, 32, 32); for (out) |px| try testing.expect(px >> 24 <= 0x80);}