summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.zig110
1 files changed, 106 insertions, 4 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index e3229b38..4537f192 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1800,6 +1800,9 @@ const Gui = struct {
native_images: std.AutoHashMapUnmanaged(pardes.ImageCacheKey, *c.SDL_GPUTexture) = .empty,
presented_images: std.ArrayListUnmanaged(SavedImagePlace) = .empty,
prepared_images: std.ArrayListUnmanaged(PreparedImage) = .empty,
+ /// renderFrame's cover of the grid by layers (coverLayers): this frame's
+ /// layers, then the layers a panel transition is leaving.
+ layer_cover: std.ArrayListUnmanaged(bool) = .empty,
font: *c.UIFont,
font_bytes: []u8 = &.{},
@@ -2397,6 +2400,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
g.native_images.deinit(gpa);
g.presented_images.deinit(gpa);
g.prepared_images.deinit(gpa);
+ g.layer_cover.deinit(gpa);
}
defer c.SDL_ReleaseGPUTransferBuffer(device, g.image_vxfer);
defer c.SDL_ReleaseGPUBuffer(device, g.image_vbuf);
@@ -5120,6 +5124,13 @@ fn renderFrame(
}
const cells: u32 = @as(u32, surface.cols) * surface.rows;
+ try g.layer_cover.resize(gpa, @as(usize, cells) * 2);
+ const covered = g.layer_cover.items[0..cells];
+ coverLayers(covered, surface.cols, surface.rows, surface.bodyLayers(), surface.tagLayers());
+ // Only a panel transition paints the grid under the layers it leaves.
+ const previous_covered = g.layer_cover.items[cells..];
+ if (paint_plan.len > 1)
+ coverLayers(previous_covered, surface.cols, surface.rows, surface.previous_body_layers, surface.previous_tag_layers);
const page = ground(theme_bg, g.transparent);
const background_opacity = backgroundOpacity(g.applied_window_opacity);
@@ -5143,7 +5154,7 @@ fn renderFrame(
while (row < surface.rows) : (row += 1) {
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
- if (bodyLayerAt(surface, col, row) != null or tagLayerIn(surface.tagLayers(), col, row) != null) continue;
+ if (covered[@as(usize, row) * surface.cols + col]) continue;
const batch_index = paintBatchAt(&paint_plan, col, row);
const track = paint_plan.batches[batch_index].track;
const data_effect = if (track) |active|
@@ -5172,7 +5183,7 @@ fn renderFrame(
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
if (!boxContains(track.contentBox(), col, row)) continue;
- if (bodyLayerIn(surface.previous_body_layers, col, row) != null or tagLayerIn(surface.previous_tag_layers, col, row) != null) continue;
+ if (previous_covered[@as(usize, row) * surface.cols + col]) continue;
const destination = if (duplicate_under) &paint_plan.batches[0] else batch;
const logical_idx: u32 = @as(u32, row) * surface.cols + col;
destination.cell_count = std.math.add(
@@ -5246,7 +5257,7 @@ fn renderFrame(
const line_cursor: ?usize = if (surface.cursor) |cu| if (cu.y == row) cu.x else null else null;
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
- if (bodyLayerAt(surface, col, row) != null or tagLayerIn(surface.tagLayers(), col, row) != null) continue;
+ if (covered[@as(usize, row) * surface.cols + col]) continue;
const batch_index = paintBatchAt(&paint_plan, col, row);
const track = paint_plan.batches[batch_index].track;
const logical_idx: u32 = @as(u32, row) * surface.cols + col;
@@ -5340,7 +5351,7 @@ fn renderFrame(
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
if (!boxContains(track.contentBox(), col, row)) continue;
- if (bodyLayerIn(surface.previous_body_layers, col, row) != null or tagLayerIn(surface.previous_tag_layers, col, row) != null) continue;
+ if (previous_covered[@as(usize, row) * surface.cols + col]) continue;
const destination: usize = if (duplicate_under) 0 else batch_index;
emitSurfaceCell(
g,
@@ -5632,6 +5643,97 @@ fn tagLayerIn(layers: []const pardes.TagLayer, col: u16, row: u16) ?*const parde
return null;
}
+/// Marks the grid cells a body or tag layer covers, which the layers paint
+/// themselves: the grid's cell loops skip them. Filling each layer's rectangle
+/// once replaces scanning every layer for every cell, which was a third of the
+/// gui's CPU on a full screen of text. It marks exactly the cells bodyLayerIn
+/// and tagLayerIn find a layer for: a tag layer covers only its viewport's
+/// first row, and a viewport past the grid covers the grid's part of it.
+fn coverLayers(cover: []bool, cols: u16, rows: u16, body_layers: []const pardes.BodyLayer, tag_layers: []const pardes.TagLayer) void {
+ @memset(cover, false);
+ for (body_layers) |*layer| {
+ const r = layer.viewport;
+ const right = @min(cols, @as(u32, r.x) + r.w);
+ if (layer.rows == 0 or r.x >= right) continue;
+ var y: u32 = r.y;
+ while (y < @min(rows, @as(u32, r.y) + r.h)) : (y += 1) @memset(cover[y * cols ..][r.x..right], true);
+ }
+ for (tag_layers) |*layer| {
+ const r = layer.viewport;
+ const right = @min(cols, @as(u32, r.x) + r.w);
+ if (layer.cols == 0 or r.y >= rows or r.x >= right) continue;
+ @memset(cover[@as(usize, r.y) * cols ..][r.x..right], true);
+ }
+}
+
+fn expectCoverIsScan(cover: []const bool, cols: u16, rows: u16, body_layers: []const pardes.BodyLayer, tag_layers: []const pardes.TagLayer) !void {
+ for (0..rows) |row| for (0..cols) |col| {
+ const scan = bodyLayerIn(body_layers, @intCast(col), @intCast(row)) != null or tagLayerIn(tag_layers, @intCast(col), @intCast(row)) != null;
+ try std.testing.expectEqual(scan, cover[row * cols + col]);
+ };
+}
+
+test "the layer cover is the per-cell layer scan, over a core's layouts and random layers" {
+ // A gui-shaped core: its tagline is narrower and shorter than the body,
+ // so tags and notices are layers.
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .cols = 97, .rows = 31 });
+ defer core.deinit();
+ core.update(.{ .resize = .{ .cols = 97, .rows = 31, .row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 7, .tagline_h = 13 } } });
+ for (1..4) |id| _ = try core.newShell(id, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ pardes.layout.insert(core, 1, 1, 2);
+ pardes.layout.insert(core, 0, 1, 3);
+ var cover: [97 * 31]bool = undefined;
+ var notices = false;
+ for (0..32) |step| {
+ core.settings.tag_bottom = step & 1 != 0;
+ core.settings.column_tags = step & 2 != 0;
+ if (step & 3 == 0) try std.testing.expect(core.executeBuiltinLine(2, "Collapse"));
+ core.leader_on = step & 4 != 0;
+ if (step & 8 != 0) {
+ try std.testing.expect(core.executeBuiltinLine(0, "Msg a message chip over the body"));
+ try std.testing.expect(core.executeBuiltinLine(3, "Msg another"));
+ }
+ const s = try core.render(core.scratch.allocator());
+ try std.testing.expectEqual(@as(usize, cover.len), @as(usize, s.cols) * s.rows);
+ @memset(&cover, true); // what an earlier frame left
+ coverLayers(&cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers());
+ try expectCoverIsScan(&cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers());
+ for (s.tagLayers()) |layer| notices = notices or (layer.cols != 0 and layer.kind == .notice);
+ }
+ try std.testing.expect(notices);
+
+ // Random layers: overlapping, empty, past the grid's right and bottom
+ // edges, and at the top of u16, where x + w overflows it.
+ const gen = struct {
+ fn coordinate(random: std.Random, limit: u16) u16 {
+ if (random.uintLessThan(u8, 12) == 0) return std.math.maxInt(u16) - random.uintLessThan(u16, 3);
+ return random.uintAtMost(u16, limit + 3);
+ }
+ fn rect(random: std.Random, cols: u16, rows: u16) pardes.Rect {
+ return .{ .x = coordinate(random, cols), .y = coordinate(random, rows), .w = coordinate(random, cols), .h = coordinate(random, rows / 2) };
+ }
+ };
+ var prng = std.Random.DefaultPrng.init(0x6c61796572);
+ const random = prng.random();
+ var body: [pardes.MAX_PANES]pardes.BodyLayer = undefined;
+ var tag: [pardes.MAX_TAG_LAYERS]pardes.TagLayer = undefined;
+ var grid: [64 * 40]bool = undefined;
+ for (0..3000) |_| {
+ const cols = random.intRangeAtMost(u16, 1, 64);
+ const rows = random.intRangeAtMost(u16, 1, 40);
+ const empty = random.intRangeAtMost(u8, 2, 5); // about one layer in `empty` is empty
+ for (&body) |*layer| layer.* = .{ .rows = @intFromBool(random.uintLessThan(u8, empty) != 0), .viewport = gen.rect(random, cols, rows) };
+ for (&tag) |*layer| layer.* = .{ .cols = @intFromBool(random.uintLessThan(u8, empty) != 0), .viewport = gen.rect(random, cols, rows) };
+ const bodies = body[0..random.uintAtMost(usize, body.len)];
+ const tags = tag[0..random.uintAtMost(usize, tag.len)];
+ const cells = grid[0 .. @as(usize, cols) * rows];
+ @memset(cells, random.boolean());
+ coverLayers(cells, cols, rows, bodies, tags);
+ try expectCoverIsScan(cells, cols, rows, bodies, tags);
+ }
+}
+
fn tagLayerCellCount(g: *const Gui, layer: *const pardes.TagLayer) u32 {
const pixels = @as(u32, layer.viewport.w) * g.cell_w;
return @min(layer.cols, (pixels + g.tagline_width - 1) / g.tagline_width);