Skip to content

VRAM budget

What it is

The Graphics Synthesizer holds 4 MiB of VRAM. Framebuffers and textures share it. ps2ui-bake sums every texture and CLUT in the blob and charges the sum against a budget. It prints a per-texture breakdown. It returns 1 without writing the blob when the sum is over.

The budget is a bake-time decision, not a console-time one. The baker knows every texture it emits. An over-budget UI therefore fails at the desk with a table, not on the console with an allocator error.

The default budget is 4 MiB minus three framebuffers at the canvas resolution. Override it per project with vramBudget, or per command with --vram-budget.

Minimal example

Bake the memcard example. The breakdown is trimmed here to one texture row, the CLUT row and the totals.

$ ps2ui-bake examples/memcard/build/library.json examples/memcard/build/saves.json -o memcard.uib
...
  tex[ 0] PSMT8     256x64   baked       16384 B payload ->   16384 B in pages
...
  tex[ 7] PSMT8      11x11   baked         121 B payload ->    8192 B in pages
...
  clut[0] PSMCT32  256 entries        ->    8192 B in pages
  framebuffers assumed: 2x draw/display + 1x Z @ 640x448 = 3440640 B
  payload 132667 B -> allocator 143104 B -> budget-charged 163840 B
  reclaimable 10437 B (7% of committed) -- the rest of the gap to 163840 B is the budget model's pessimism, which nothing allocates and P3c cannot reclaim
  textures 163840 B of 753664 B budget (21%)
ps2ui-bake: 2 screen(s), 1062 records, 11 textures (128 KiB baked), 1 CLUTs -> memcard.uib
ps2ui-bake: arena 1662 bytes (static uint8_t arena[1662] __attribute__((aligned(16))))

The canvas is 640x448. Three framebuffers take 3440640 B and leave 753664 B for textures. The eleven textures plus one CLUT book 163840 B of that.

The arena line is a separate figure and is not part of the budget. It is host RAM, and it is per blob.

The full transcript and its ordering live on ps2ui-bake.

Reference table

Each texture row is tex[i] <format> <W>x<H> <kind> <N> B payload -> <M> B in pages.

column meaning
tex[i] index in the blob's texture table, the order ps2ui_upload walks
<format> PSMT8 or PSMCT32
<W>x<H> texel dimensions after pre-scaling
<kind> baked when texels sit in the blob, streamed when the app supplies them
payload texel bytes: the baked data length, or the streamed slot's reservation
in pages the 8 KiB page-rounded footprint, the figure the budget charges
clut[i] one 256-entry PSMCT32 palette, charged one whole page
framebuffers assumed two draw/display buffers plus one Z buffer at the canvas
payload total every texel byte and palette byte in the blob
allocator what ps2ui_upload's preflight computes and gsKit commits
budget-charged the page-rounded total the bake refuses against
reclaimable allocator minus payload, the only part a packer could win back
textures N B of M B budget the verdict, with the percentage of the budget used

Three ways to set the budget:

flag or key effect
ps2ui-bake --vram-budget BYTES replaces the default for that bake
ps2ui-check --vram-budget BYTES replaces the default for that check
vramBudget in ps2ui.json replaces the default for ps2ui build and ps2ui check alike

The project key is documented with the rest of the file on the project file.

Behaviour

Two cost models

The baker carries two models of what a texture costs, and the breakdown prints both. The page model rounds to 8 KiB pages and is what the budget charges. The allocator model charges 256-byte blocks rounded up to an alignment group, which is what gsKit_texture_size does and therefore what the runtime commits. A full page is the largest group, not the unit.

The page model is deliberately pessimistic. Refusing a blob that would have fitted is the safe direction, because the console has no way to report exhaustion once the upload starts.

The allocator port is checked against the vendored C rather than trusted.

$ python3 tools/check-vram-model.py
ok - alloc_size agrees with gsKit_texture_size on 45000 sizes
ok - and differs from the page model on 5120 of them, which is why both are reported

