render3d/overlay: the font atlas kept as its coverage, one byte a texel

The atlas is white glyphs on alpha and the overlay reads only the coverage,
but it was uploaded RGBA8: Maroon Lake's 3072 x 2688 atlas was 43 MB of Metal
memory with its mips, the largest single thing in the launcher window.
overlay_font now keeps the alpha alone as R8 (ov_font_tex; any other PNG
shape is uploaded as before) and overlay.frag reads .r - the white fallback
texture reads 1 there as it did from .a.

Launcher window, graphics regions: the atlas 43.0 -> 11.5 MB; process
footprint at 25 s 402 -> 378 MB. The headless launcher shot is identical to
the unchanged build's (0 pixels differ; two unchanged runs differ from each
other by 1.69%, and the new one by the same against them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 14:45:02 +03:00
parent 2bb7ae1883
commit db7c21e5d8
4 changed files with 26 additions and 3 deletions

View file

@ -67,6 +67,29 @@ function overlay_init(render3d_st: mut Render3dState, font_dir: string) -> bool
return true
}
# The atlas is white glyphs on alpha, and the overlay reads only the coverage: it is kept as one
# byte a texel (R8). As RGBA a 3072 x 2688 atlas was 44 MB with its mips, the most of any UI texture.
@alloc_ok("loading a font: a load, not a frame")
function ov_font_tex(render3d_st: mut Render3dState, path: string) -> int {
let px = png_decode(render3d_st, path)
if px == null { return 0 }
if render3d_st.tex_channels != 4 or render3d_st.tex_depth != 8 {
let id0 = tex_upload(render3d_st, px, false, true)
free(px)
tex_note_size(render3d_st, id0)
return id0
}
let n = render3d_st.tex_w * render3d_st.tex_h
let a = bytes(n)
for i in 0 .. n { a[i] = px[i * 4 + 3] }
free(px)
render3d_st.tex_channels = 1
let id = tex_upload(render3d_st, a, false, true)
free(a)
tex_note_size(render3d_st, id)
return id
}
# Load (or swap to) the font atlas in `font_dir`: font.png and font.json. A game calls it
# again to change fonts at run time - a language whose script the default atlas does not
# carry brings its own. Returns false, and keeps the font it had, if there is none there.
@ -74,7 +97,7 @@ function overlay_init(render3d_st: mut Render3dState, font_dir: string) -> bool
function overlay_font(render3d_st: mut Render3dState, font_dir: string) -> bool {
let meta = Fs.read_text(font_dir + "/font.json")
if meta == null { return false }
let tex = tex_load_ex(render3d_st, font_dir + "/font.png", false, 0)
let tex = ov_font_tex(render3d_st, font_dir + "/font.png")
if tex == 0 { return false }
let j = Json.parse(meta)
render3d_st.ov_font_cols = jint(j, "cols", 16); render3d_st.ov_font_rows = jint(j, "rows", 6)

View file

@ -1,4 +1,4 @@
// 2D overlay: an image texture, the font atlas (white glyphs on alpha) or a flat colour, times a
// 2D overlay: an image texture, the font atlas (glyph coverage, R8) or a flat colour, times a
// colour. Which one is the vertex's own: v carries 4 x the mode (0 image, 1 font, 2 flat) on top of
// its real coordinate, so text, panels and an image's quads share a draw (overlay.ludic, ov_use_tex).
in vec2 v_uv;
@ -27,7 +27,7 @@ void main() {
if (mode > 1.5) {
o_color = v_col;
} else if (mode > 0.5) {
o_color = vec4(v_col.rgb, v_col.a * texture(u_font, uv).a);
o_color = vec4(v_col.rgb, v_col.a * texture(u_font, uv).r); // the atlas is its coverage alone (R8)
} else {
vec4 t = texture(u_tex, uv);
o_color = vec4(v_col.rgb * t.rgb, v_col.a * t.a);