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:
@@ -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