193 lines
8.6 KiB
Zig
193 lines
8.6 KiB
Zig
//! 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
|
||
}
|