The gap between the two is why reclaimable exists as its own number. On memcard the allocator commits 143104 B where the budget charges 163840 B. Only the 10437 B above the payload can be reclaimed. The rest is margin the budget holds back.

Which number the runtime wants

payload is the len argument ps2ui_tex_set demands. It must equal the slot's reservation exactly; the page-rounded figure is PS2UI_ERR_SIZE. Bake the streaming bench fixture and read a slot row.

$ ps2ui-layout fixtures/bench-stream/ui/covers.html fixtures/bench-stream/ui/bench.css -o covers.json
ps2ui-layout: 6 paint commands, 0 focusables -> covers.json
$ ps2ui-layout fixtures/bench-stream/ui/dialog.html fixtures/bench-stream/ui/bench.css -o dialog.json
ps2ui-layout: 11 paint commands, 2 focusables -> dialog.json
$ ps2ui-bake covers.json dialog.json -o bench.uib
...
  tex[ 1] PSMCT32   128x128  streamed    65536 B payload ->   65536 B in pages
...
  textures 368640 B of 753664 B budget (48%)
ps2ui-bake: 2 screen(s), 74 records, 9 textures (96 KiB baked + 256 KiB reserved by slots), 1 CLUTs -> bench.uib
ps2ui-bake: arena 1875 bytes (static uint8_t arena[1875] __attribute__((aligned(16))))

Slot 1 is a 128x128 PSMCT32 cover. Pass 65536 as len. The two figures match here because the texture already fills whole pages. The 11x11 icon in the memcard bake above carries 121 B of payload and occupies 8192 B in pages.

A streamed slot costs its reservation from the moment the blob loads. The four covers are unfilled and still reserve 256 KiB. Streaming art has the runtime call and the host-side conversion.

Overriding the budget

New in 0.6.0. The budget reaches the build and the check from one place. Set vramBudget in the project file and both commands charge against it.

$ ps2ui build proj
...
  textures 163840 B of 262144 B budget (62%)
$ ps2ui check proj
...
ok 60 - VRAM 160 KiB within budget 256 KiB

proj is a copy of the memcard sources with "vramBudget": 262144. Without the key the same blob checks against the default: ok 60 - VRAM 160 KiB within budget 736 KiB.

Override the default when the real framebuffer layout is known. The sample in this tree runs with ZBuffering off and holds two buffers, not three. The third reservation is there so the default holds for a host that turns Z on.

When the default cannot exist

New in 0.6.0. Past a canvas width, three framebuffers do not fit in 4 MiB and the default budget goes negative. The bake says so instead of blaming the textures.

$ ps2ui-layout wide.html wide.css --canvas 796x448 -o wide.json
...
ps2ui-layout: 2 paint commands, 0 focusables -> wide.json
$ ps2ui-bake wide.json -o wide.uib
...
  framebuffers assumed: 2x draw/display + 1x Z @ 796x448 = 4472832 B
  the default budget does not exist at this canvas: three framebuffers at 796x448 need 4472832 B of 4194304 B total VRAM, so there is nothing left to charge textures against and an empty blob would fail here
  declare vramBudget (or --vram-budget) for the layout you actually run: with ZBuffering off the console holds two buffers, not three, which leaves 1212416 B
  payload 17408 B -> allocator 17408 B -> budget-charged 24576 B
  ...
  textures 24576 B, and no budget to charge them to
error: texture VRAM footprint exceeds budget (see breakdown above; override with --vram-budget)

796x448 is the canvas that gives square pixels at 16:9 on a 448-line frame. Declaring the two-buffer figure clears it: ps2ui-bake wide.json -o wide.uib --vram-budget 1212416 prints textures 24576 B of 1212416 B budget (2%), then arena 1066 bytes, and exits 0.

Higher still, two framebuffers stop fitting and no budget helps. At 448 lines the two crossovers are 769 and 1153 columns.

