display: damage-rect list + tile-grid trackers, vectorizable pixel loops, wide WC stores
Tearing mitigation for the GOP floor, attacking the copy window from three sides: - Damage is no longer one bounding box. Two trackers, A/B-switchable at compile time (display.zig damage_mode): DamageList (free-form dirty rects, overlap-merged) and TileGrid (fixed 64-px tiles, exact O(1) marking, runs coalesced back into rects). Far-apart changes — the cursor here, an animating layer there — no longer unite into one huge repaint. - fillRect/composite/blitTile now work in row spans (@memset/@memcpy), so the compiler vectorizes them and ReleaseSafe bounds checks drop to per-row. - The back->front present streams 8-byte volatile stores (presentSpan); Backend.present takes the rect list, so each present copies only what changed, faster. Host tests cover both trackers; the display QEMU cases all pass.
This commit is contained in:
@@ -110,22 +110,48 @@ pub const Gop = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Stream the damaged rectangle from the back buffer to the write-combining LFB, row by
|
||||
/// row (sequential writes — what WC memory wants; the LFB is never read).
|
||||
pub fn present(self: *const Gop, damage: Rect) void {
|
||||
const c = damage.intersect(.{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) });
|
||||
if (c.isEmpty()) return;
|
||||
var y: i32 = c.y;
|
||||
while (y < c.bottom()) : (y += 1) {
|
||||
const off = @as(usize, @intCast(y)) * self.pitch;
|
||||
const src: [*]const u32 = @ptrCast(@alignCast(self.back + off));
|
||||
const dst: [*]volatile u32 = @ptrCast(@alignCast(self.front + off));
|
||||
var x: i32 = c.x;
|
||||
while (x < c.right()) : (x += 1) dst[@intCast(x)] = src[@intCast(x)];
|
||||
/// Stream each damaged rectangle from the back buffer to the write-combining LFB, row
|
||||
/// by row (sequential writes — what WC memory wants; the LFB is never read). The rows
|
||||
/// are copied by `presentSpan` below, which widens the stores by hand: `volatile`
|
||||
/// keeps the compiler from eliding or reordering framebuffer writes, but it also
|
||||
/// forbids it from merging them, so a naive per-pixel loop is stuck at one 4-byte
|
||||
/// store per iteration. Keeping each copy small (the damage list) and each store wide
|
||||
/// shrinks the window in which scanout can sample a half-written frame.
|
||||
pub fn present(self: *const Gop, damage: []const Rect) void {
|
||||
const bounds = Rect{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) };
|
||||
for (damage) |rect| {
|
||||
const c = rect.intersect(bounds);
|
||||
if (c.isEmpty()) continue;
|
||||
const span: usize = @intCast(c.w);
|
||||
var y: i32 = c.y;
|
||||
while (y < c.bottom()) : (y += 1) {
|
||||
const offset = @as(usize, @intCast(y)) * self.pitch + @as(usize, @intCast(c.x)) * 4;
|
||||
const source: [*]const u32 = @ptrCast(@alignCast(self.back + offset));
|
||||
const front_row: [*]volatile u32 = @ptrCast(@alignCast(self.front + offset));
|
||||
presentSpan(front_row, source, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Copy `count` pixels into the write-combining front buffer with 8-byte volatile stores
|
||||
/// (plus a 4-byte head/tail where the span isn't 8-aligned — pixel spans are always
|
||||
/// 4-aligned). The loads come from the cacheable back buffer and are assembled into a
|
||||
/// `u64` in registers, so nothing here reads the front buffer.
|
||||
fn presentSpan(destination: [*]volatile u32, source: [*]const u32, count: usize) void {
|
||||
var i: usize = 0;
|
||||
if (i < count and (@intFromPtr(destination) & 7) != 0) {
|
||||
destination[0] = source[0];
|
||||
i = 1;
|
||||
}
|
||||
while (i + 2 <= count) : (i += 2) {
|
||||
const pair = @as(u64, source[i]) | (@as(u64, source[i + 1]) << 32);
|
||||
const wide: *volatile u64 = @ptrCast(@alignCast(destination + i));
|
||||
wide.* = pair;
|
||||
}
|
||||
if (i < count) destination[i] = source[i];
|
||||
}
|
||||
|
||||
/// A display mode the native backend can switch to.
|
||||
pub const Mode = scanout_protocol.Mode;
|
||||
|
||||
@@ -152,8 +178,9 @@ pub const VirtioGpu = struct {
|
||||
return .{ .pixels = self.pixels, .stride = self.stride, .width = self.width, .height = self.height };
|
||||
}
|
||||
/// Ask the driver to present. The composited pixels are already in the shared surface, so
|
||||
/// this is a single request over `.scanout`; the driver transfers + fenced-flushes.
|
||||
pub fn present(self: *const VirtioGpu, damage: Rect) void {
|
||||
/// this is a single request over `.scanout` regardless of how many damage rectangles
|
||||
/// accumulated; the driver transfers + fenced-flushes the whole frame.
|
||||
pub fn present(self: *const VirtioGpu, damage: []const Rect) void {
|
||||
_ = damage;
|
||||
var request = scanout_protocol.Request{
|
||||
.operation = @intFromEnum(scanout_protocol.Operation.present),
|
||||
@@ -210,7 +237,7 @@ pub const Backend = union(enum) {
|
||||
inline else => |*b| b.surface(),
|
||||
};
|
||||
}
|
||||
pub fn present(self: *const Backend, damage: Rect) void {
|
||||
pub fn present(self: *const Backend, damage: []const Rect) void {
|
||||
switch (self.*) {
|
||||
inline else => |*b| b.present(damage),
|
||||
}
|
||||
|
||||
@@ -57,6 +57,177 @@ pub const Rect = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// 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 {
|
||||
@@ -74,15 +245,17 @@ pub const Surface = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Fill `rect` of `s` with the native pixel `colour`, clipped to `s`'s bounds.
|
||||
/// 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) {
|
||||
const r = s.row(@intCast(y));
|
||||
var x: i32 = c.x;
|
||||
while (x < c.right()) : (x += 1) r[@intCast(x)] = colour;
|
||||
@memset((s.row(@intCast(y)) + x0)[0..span], colour);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,35 +267,36 @@ pub fn composite(dst: Surface, dx: i32, dy: i32, layer: Surface, clip: Rect) voi
|
||||
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 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)];
|
||||
}
|
||||
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` 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.
|
||||
/// 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;
|
||||
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);
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +353,92 @@ test "composite honours the damage rectangle" {
|
||||
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 };
|
||||
|
||||
@@ -50,8 +50,10 @@ var pending_modeset_check: bool = false;
|
||||
var background: u32 = 0;
|
||||
|
||||
/// The layer stack. A fixed table (a compositor has few top-level surfaces during
|
||||
/// bring-up); each used slot owns an mmap'd surface. `damage` accumulates the dirty
|
||||
/// screen region since the last `present`, so a present touches only what changed.
|
||||
/// bring-up); each used slot owns an mmap'd surface. `damage_list` accumulates the dirty
|
||||
/// screen rectangles since the last `present`, so a present touches only what changed —
|
||||
/// and keeps far-apart changes (the cursor here, an animating layer there) as *separate*
|
||||
/// small copies rather than one huge bounding box (see compositor.DamageList).
|
||||
const maximum_layers = 16;
|
||||
|
||||
const Layer = struct {
|
||||
@@ -65,7 +67,16 @@ const Layer = struct {
|
||||
};
|
||||
|
||||
var layers: [maximum_layers]Layer = [_]Layer{.{}} ** maximum_layers;
|
||||
var damage: Rect = Rect.empty;
|
||||
|
||||
/// Which damage tracker drives `present` — a compile-time A/B switch (both are in
|
||||
/// compositor.zig with the trade-off discussion):
|
||||
/// .list — free-form dirty rectangles (tight bounds, heuristic merging)
|
||||
/// .grid — a fixed 64-px tile grid (exact O(1) merging, tile-quantized repaints)
|
||||
const DamageMode = enum { list, grid };
|
||||
const damage_mode: DamageMode = .grid;
|
||||
|
||||
var damage_list: compositor.DamageList = .{};
|
||||
var damage_grid: compositor.TileGrid = .{};
|
||||
|
||||
// --- geometry helpers -------------------------------------------------------
|
||||
|
||||
@@ -78,9 +89,20 @@ fn layerScreenRect(l: *const Layer) Rect {
|
||||
return .{ .x = l.x, .y = l.y, .w = @intCast(l.surface.width), .h = @intCast(l.surface.height) };
|
||||
}
|
||||
|
||||
/// Add `r` (screen coordinates) to the pending damage, clipped to the screen.
|
||||
/// Add `r` (screen coordinates) to the pending damage, clipped to the screen. In grid
|
||||
/// mode the grid re-sizes itself lazily when the screen geometry changes — every
|
||||
/// geometry-changing path (`attach_scanout`, `set_mode`) damages the whole new screen
|
||||
/// right after, so damage pending from the old geometry is safely superseded.
|
||||
fn addDamage(r: Rect) void {
|
||||
damage = damage.unite(r.intersect(screenRect()));
|
||||
const clipped = r.intersect(screenRect());
|
||||
switch (damage_mode) {
|
||||
.list => damage_list.add(clipped),
|
||||
.grid => {
|
||||
const mode = backend.info();
|
||||
if (!damage_grid.matches(mode.width, mode.height)) damage_grid.reset(mode.width, mode.height);
|
||||
damage_grid.add(clipped);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// --- layer operations (called from onMessage and the self-check) ------------
|
||||
@@ -183,21 +205,29 @@ fn compositeInto(clip: Rect) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Composite the accumulated damage into the backend's surface, hand it to the backend to
|
||||
/// put on screen, then clear the damage. A no-op when nothing is dirty. The frame counter
|
||||
/// advances regardless, so callers can name frames.
|
||||
/// Composite each accumulated damage rectangle into the backend's surface, hand the list
|
||||
/// to the backend to put on screen, then clear the damage. A no-op when nothing is dirty.
|
||||
/// The frame counter advances regardless, so callers can name frames.
|
||||
fn present() void {
|
||||
const dirty = damage.intersect(screenRect());
|
||||
if (!dirty.isEmpty()) {
|
||||
compositeInto(dirty);
|
||||
var scratch: [compositor.TileGrid.maximum_rects]Rect = undefined;
|
||||
const dirty: []const Rect = switch (damage_mode) {
|
||||
.list => damage_list.slice(),
|
||||
.grid => damage_grid.collect(&scratch),
|
||||
};
|
||||
const had_damage = dirty.len != 0;
|
||||
if (had_damage) {
|
||||
for (dirty) |region| compositeInto(region);
|
||||
backend.present(dirty);
|
||||
}
|
||||
damage = Rect.empty;
|
||||
switch (damage_mode) {
|
||||
.list => damage_list.clear(),
|
||||
.grid => damage_grid.clear(),
|
||||
}
|
||||
frames += 1;
|
||||
|
||||
// The first present after a native upgrade confirms the composited frame actually reached
|
||||
// the shared scanout surface (the automated stand-in for "it's on screen").
|
||||
if (pending_native_verify and !dirty.isEmpty()) {
|
||||
if (pending_native_verify and had_damage) {
|
||||
pending_native_verify = false;
|
||||
verifyNativePresent();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user