//! 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 }; } }; /// The dirty screen regions accumulated between presents. Kept as a *list* of rectangles, /// not one bounding box: when two small things move far apart — the cursor on one side of /// the screen, an animating layer on the other — a single bounding box unites them into a /// huge region, and presenting it streams megabytes to the framebuffer for a few thousand /// changed pixels. The long copy widens the window in which scanout (or QEMU's display /// refresh) samples a half-written frame — visible as tearing and cursor trails. Small /// separate rectangles keep each copy, and that window, tight. /// /// A new rectangle that overlaps an existing entry is united into it (repainting a modest /// superset is harmless — compositing is idempotent); the grown entry is *not* re-merged /// against the rest, so entries may overlap, which costs only a duplicate repaint. When /// the table is full the newcomer folds into the last entry — degrading toward the old /// bounding-box behaviour instead of dropping damage. pub const DamageList = struct { pub const capacity = 16; rects: [capacity]Rect = [_]Rect{Rect.empty} ** capacity, count: usize = 0, pub fn add(self: *DamageList, r: Rect) void { if (r.isEmpty()) return; for (self.rects[0..self.count]) |*existing| { if (!existing.intersect(r).isEmpty()) { existing.* = existing.unite(r); return; } } if (self.count < capacity) { self.rects[self.count] = r; self.count += 1; return; } self.rects[capacity - 1] = self.rects[capacity - 1].unite(r); } pub fn isEmpty(self: *const DamageList) bool { return self.count == 0; } pub fn slice(self: *const DamageList) []const Rect { return self.rects[0..self.count]; } pub fn clear(self: *DamageList) void { self.count = 0; } }; /// The alternative damage tracker: a **fixed tile grid**, the scheme browser compositors /// and tile-based GPUs use. The screen is divided into `tile_size`-pixel tiles up front; /// `add` marks the tiles a rectangle touches (a bit per tile — merging is free and exact, /// no heuristics), and `collect` walks the grid turning runs of adjacent dirty tiles into /// repaint rectangles (horizontal runs, then equal-span rows merged vertically, so /// full-screen damage collapses back to a single rectangle). /// /// Trade-off against `DamageList`: tracking is O(1) with a strictly bounded worst case /// (never more than the dirty tiles), but repaints are quantized — a 1-pixel change /// repaints a whole tile. Which wins depends on the workload; the display service has a /// compile-time switch (`damage_mode`) to compare them. pub const TileGrid = struct { pub const tile_size = 64; pub const maximum_columns = 128; // supports screens up to 8192 px wide… pub const maximum_rows = 128; // …and 8192 px tall (beyond that, edge tiles stretch) pub const maximum_tiles = maximum_columns * maximum_rows; /// The most rectangles `collect` produces; extras fold into the last (never dropped). pub const maximum_rects = 64; width: u32 = 0, height: u32 = 0, columns: u32 = 0, rows: u32 = 0, dirty_count: u32 = 0, dirty: [maximum_tiles]bool = [_]bool{false} ** maximum_tiles, /// Size the grid for a screen. Also clears it — callers reset on a geometry change, /// where the mode-set paths damage the whole new screen anyway. pub fn reset(self: *TileGrid, width: u32, height: u32) void { self.width = width; self.height = height; self.columns = @min((width + tile_size - 1) / tile_size, maximum_columns); self.rows = @min((height + tile_size - 1) / tile_size, maximum_rows); self.clear(); } pub fn matches(self: *const TileGrid, width: u32, height: u32) bool { return self.width == width and self.height == height; } pub fn isEmpty(self: *const TileGrid) bool { return self.dirty_count == 0; } pub fn clear(self: *TileGrid) void { @memset(&self.dirty, false); self.dirty_count = 0; } /// Mark every tile `r` touches. Clips to the screen first, so out-of-range /// rectangles are harmless. pub fn add(self: *TileGrid, r: Rect) void { const screen = Rect{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) }; const c = r.intersect(screen); if (c.isEmpty()) return; const column_first: u32 = @intCast(@divTrunc(c.x, tile_size)); const row_first: u32 = @intCast(@divTrunc(c.y, tile_size)); const column_last: u32 = @min(@as(u32, @intCast(@divTrunc(c.right() - 1, tile_size))), self.columns - 1); const row_last: u32 = @min(@as(u32, @intCast(@divTrunc(c.bottom() - 1, tile_size))), self.rows - 1); var row = row_first; while (row <= row_last) : (row += 1) { var column = column_first; while (column <= column_last) : (column += 1) { const index = row * self.columns + column; if (!self.dirty[index]) { self.dirty[index] = true; self.dirty_count += 1; } } } } /// The screen rectangle covered by tiles [column_first, column_end) of `row`. Edge /// tiles clamp to the true screen size (the last column/row may be partial — or, on a /// screen wider than the grid supports, stretched to cover the remainder). fn tileSpanRect(self: *const TileGrid, column_first: u32, column_end: u32, row: u32) Rect { const x: i32 = @intCast(column_first * tile_size); const y: i32 = @intCast(row * tile_size); const right: i32 = if (column_end >= self.columns) @intCast(self.width) else @intCast(column_end * tile_size); const bottom: i32 = if (row + 1 >= self.rows) @intCast(self.height) else @intCast((row + 1) * tile_size); return .{ .x = x, .y = y, .w = right - x, .h = bottom - y }; } /// Turn the dirty tiles into repaint rectangles in `out`: coalesce each row's runs of /// adjacent dirty tiles, then merge a run into the rectangle directly above it when /// the spans match — so a dirty block of tiles becomes one rectangle. Returns the /// filled prefix of `out`. pub fn collect(self: *const TileGrid, out: []Rect) []Rect { var count: usize = 0; var row: u32 = 0; while (row < self.rows) : (row += 1) { var column: u32 = 0; while (column < self.columns) { if (!self.dirty[row * self.columns + column]) { column += 1; continue; } var run_end = column + 1; while (run_end < self.columns and self.dirty[row * self.columns + run_end]) run_end += 1; const rect = self.tileSpanRect(column, run_end, row); column = run_end; var merged = false; for (out[0..count]) |*existing| { if (existing.x == rect.x and existing.w == rect.w and existing.bottom() == rect.y) { existing.h += rect.h; merged = true; break; } } if (merged) continue; if (count < out.len) { out[count] = rect; count += 1; } else { out[count - 1] = out[count - 1].unite(rect); } } } return out[0..count]; } }; /// 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. Each row is /// one `@memset` over the clipped span, so the compiler vectorizes it and the bounds check /// runs once per row, not once per pixel. pub fn fillRect(s: Surface, rect: Rect, colour: u32) void { const c = rect.intersect(s.bounds()); if (c.isEmpty()) return; const x0: usize = @intCast(c.x); const span: usize = @intCast(c.w); var y: i32 = c.y; while (y < c.bottom()) : (y += 1) { @memset((s.row(@intCast(y)) + x0)[0..span], 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; const span: usize = @intCast(region.w); const dst_x: usize = @intCast(region.x); const src_x: usize = @intCast(region.x - dx); var y: i32 = region.y; while (y < region.bottom()) : (y += 1) { const source_row = layer.row(@intCast(y - dy)) + src_x; const destination_row = dst.row(@intCast(y)) + dst_x; @memcpy(destination_row[0..span], source_row[0..span]); } } /// 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` comes /// straight out of an IPC message buffer and carries no alignment guarantee, so each /// clipped row is a byte-wise `@memcpy` — which equals the old per-pixel little-endian /// `readInt` on every danos target (all little-endian) without the alignment concern. /// 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; const region = Rect.init(dx, dy, @intCast(w), @intCast(h)).intersect(dst.bounds()); if (region.isEmpty()) return; const span: usize = @intCast(region.w); const tile_x: usize = @intCast(region.x - dx); const dst_x: usize = @intCast(region.x); var y: i32 = region.y; while (y < region.bottom()) : (y += 1) { const tile_y: usize = @intCast(y - dy); const offset = (tile_y * w + tile_x) * 4; const destination_row = dst.row(@intCast(y)) + dst_x; @memcpy(std.mem.sliceAsBytes(destination_row[0..span]), src[offset..][0 .. span * 4]); } } // --- 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 "damage list keeps disjoint rectangles separate and merges overlap" { var list = DamageList{}; list.add(Rect.init(0, 0, 10, 10)); list.add(Rect.init(100, 100, 10, 10)); // far away: its own entry try std.testing.expectEqual(@as(usize, 2), list.slice().len); list.add(Rect.init(5, 5, 10, 10)); // overlaps the first: united into it try std.testing.expectEqual(@as(usize, 2), list.slice().len); try std.testing.expectEqual(Rect.init(0, 0, 15, 15), list.slice()[0]); try std.testing.expect(!list.isEmpty()); list.clear(); try std.testing.expect(list.isEmpty()); } test "damage list folds overflow into the last entry instead of dropping it" { var list = DamageList{}; var i: i32 = 0; while (i < DamageList.capacity) : (i += 1) { list.add(Rect.init(i * 100, 0, 10, 10)); // disjoint: fills every slot } try std.testing.expectEqual(@as(usize, DamageList.capacity), list.slice().len); const overflow = Rect.init(0, 5000, 10, 10); list.add(overflow); try std.testing.expectEqual(@as(usize, DamageList.capacity), list.slice().len); const last = list.slice()[DamageList.capacity - 1]; try std.testing.expect(!last.intersect(overflow).isEmpty()); // still covered } test "damage list ignores empty rectangles" { var list = DamageList{}; list.add(Rect.empty); try std.testing.expect(list.isEmpty()); } test "tile grid coalesces a run of adjacent tiles into one rectangle" { var grid = TileGrid{}; grid.reset(256, 128); // 4×2 tiles of 64 px grid.add(Rect.init(10, 10, 100, 10)); // spans tiles (0,0) and (1,0) var scratch: [TileGrid.maximum_rects]Rect = undefined; const rects = grid.collect(&scratch); try std.testing.expectEqual(@as(usize, 1), rects.len); try std.testing.expectEqual(Rect.init(0, 0, 128, 64), rects[0]); } test "tile grid: full-screen damage collapses back to a single rectangle" { var grid = TileGrid{}; grid.reset(1280, 720); // 20×12 tiles; the bottom row is partial (720 = 11*64 + 16) grid.add(Rect.init(0, 0, 1280, 720)); var scratch: [TileGrid.maximum_rects]Rect = undefined; const rects = grid.collect(&scratch); try std.testing.expectEqual(@as(usize, 1), rects.len); try std.testing.expectEqual(Rect.init(0, 0, 1280, 720), rects[0]); } test "tile grid keeps far-apart damage as separate rectangles" { var grid = TileGrid{}; grid.reset(1280, 720); grid.add(Rect.init(0, 0, 10, 10)); // top-left tile grid.add(Rect.init(1000, 600, 10, 10)); // a far-away tile var scratch: [TileGrid.maximum_rects]Rect = undefined; const rects = grid.collect(&scratch); try std.testing.expectEqual(@as(usize, 2), rects.len); } test "tile grid clamps edge tiles to the true screen size" { var grid = TileGrid{}; grid.reset(100, 100); // 2×2 tiles, both partial in each axis grid.add(Rect.init(0, 0, 100, 100)); var scratch: [TileGrid.maximum_rects]Rect = undefined; const rects = grid.collect(&scratch); try std.testing.expectEqual(@as(usize, 1), rects.len); try std.testing.expectEqual(Rect.init(0, 0, 100, 100), rects[0]); } test "tile grid clear empties it and reset resizes it" { var grid = TileGrid{}; grid.reset(256, 256); grid.add(Rect.init(0, 0, 256, 256)); try std.testing.expect(!grid.isEmpty()); grid.clear(); try std.testing.expect(grid.isEmpty()); try std.testing.expect(grid.matches(256, 256)); grid.reset(512, 512); try std.testing.expect(!grid.matches(256, 256)); try std.testing.expect(grid.isEmpty()); } 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 }