$ python3 -c "
from ps2ui_bake import vram
V = vram.VRAM_TOTAL
w1 = next(w for w in range(1,4096) if 3*vram.framebuffer_size(w,448) >= V)
w2 = next(w for w in range(1,4096) if 2*vram.framebuffer_size(w,448) >= V)
print('first width where three framebuffers stop fitting:', w1)
print('first width where two framebuffers stop fitting:', w2)
print()
for l in vram.budget_note(1153,448): print(l)
"
first width where three framebuffers stop fitting: 769
first width where two framebuffers stop fitting: 1153

  two framebuffers at 1153x448 need 4358144 B of 4194304 B total VRAM, so this canvas cannot be displayed from GS VRAM under any Z setting
  no budget can be declared for it; a narrower canvas is the only fix

test_the_advice_stops_when_two_buffers_stop_fitting in packages/baker/tests/test_baker.py pins both regimes and computes the crossover rather than hard-coding it.

The note prints only when the budget was inherited. A caller who passed --vram-budget has already made the decision it argues for. ps2ui-check prints the same two lines as notes and marks the label: not ok 40 - VRAM 24 KiB within budget -272 KiB -- the default budget is unusable at this canvas, see notes. The checker's VRAM check is on ps2ui-check.

On the console

ps2ui_upload preflights before it transfers anything. It sums gsKit_texture_size per texture and adds one 16x16 PSMCT32 block per PSMT8 texture for the palette. It returns -1 when that sum plus the current VRAM pointer exceeds 4 MiB. Nothing is transferred, and ctx->uploaded stays 0.

The refusal is all-or-nothing by design. gsKit_TexManager_bind cannot report exhaustion. Its allocator evicts in a loop that never exits when nothing can ever fit. An over-budget blob is therefore a hang, not an error code. make -C runtime test covers it.

ok 309 - upload reports failure when VRAM is exhausted, so step 9's expected 0 is a result and not a constant
ok 310 - and leaves the context not-uploaded, so a caller cannot render through a half-built texture table
ok 311 - upload refuses a budget that holds some textures but not all of them
ok 312 - and transfers nothing at all (0 transfers) -- all-or-nothing, because bind cannot fail and a partial table cannot render
ok 313 - and still leaves it not-uploaded

ps2ui_render re-tests the same fit every frame against ctx->vram_need, because a host that allocates VRAM after the upload can shrink what is left. When it no longer fits, every textured draw is skipped and stats.vram_lost reads 1. The signatures are on the C API reference. Check the return of ps2ui_upload before rendering; First boot turns that return into step 9.

Limits and errors

situation what happens
sum over budget error: texture VRAM footprint exceeds budget (see breakdown above; override with --vram-budget), exit 1, no blob written
sum over budget at check time not ok - VRAM N KiB within budget M KiB, exit 1
default budget not positive the two diagnostic lines, no percentage, and the bake fails even for an empty blob
canvas at or past 1153x448 no budget can be declared; narrow the canvas
ps2ui_upload does not fit returns -1, transfers nothing, leaves the context not uploaded
VRAM shrinks after upload ps2ui_render skips every textured draw and sets stats.vram_lost

The budget covers textures and CLUTs only. The arena is host RAM and is counted separately. Its figure is per blob, not a constant. The bakes above printed arena 1662 bytes for memcard, arena 1875 bytes for the bench blob and arena 1066 bytes for the 796x448 blob.

The page model charges whole pages, so a texture smaller than a page costs a whole one. The memcard bake charges 8192 B for an 11x11 icon of 121 B. Pack small art into one atlas image instead.

PSMT8 costs a quarter of PSMCT32 per texel, plus one palette per image. Images has the two bakes that measure it.

page why
ps2ui-bake the full transcript the breakdown sits in
ps2ui-check the VRAM check and its label
The project file vramBudget and the keys around it
Images PSMT8 against PSMCT32, measured
Streaming art filling a streamed slot with ps2ui_tex_set
C API reference ps2ui_upload and its return
First boot step 9, where an upload refusal shows up