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:
Daniel Samson
2026-07-21 11:22:00 +01:00
parent 981a4af7e0
commit 23f915c593
3 changed files with 370 additions and 53 deletions
+42 -15
View File
@@ -110,22 +110,48 @@ pub const Gop = struct {
}; };
} }
/// Stream the damaged rectangle from the back buffer to the write-combining LFB, row by /// Stream each damaged rectangle from the back buffer to the write-combining LFB, row
/// row (sequential writes — what WC memory wants; the LFB is never read). /// by row (sequential writes — what WC memory wants; the LFB is never read). The rows
pub fn present(self: *const Gop, damage: Rect) void { /// are copied by `presentSpan` below, which widens the stores by hand: `volatile`
const c = damage.intersect(.{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) }); /// keeps the compiler from eliding or reordering framebuffer writes, but it also
if (c.isEmpty()) return; /// forbids it from merging them, so a naive per-pixel loop is stuck at one 4-byte
var y: i32 = c.y; /// store per iteration. Keeping each copy small (the damage list) and each store wide
while (y < c.bottom()) : (y += 1) { /// shrinks the window in which scanout can sample a half-written frame.
const off = @as(usize, @intCast(y)) * self.pitch; pub fn present(self: *const Gop, damage: []const Rect) void {
const src: [*]const u32 = @ptrCast(@alignCast(self.back + off)); const bounds = Rect{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) };
const dst: [*]volatile u32 = @ptrCast(@alignCast(self.front + off)); for (damage) |rect| {
var x: i32 = c.x; const c = rect.intersect(bounds);
while (x < c.right()) : (x += 1) dst[@intCast(x)] = src[@intCast(x)]; 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. /// A display mode the native backend can switch to.
pub const Mode = scanout_protocol.Mode; 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 }; 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 /// 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. /// this is a single request over `.scanout` regardless of how many damage rectangles
pub fn present(self: *const VirtioGpu, damage: Rect) void { /// accumulated; the driver transfers + fenced-flushes the whole frame.
pub fn present(self: *const VirtioGpu, damage: []const Rect) void {
_ = damage; _ = damage;
var request = scanout_protocol.Request{ var request = scanout_protocol.Request{
.operation = @intFromEnum(scanout_protocol.Operation.present), .operation = @intFromEnum(scanout_protocol.Operation.present),
@@ -210,7 +237,7 @@ pub const Backend = union(enum) {
inline else => |*b| b.surface(), 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.*) { switch (self.*) {
inline else => |*b| b.present(damage), inline else => |*b| b.present(damage),
} }
+285 -25
View File
@@ -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 /// 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). /// row starts (≥ width — the framebuffer's stride is pitch/4, a layer's is its width).
pub const Surface = struct { 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 { pub fn fillRect(s: Surface, rect: Rect, colour: u32) void {
const c = rect.intersect(s.bounds()); const c = rect.intersect(s.bounds());
if (c.isEmpty()) return; if (c.isEmpty()) return;
const x0: usize = @intCast(c.x);
const span: usize = @intCast(c.w);
var y: i32 = c.y; var y: i32 = c.y;
while (y < c.bottom()) : (y += 1) { while (y < c.bottom()) : (y += 1) {
const r = s.row(@intCast(y)); @memset((s.row(@intCast(y)) + x0)[0..span], colour);
var x: i32 = c.x;
while (x < c.right()) : (x += 1) r[@intCast(x)] = 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 on_screen = Rect{ .x = dx, .y = dy, .w = @intCast(layer.width), .h = @intCast(layer.height) };
const region = on_screen.intersect(clip).intersect(dst.bounds()); const region = on_screen.intersect(clip).intersect(dst.bounds());
if (region.isEmpty()) return; 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; var y: i32 = region.y;
while (y < region.bottom()) : (y += 1) { while (y < region.bottom()) : (y += 1) {
const src = layer.row(@intCast(y - dy)); const source_row = layer.row(@intCast(y - dy)) + src_x;
const d = dst.row(@intCast(y)); const destination_row = dst.row(@intCast(y)) + dst_x;
var x: i32 = region.x; @memcpy(destination_row[0..span], source_row[0..span]);
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, /// 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 /// tightly packed) into `dst` at (`dx`, `dy`), clipped to `dst`'s bounds. `src` comes
/// with `readInt` because it comes straight out of an IPC message buffer and carries no /// straight out of an IPC message buffer and carries no alignment guarantee, so each
/// alignment guarantee. Returns without touching anything if `src` is short. /// 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 { 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; if (src.len < @as(usize, w) * h * 4) return;
var ty: u32 = 0; const region = Rect.init(dx, dy, @intCast(w), @intCast(h)).intersect(dst.bounds());
while (ty < h) : (ty += 1) { if (region.isEmpty()) return;
const yy = dy + @as(i32, @intCast(ty)); const span: usize = @intCast(region.w);
if (yy < 0 or yy >= dst.height) continue; const tile_x: usize = @intCast(region.x - dx);
const drow = dst.row(@intCast(yy)); const dst_x: usize = @intCast(region.x);
var tx: u32 = 0; var y: i32 = region.y;
while (tx < w) : (tx += 1) { while (y < region.bottom()) : (y += 1) {
const xx = dx + @as(i32, @intCast(tx)); const tile_y: usize = @intCast(y - dy);
if (xx < 0 or xx >= dst.width) continue; const offset = (tile_y * w + tile_x) * 4;
const off = (@as(usize, ty) * w + tx) * 4; const destination_row = dst.row(@intCast(y)) + dst_x;
drow[@intCast(xx)] = std.mem.readInt(u32, src[off..][0..4], .little); @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 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" { test "blitTile copies a packed tile, clipping and reading unaligned bytes" {
var back = [_]u32{0} ** (4 * 4); var back = [_]u32{0} ** (4 * 4);
const dst = Surface{ .pixels = &back, .stride = 4, .width = 4, .height = 4 }; const dst = Surface{ .pixels = &back, .stride = 4, .width = 4, .height = 4 };
+43 -13
View File
@@ -50,8 +50,10 @@ var pending_modeset_check: bool = false;
var background: u32 = 0; var background: u32 = 0;
/// The layer stack. A fixed table (a compositor has few top-level surfaces during /// 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 /// bring-up); each used slot owns an mmap'd surface. `damage_list` accumulates the dirty
/// screen region since the last `present`, so a present touches only what changed. /// 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 maximum_layers = 16;
const Layer = struct { const Layer = struct {
@@ -65,7 +67,16 @@ const Layer = struct {
}; };
var layers: [maximum_layers]Layer = [_]Layer{.{}} ** maximum_layers; 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 ------------------------------------------------------- // --- 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) }; 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 { 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) ------------ // --- 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 /// Composite each accumulated damage rectangle into the backend's surface, hand the list
/// put on screen, then clear the damage. A no-op when nothing is dirty. The frame counter /// to the backend to put on screen, then clear the damage. A no-op when nothing is dirty.
/// advances regardless, so callers can name frames. /// The frame counter advances regardless, so callers can name frames.
fn present() void { fn present() void {
const dirty = damage.intersect(screenRect()); var scratch: [compositor.TileGrid.maximum_rects]Rect = undefined;
if (!dirty.isEmpty()) { const dirty: []const Rect = switch (damage_mode) {
compositeInto(dirty); .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); backend.present(dirty);
} }
damage = Rect.empty; switch (damage_mode) {
.list => damage_list.clear(),
.grid => damage_grid.clear(),
}
frames += 1; frames += 1;
// The first present after a native upgrade confirms the composited frame actually reached // 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"). // 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; pending_native_verify = false;
verifyNativePresent(); verifyNativePresent();
} }