summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig72
1 files changed, 64 insertions, 8 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 4537f192..0b9de624 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1826,7 +1826,10 @@ const Gui = struct {
glyphs: std.AutoHashMap(GlyphKey, Slot),
pen_x: u32 = 0,
pen_y: u32 = 0,
- atlas_dirty: bool = true,
+ /// The atlas rows drawn into since the last upload, [top, bottom): the
+ /// whole atlas at first, since the texture starts undefined.
+ atlas_dirty_top: u32 = 0,
+ atlas_dirty_bottom: u32 = atlas_h,
space_slot: Slot = .{ .u = 0, .v = 0 },
words: Words = .{},
@@ -5381,7 +5384,7 @@ fn renderFrame(
shifted = emitScrollRows(g, instances, cell_total, surface, layout, win_w, win_h, page);
c.SDL_UnmapGPUTransferBuffer(g.device, g.vxfer.?);
- if (g.atlas_dirty) uploadAtlas(g, cmd);
+ if (g.atlas_dirty_top < g.atlas_dirty_bottom) uploadAtlas(g, cmd);
const copy = c.SDL_BeginGPUCopyPass(cmd);
const src = c.SDL_GPUTransferBufferLocation{ .transfer_buffer = g.vxfer.?, .offset = 0 };
@@ -6158,7 +6161,8 @@ fn resetGlyphAtlas(g: *Gui) void {
g.space_slot = .{ .u = 0, .v = 0 };
g.pen_x = g.cell_w;
g.pen_y = 0;
- g.atlas_dirty = true;
+ g.atlas_dirty_top = 0;
+ g.atlas_dirty_bottom = atlas_h;
}
/// A changed Ligatures setting drops what was resolved under the old one: the
@@ -6539,9 +6543,10 @@ fn cachedGlyph(g: *Gui, key: GlyphKey) ?Slot {
}
decorateLine(out, atlas_w, raster_w, raster_h, baseline, key.decoration);
if (key.centered) centerGlyphInk(out, raster_w, raster_h);
+ g.atlas_dirty_top = @min(g.atlas_dirty_top, s.v);
+ g.atlas_dirty_bottom = @max(g.atlas_dirty_bottom, s.v + raster_h);
g.glyphs.put(key, s) catch return null;
g.pen_x += slot_w;
- g.atlas_dirty = true;
return s;
}
@@ -6720,18 +6725,69 @@ test "glyph decorations render spaces and stay inside their atlas slot" {
try std.testing.expect(!GlyphDecoration.fromCell(&hidden).drawsBlank());
}
+/// Uploads only the atlas rows drawn into since the last upload. Glyphs fill
+/// the atlas pen-style, a band of rows at a time, so new glyphs are a band a
+/// cell or two tall, not the whole 4 MB texture. The band sits at the same
+/// offset in the transfer buffer as in the stage, and the texture keeps the
+/// rows outside it.
fn uploadAtlas(g: *Gui, cmd: *c.SDL_GPUCommandBuffer) void {
+ const top = g.atlas_dirty_top;
+ const rows = g.atlas_dirty_bottom - top;
const ptr = c.SDL_MapGPUTransferBuffer(g.device, g.atlas_xfer, false) orelse return;
const dst: [*]u8 = @ptrCast(ptr);
- @memcpy(dst[0 .. atlas_w * atlas_h], g.atlas_stage);
+ @memcpy(dst[top * atlas_w ..][0 .. rows * atlas_w], g.atlas_stage[top * atlas_w ..][0 .. rows * atlas_w]);
c.SDL_UnmapGPUTransferBuffer(g.device, g.atlas_xfer);
const copy = c.SDL_BeginGPUCopyPass(cmd);
- const src = c.SDL_GPUTextureTransferInfo{ .transfer_buffer = g.atlas_xfer, .offset = 0, .pixels_per_row = atlas_w, .rows_per_layer = atlas_h };
- const dstregion = c.SDL_GPUTextureRegion{ .texture = g.atlas_tex, .mip_level = 0, .layer = 0, .x = 0, .y = 0, .z = 0, .w = atlas_w, .h = atlas_h, .d = 1 };
+ const src = c.SDL_GPUTextureTransferInfo{ .transfer_buffer = g.atlas_xfer, .offset = top * atlas_w, .pixels_per_row = atlas_w, .rows_per_layer = rows };
+ const dstregion = c.SDL_GPUTextureRegion{ .texture = g.atlas_tex, .mip_level = 0, .layer = 0, .x = 0, .y = top, .z = 0, .w = atlas_w, .h = rows, .d = 1 };
c.SDL_UploadToGPUTexture(copy, &src, &dstregion, false);
c.SDL_EndGPUCopyPass(copy);
- g.atlas_dirty = false;
+ g.atlas_dirty_top = atlas_h;
+ g.atlas_dirty_bottom = 0;
+}
+
+test "the atlas's dirty rows hold every row a new glyph draws into, and a reset dirties them all" {
+ const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)).?;
+ defer c.ui_font_free(font);
+ var g = try textTestGui(font, 18);
+ defer font_allocator = std.heap.smp_allocator;
+ defer std.testing.allocator.free(g.atlas_stage);
+ defer g.glyphs.deinit();
+ defer g.words.deinit(std.testing.allocator);
+ // The texture as uploadAtlas leaves it, which starts undefined.
+ const texture = try std.testing.allocator.alloc(u8, atlas_w * atlas_h);
+ defer std.testing.allocator.free(texture);
+ @memset(texture, 0xa5);
+ var glyph: u32 = 0;
+ for (0..40) |frame| {
+ if (frame == 30) resetGlyphAtlas(&g); // a font change
+ // Plain glyphs, strips, taglines, grip glyphs and decorations.
+ for (0..25) |_| {
+ glyph += 1;
+ const span: u4 = @intCast(1 + glyph % 3);
+ _ = cachedGlyph(&g, .{
+ .face = 0,
+ .glyph = glyph,
+ .role = if (glyph % 4 == 0) .tagline else .body,
+ .decoration = .{ .bold = glyph % 2 == 0, .underline = if (glyph % 5 == 0) .curly else .off, .strikethrough = glyph % 11 == 0 },
+ .centered = glyph % 13 == 0,
+ .lead = @intCast(glyph % span),
+ .span = span,
+ }) orelse return error.AtlasFull;
+ }
+ for (0..atlas_h) |row| {
+ const stage = g.atlas_stage[row * atlas_w ..][0..atlas_w];
+ const uploaded = texture[row * atlas_w ..][0..atlas_w];
+ if (row >= g.atlas_dirty_top and row < g.atlas_dirty_bottom)
+ @memcpy(uploaded, stage)
+ else
+ try std.testing.expect(std.mem.eql(u8, stage, uploaded));
+ }
+ g.atlas_dirty_top = atlas_h;
+ g.atlas_dirty_bottom = 0;
+ }
+ try std.testing.expect(g.pen_y >= 2 * g.cell_h);
}
fn ensureVbuf(g: *Gui, cells: u32) !void {