diff options
| -rw-r--r-- | src/gui/gui.zig | 72 |
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 { |
