// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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); }