perf(font): upload only the dirty sub-rect of the glyph atlas (#51) #83

Merged
catbot merged 2 commits from claude/issue-51 into master 2026-06-16 17:43:23 +02:00
Member

Problem

FontAtlas::Update re-uploaded the entire 1024×1024 R8 atlas (1 MiB) whenever dirty was set — and dirty flipped the moment a single glyph was rasterized. During warm-up / language-switch / size-churn that was roughly one full-atlas copy (full-extent vkCmdCopyBufferToImage + host-write barrier + two whole-image transitions on Vulkan; full writeTexture on WebGPU) per frame that added a codepoint.

Fix

Accumulate a dirty bounding box as glyphs are placed and copy only that sub-rect.

  • FontAtlas::DirtyRect — a half-open [minX,maxX) × [minY,maxY) box grown by MarkDirty in Ensure/Initialize. UploadBox clamps it to the atlas and converts it to an (origin, extent) upload rect. dirty stays the cheap per-frame poll flag, kept in lockstep with the rect.
  • Vulkan — new ImageVulkan::UpdateRegion: the staging buffer keeps the full atlas row stride, so bufferRowLength stays the atlas width and bufferOffset points at the rect's first texel, while imageOffset/imageExtent carve out the rect. Layout transitions stay whole-image (cheap); only the copy extent shrinks. Single-mip only (the atlas carries no mips). R8 has a 1-byte texel block, so the offset-alignment caveat is moot — confirmed validation-clean.
  • WebGPU — the rect is passed straight to wgpuWriteAtlasRegion, which already accepted x/y/w/h.

The freshly-zeroed atlas marks its whole extent dirty in Initialize, so the first Update clears the image once; thereafter every untouched texel stays the zero it was uploaded as, keeping the GPU image byte-identical to the staging copy.

Tests

  • New FontAtlasDirtyRect unit test — drives the accumulation / degenerate-glyph / clamp / dirty-lockstep math directly (no GPU device needed at runtime). crafter-build test → all 4 tests green.
  • HelloUI renders text correctly on Vulkan (NVIDIA RTX 4090, validation layers enabled, zero errors) and WebGPU (Firefox). See below.

Screenshots

Native Vulkan (left) and WebGPU in Firefox (right) — glyphs render crisply from the partially-uploaded atlas, no garbage in untouched regions:

result

Resolves #51

🤖 Generated with Claude Code

## Problem `FontAtlas::Update` re-uploaded the **entire** 1024×1024 R8 atlas (1 MiB) whenever `dirty` was set — and `dirty` flipped the moment a single glyph was rasterized. During warm-up / language-switch / size-churn that was roughly one full-atlas copy (full-extent `vkCmdCopyBufferToImage` + host-write barrier + two whole-image transitions on Vulkan; full `writeTexture` on WebGPU) per frame that added a codepoint. ## Fix Accumulate a dirty bounding box as glyphs are placed and copy only that sub-rect. - **`FontAtlas::DirtyRect`** — a half-open `[minX,maxX) × [minY,maxY)` box grown by `MarkDirty` in `Ensure`/`Initialize`. `UploadBox` clamps it to the atlas and converts it to an `(origin, extent)` upload rect. `dirty` stays the cheap per-frame poll flag, kept in lockstep with the rect. - **Vulkan** — new `ImageVulkan::UpdateRegion`: the staging buffer keeps the full atlas row stride, so `bufferRowLength` stays the atlas width and `bufferOffset` points at the rect's first texel, while `imageOffset`/`imageExtent` carve out the rect. Layout transitions stay whole-image (cheap); only the copy extent shrinks. Single-mip only (the atlas carries no mips). R8 has a 1-byte texel block, so the offset-alignment caveat is moot — confirmed validation-clean. - **WebGPU** — the rect is passed straight to `wgpuWriteAtlasRegion`, which already accepted `x/y/w/h`. The freshly-zeroed atlas marks its **whole** extent dirty in `Initialize`, so the first `Update` clears the image once; thereafter every untouched texel stays the zero it was uploaded as, keeping the GPU image byte-identical to the staging copy. ## Tests - **New `FontAtlasDirtyRect` unit test** — drives the accumulation / degenerate-glyph / clamp / `dirty`-lockstep math directly (no GPU device needed at runtime). `crafter-build test` → all 4 tests green. - **HelloUI** renders text correctly on **Vulkan** (NVIDIA RTX 4090, validation layers enabled, zero errors) and **WebGPU** (Firefox). See below. ## Screenshots Native Vulkan (left) and WebGPU in Firefox (right) — glyphs render crisply from the partially-uploaded atlas, no garbage in untouched regions: ![result](https://forgejo.catcrafts.net/attachments/8c2f2436-4f50-405b-8c32-3484460b4f70) Resolves #51 🤖 Generated with [Claude Code](https://claude.com/claude-code)
FontAtlas::Update re-uploaded the entire 1024x1024 R8 atlas (1 MiB)
whenever a single glyph was rasterized, so warm-up / language-switch /
size-churn cost roughly one full-atlas copy per frame that added a
codepoint.

Accumulate a dirty bounding box (FontAtlas::DirtyRect) in MarkDirty as
glyphs are placed, and copy only that sub-rect. On Vulkan this is a new
ImageVulkan::UpdateRegion: the staging buffer keeps the full atlas row
stride, so bufferRowLength stays the atlas width and bufferOffset points
at the rect origin while imageOffset/imageExtent carve out the rect.
Layout transitions stay whole-image (cheap); only the copy extent
shrinks. WebGPU passes the rect straight to wgpuWriteAtlasRegion, which
already took x/y/w/h.

The freshly-zeroed atlas marks its whole extent dirty in Initialize so
the first Update clears the image once; thereafter every untouched texel
stays the zero it was uploaded as, keeping the image byte-identical to
the staging copy.

Tested: new FontAtlasDirtyRect unit test covers the accumulation /
clamp / lockstep-with-`dirty` math (no GPU needed); HelloUI renders text
correctly on both Vulkan (NVIDIA, validation-clean) and WebGPU.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit 991a39e094 into master 2026-06-16 17:43:23 +02:00
catbot deleted branch claude/issue-51 2026-06-16 17:43:23 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics!83
No description provided.