HUD/console: cache charset glyphs as blit-ready sprites - #30
Open
tannevaled wants to merge 1 commit into
Open
Conversation
…verlay
The 2D overlay layer (console, menu, HUD text, centerprint) funnels
every glyph through DrawCharacter, which per call re-derives the source
origin in the 128x128 conchars sheet, walks the sheet with a strided
read, and performs a PER-PIXEL integer divide ((off+u)/scale) to project
virtual pixels onto the HUDScale-upscaled framebuffer. That work is
identical every frame yet redone tens of thousands of times per second
(a full 80x25 console screen is ~2000 glyphs).
Add GlyphCache: a retained-mode fast path that pre-expands each of the
256 conchars glyphs ONCE, at the active scale, into a contiguous
ready-to-blit tile. The per-frame draw then collapses to a
contiguous-row copy with the same transparent-index skip -- no row/col
math, no strided sheet reads, no per-pixel divide, no per-call shape
validation. Output is BYTE-IDENTICAL to DrawCharacter.
Additive only: no existing file is touched. New API:
- GlyphCache + NewGlyphCache(chars, scale) + EnsureFor (invalidation
on charset/HUDScale change; a palette swap does NOT invalidate --
the tiles hold palette-INDEXED bytes, the RGB palette is applied
downstream by FrameBuffer.Expand).
- GlyphCache.DrawCharacter / DrawString / DrawColorString /
DrawCenteredString (drop-in cached counterparts).
- Screen.DrawConsoleCached / DrawNotifyCached (cached console/notify).
Identical-output proof: TestGlyphCacheParityExhaustive diffs the cached
blit against the free DrawCharacter byte-for-byte across scales {1,2,3},
all 256 glyphs, and 7 clip positions (on-screen, partial top-left,
bottom-right overrun, fully off-screen). Console/notify/string paths
each diffed against their free counterparts too.
Benchmarks (Apple silicon, go1.26.4, CGO_ENABLED=0, median of 4):
full 80x25 console screen scale 1 81.0us -> 66.7us (1.21x)
scale 2 291us -> 245us (1.19x)
scale 3 623us -> 517us (1.20x)
HUD string (31 glyphs) scale 1 1048ns -> 845ns (1.24x)
scale 2 3585ns -> 2864ns (1.25x)
Zero per-frame allocations. One-time build ~39us / 72KB at scale 2,
amortised over thousands of frames per video-mode change.
render package stays at 100% statement coverage; race-clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The 2D overlay layer (console, menu, HUD text, centerprint) funnels every glyph through
render.DrawCharacter, which per call re-derives the source origin in the 128×128 conchars sheet, walks the sheet with a strided (128-wide) read, and performs a per-pixel integer divide ((off+u)/scale) to project virtual pixels onto the HUDScale-upscaled framebuffer. That work is identical on every frame yet redone tens of thousands of times per second (a full 80×25 console screen is ~2000 glyphs × 64+ pixels).This adds
GlyphCache, a retained-mode fast path (the same idea proven in the go-news-reader UI): pre-expand each of the 256 conchars glyphs once, at the active scale, into a contiguous ready-to-blit tile. The per-frame draw then collapses to a contiguous-row copy with the same transparent-index skip — no row/col math, no strided sheet reads, no per-pixel divide, no per-call shape validation.Technique
FrameBuffer.Expand, after the 2D layer is composited, so a palette swap (gun-flash / underwater tint) does not invalidate the cache. Only a charset reload (video-mode change) or a HUDScale change rebuilds —EnsureForis the invalidation hook.GlyphCache,NewGlyphCache(chars, scale),EnsureFor(chars, scale) (rebuilt, err)GlyphCache.DrawCharacter / DrawString / DrawColorString / DrawCenteredString(drop-in cached counterparts of the free funcs)Screen.DrawConsoleCached / DrawNotifyCached(cached console + notify draw)Identical-output proof
TestGlyphCacheParityExhaustivediffs the cached blit against the freeDrawCharacterbyte-for-byte across scales {1,2,3} × all 256 glyphs × 7 clip positions (on-screen, top-left corner, partial top-left clip, left-only clip, bottom-right overrun, and two fully-off-screen cases). The console, notify, and string paths are each diffed against their free counterparts on identical framebuffers too. All pass → cached output is byte-identical to the existing renderer.Benchmarks
Apple-silicon, go1.26.4,
CGO_ENABLED=0, median of 4 (-benchmem):Zero per-frame allocations. One-time cache build ≈ 39 µs / 72 KB at scale 2 (3 allocs), amortised over thousands of frames per video-mode change.
Tests
renderpackage stays at 100.0% statement coverage (new files included).go test -race ./render/).go vetclean,gofmtclean.GPL-2.0-or-later (engine subtree), matching the files it sits beside.
🤖 Generated with Claude Code