//! The compositor's pure core: rectangle math and the three blitting primitives the //! display service composes frames from — fill a rectangle of a surface, composite one //! surface onto another clipped to a damage rectangle, and copy a client-supplied pixel //! tile in. Deliberately free of any syscall or `runtime` dependency (it takes plain //! pixel pointers), so it is host-tested under `zig build test`. The service //! (system/services/display/display.zig) wires real mmap'd surfaces and the framebuffer //! to it. Pixels are opaque native 32-bit values — v1 layers don't alpha-blend, and //! channel order (rgbx/bgrx) is the caller's concern (see protocol.pack). const std = @import("std"); /// An axis-aligned rectangle in pixels. Signed, so a surface partly off-screen (a layer /// dragged past an edge) clips with plain arithmetic. Half-open: covers [x, x+w) × [y, y+h). pub const Rect = struct { x: i32, y: i32, w: i32, h: i32, pub const empty = Rect{ .x = 0, .y = 0, .w = 0, .h = 0 }; pub fn init(x: i32, y: i32, w: i32, h: i32) Rect { return .{ .x = x, .y = y, .w = w, .h = h }; } pub fn isEmpty(r: Rect) bool { return r.w <= 0 or r.h <= 0; } pub fn right(r: Rect) i32 { return r.x + r.w; } pub fn bottom(r: Rect) i32 { return r.y + r.h; } /// The overlap of two rectangles, or an empty rectangle if they don't touch. pub fn intersect(a: Rect, b: Rect) Rect { const x0 = @max(a.x, b.x); const y0 = @max(a.y, b.y); const x1 = @min(a.right(), b.right()); const y1 = @min(a.bottom(), b.bottom()); return .{ .x = x0, .y = y0, .w = x1 - x0, .h = y1 - y0 }; } /// The bounding box of two rectangles. An empty operand contributes nothing (returns /// the other), so folding damage rectangles with `unite` from `empty` yields their /// bounding box. pub fn unite(a: Rect, b: Rect) Rect { if (a.isEmpty()) return b; if (b.isEmpty()) return a; const x0 = @min(a.x, b.x); const y0 = @min(a.y, b.y); const x1 = @max(a.right(), b.right()); const y1 = @max(a.bottom(), b.bottom()); return .{ .x = x0, .y = y0, .w = x1 - x0, .h = y1 - y0 }; } }; /// A block of 32-bit pixels: `pixels` addressed row-major with `stride` pixels between /// row starts (≥ width — the framebuffer's stride is pitch/4, a layer's is its width). pub const Surface = struct { pixels: [*]u32, stride: u32, // pixels per row width: u32, height: u32, pub fn bounds(s: Surface) Rect { return .{ .x = 0, .y = 0, .w = @intCast(s.width), .h = @intCast(s.height) }; } inline fn row(s: Surface, y: u32) [*]u32 { return s.pixels + @as(usize, y) * s.stride; } }; /// Fill `rect` of `s` with the native pixel `colour`, clipped to `s`'s bounds. pub fn fillRect(s: Surface, rect: Rect, colour: u32) void { const c = rect.intersect(s.bounds()); if (c.isEmpty()) return; var y: i32 = c.y; while (y < c.bottom()) : (y += 1) { const r = s.row(@intCast(y)); var x: i32 = c.x; while (x < c.right()) : (x += 1) r[@intCast(x)] = colour; } } /// Composite the whole of `layer` onto `dst` with the layer's top-left at (`dx`, `dy`), /// painting only the pixels that fall inside `clip` (a `dst`-space rectangle) and inside /// `dst`. Opaque copy. This is the primitive `present` repeats over the visible layer /// stack, bottom to top, for each damaged region. pub fn composite(dst: Surface, dx: i32, dy: i32, layer: Surface, clip: Rect) void { const on_screen = Rect{ .x = dx, .y = dy, .w = @intCast(layer.width), .h = @intCast(layer.height) }; const region = on_screen.intersect(clip).intersect(dst.bounds()); if (region.isEmpty()) return; var y: i32 = region.y; while (y < region.bottom()) : (y += 1) { const src = layer.row(@intCast(y - dy)); const d = dst.row(@intCast(y)); var x: i32 = region.x; while (x < region.right()) : (x += 1) { d[@intCast(x)] = src[@intCast(x - dx)]; } } } /// Copy a `w`×`h` tile of native pixels from `src` (raw little-endian bytes, row-major, /// tightly packed) into `dst` at (`dx`, `dy`), clipped to `dst`'s bounds. `src` is read /// with `readInt` because it comes straight out of an IPC message buffer and carries no /// alignment guarantee. Returns without touching anything if `src` is short. pub fn blitTile(dst: Surface, dx: i32, dy: i32, src: []const u8, w: u32, h: u32) void { if (src.len < @as(usize, w) * h * 4) return; var ty: u32 = 0; while (ty < h) : (ty += 1) { const yy = dy + @as(i32, @intCast(ty)); if (yy < 0 or yy >= dst.height) continue; const drow = dst.row(@intCast(yy)); var tx: u32 = 0; while (tx < w) : (tx += 1) { const xx = dx + @as(i32, @intCast(tx)); if (xx < 0 or xx >= dst.width) continue; const off = (@as(usize, ty) * w + tx) * 4; drow[@intCast(xx)] = std.mem.readInt(u32, src[off..][0..4], .little); } } } // --- tests ------------------------------------------------------------------ test "rect intersect: overlap and disjoint" { try std.testing.expectEqual(Rect.init(5, 5, 5, 5), Rect.init(0, 0, 10, 10).intersect(Rect.init(5, 5, 10, 10))); try std.testing.expect(Rect.init(0, 0, 10, 10).intersect(Rect.init(20, 20, 5, 5)).isEmpty()); } test "rect unite: bounding box, empty is identity" { const a = Rect.init(2, 2, 4, 4); try std.testing.expectEqual(Rect.init(2, 1, 10, 5), a.unite(Rect.init(10, 1, 2, 2))); try std.testing.expectEqual(a, a.unite(Rect.empty)); try std.testing.expectEqual(a, Rect.empty.unite(a)); } test "fillRect clips to surface and honours stride padding" { // A 4×3 surface inside a 6-wide allocation (stride 6 > width 4), like pitch padding. var mem = [_]u32{0} ** (6 * 3); const s = Surface{ .pixels = &mem, .stride = 6, .width = 4, .height = 3 }; fillRect(s, Rect.init(-1, -1, 3, 3), 0xAB); // straddles the top-left corner try std.testing.expectEqual(@as(u32, 0xAB), mem[0 * 6 + 0]); try std.testing.expectEqual(@as(u32, 0xAB), mem[1 * 6 + 1]); try std.testing.expectEqual(@as(u32, 0), mem[0 * 6 + 2]); // beyond the 2-wide fill try std.testing.expectEqual(@as(u32, 0), mem[2 * 6 + 0]); // row 2 untouched try std.testing.expectEqual(@as(u32, 0), mem[0 * 6 + 4]); // stride padding untouched } test "composite: overlap shows the top layer, clipped to damage" { var back = [_]u32{0} ** (8 * 8); const dst = Surface{ .pixels = &back, .stride = 8, .width = 8, .height = 8 }; var lo = [_]u32{0x11} ** (4 * 4); var hi = [_]u32{0x22} ** (4 * 4); const low = Surface{ .pixels = &lo, .stride = 4, .width = 4, .height = 4 }; const high = Surface{ .pixels = &hi, .stride = 4, .width = 4, .height = 4 }; composite(dst, 0, 0, low, dst.bounds()); // bottom at (0,0) composite(dst, 2, 2, high, dst.bounds()); // top overlaps at (2,2) try std.testing.expectEqual(@as(u32, 0x11), back[0 * 8 + 0]); // bottom-only try std.testing.expectEqual(@as(u32, 0x22), back[3 * 8 + 3]); // overlap → top wins try std.testing.expectEqual(@as(u32, 0x22), back[5 * 8 + 5]); // top-only try std.testing.expectEqual(@as(u32, 0), back[7 * 8 + 7]); // neither } test "composite honours the damage rectangle" { var back = [_]u32{0} ** (8 * 8); const dst = Surface{ .pixels = &back, .stride = 8, .width = 8, .height = 8 }; var fill = [_]u32{0x33} ** (8 * 8); const layer = Surface{ .pixels = &fill, .stride = 8, .width = 8, .height = 8 }; composite(dst, 0, 0, layer, Rect.init(2, 2, 2, 2)); // only this damage region try std.testing.expectEqual(@as(u32, 0x33), back[2 * 8 + 2]); try std.testing.expectEqual(@as(u32, 0x33), back[3 * 8 + 3]); try std.testing.expectEqual(@as(u32, 0), back[1 * 8 + 1]); // outside damage try std.testing.expectEqual(@as(u32, 0), back[4 * 8 + 4]); // outside damage } test "blitTile copies a packed tile, clipping and reading unaligned bytes" { var back = [_]u32{0} ** (4 * 4); const dst = Surface{ .pixels = &back, .stride = 4, .width = 4, .height = 4 }; // A 2×2 tile in a byte buffer offset by one byte, so reads are unaligned. var raw = [_]u8{0} ** (1 + 2 * 2 * 4); const tile = raw[1..]; for (0..4) |i| std.mem.writeInt(u32, tile[i * 4 ..][0..4], @intCast(0xA0 + i), .little); blitTile(dst, 3, 3, tile, 2, 2); // bottom-right corner; only (3,3) lands on-surface try std.testing.expectEqual(@as(u32, 0xA0), back[3 * 4 + 3]); try std.testing.expectEqual(@as(u32, 0), back[0]); // nothing else touched }