.uib¶
A .uib file is the blob ps2ui-bake writes and ps2ui_load maps. Every record is little-endian and fixed-size. Every u32 sits at a 4-aligned offset, so the C runtime overlays the structs in runtime/ps2ui.h on the file without parsing or packing pragmas. The Python writer and reader are in uib.py.
Layout¶
The file is a header, eight tables, padding, and a blob. Each table starts where the previous one ends. Every data_off and name_off in a table is relative to the blob, not to the file.
| section | offset field | count field | stride | meaning |
|---|---|---|---|---|
| header | 0 | 84 | magic, counts, offsets, crc, aspect | |
| tex table | off_tex |
n_tex |
20 | one entry per texture |
| clut table | off_clut |
n_clut |
8 | one entry per palette |
| command list | off_cmd |
n_cmd |
32 | draw and scissor commands in replay order |
| focus table | off_focus |
n_focus |
24 | the solved D-pad graph |
| font table | off_font |
n_font |
24 | glyph atlases for dynamic text |
| slot table | off_slot |
n_slot |
28 | dynamic text slots |
| screen table | off_screen |
n_screen |
24 | contiguous ranges of the three tables above |
| tint table | off_tint |
n_theme rows of n_tint |
4 | colours, theme-major |
| padding | 0 to 12 | brings off_blob to a multiple of 16 |
||
| blob | off_blob |
blob_len |
texels, palettes, glyph and kern records, names |
The writer computes each offset in write_uib. Read the header of the memcard example to see the arithmetic land.
python3 - <<'PY'
import struct
d = open("examples/memcard/build/ui.uib", "rb").read()
H = struct.Struct("<IHHHHHHIHHIIIIIIIHHIIHHIIHHHH")
names = ("magic version feature_flags canvas_w canvas_h n_tex n_clut n_cmd "
"n_focus initial_focus off_tex off_clut off_cmd off_focus off_blob "
"blob_len crc32 n_font n_slot off_font off_slot n_screen n_tint "
"off_screen off_tint n_theme pad display_aspect_num "
"display_aspect_den").split()
print("file", len(d), "bytes; header", H.size, "bytes")
for n, v in zip(names, H.unpack_from(d, 0)):
print(f"{n:<19} {v:#x}" if n in ("magic", "feature_flags", "crc32") else f"{n:<19} {v}")
PY
file 176208 bytes; header 84 bytes
magic 0x31424955
version 7
feature_flags 0x3
canvas_w 640
canvas_h 448
n_tex 11
n_clut 1
n_cmd 1062
n_focus 16
initial_focus 0
off_tex 84
off_clut 304
off_cmd 312
off_focus 34296
off_blob 35040
blob_len 141168
crc32 0x4d8c37e4
n_font 2
n_slot 6
off_font 34680
off_slot 34728
n_screen 2
n_tint 23
off_screen 34896
off_tint 34944
n_theme 1
pad 0
display_aspect_num 4
display_aspect_den 3
off_tex is 84 because the header is 84 bytes. off_clut is 84 plus 11 times 20. The tint table ends at 35036 and the blob starts at 35040, after 4 bytes of padding.
Header¶
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | magic | 0x31424955, the bytes UIB1 |
| 4 | 2 | u16 | version | 7 |
| 6 | 2 | u16 | feature_flags | bits from the feature table under Invariants; an unknown bit is refused |
| 8 | 2 | u16 | canvas_w | framebuffer width in pixels |
| 10 | 2 | u16 | canvas_h | framebuffer height in pixels |
| 12 | 2 | u16 | n_tex | texture count |
| 14 | 2 | u16 | n_clut | palette count |
| 16 | 4 | u32 | n_cmd | command count |
| 20 | 2 | u16 | n_focus | focus node count |
| 22 | 2 | u16 | initial_focus | screen 0's initial focus index, 0xFFFF for none |
| 24 | 4 | u32 | off_tex | file offset of the tex table |
| 28 | 4 | u32 | off_clut | file offset of the clut table |
| 32 | 4 | u32 | off_cmd | file offset of the command list |
| 36 | 4 | u32 | off_focus | file offset of the focus table |
| 40 | 4 | u32 | off_blob | file offset of the blob, a multiple of 16 |
| 44 | 4 | u32 | blob_len | blob length in bytes |
| 48 | 4 | u32 | crc32 | CRC-32 of the file with these 4 bytes read as zero |
| 52 | 2 | u16 | n_font | font count |
| 54 | 2 | u16 | n_slot | slot count |
| 56 | 4 | u32 | off_font | file offset of the font table |
| 60 | 4 | u32 | off_slot | file offset of the slot table |
| 64 | 2 | u16 | n_screen | screen count, at least 1 |
| 66 | 2 | u16 | n_tint | colours per theme row |
| 68 | 4 | u32 | off_screen | file offset of the screen table |
| 72 | 4 | u32 | off_tint | file offset of the tint table |
| 76 | 2 | u16 | n_theme | theme rows, at least 1 |
| 78 | 2 | u16 | pad | zero |
| 80 | 2 | u16 | display_aspect_num | panel aspect numerator |
| 82 | 2 | u16 | display_aspect_den | panel aspect denominator |
The runtime derives the pixel aspect from the last two fields and the canvas size, see ps2ui_pixel_aspect_x1000. The C struct for every record on this page is listed on C API reference.
Records¶
Texture entry¶
Stride 20. A baked entry points at texels in the blob. A streamed entry carries no texels; ps2ui_tex_set supplies them at runtime.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | format | 0 PSMT8 (indexed), 1 PSMCT32 |
| 1 | 1 | u8 | kind | 0 baked, 1 streamed |
| 2 | 2 | u16 | width | texels |
| 4 | 2 | u16 | height | texels |
| 6 | 2 | u16 | clut | clut index, 0xFFFF for none; required when format is PSMT8 |
| 8 | 4 | u32 | data_off | blob offset of the texels, a multiple of 16; unused when streamed |
| 12 | 4 | u32 | data_len | texel byte count; the reservation ps2ui_tex_set demands when streamed |
| 16 | 4 | u32 | name_off | blob offset of a NUL-terminated name, 0xFFFFFFFF for none; required when streamed |
PSMCT32 texels are r g b a bytes with alpha in the GS 0 to 128 domain. PSMT8 texels are palette indices.
CLUT entry¶
Stride 8. Colours are stored in palette order. The CSM1 index swap is applied by the uploader, never by the file.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 2 | u16 | ncolors | entries in the palette |
| 2 | 2 | u16 | pad | zero |
| 4 | 4 | u32 | data_off | blob offset of ncolors PSMCT32 colours |
Command¶
Stride 32. Commands replay in file order. A command draws when state is 0, when state is 2 and focus is the focused node, or when state is 1 and it is not.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | op | 0 QUAD, 1 TEXQUAD, 2 SCISSOR_PUSH, 3 SCISSOR_POP |
| 1 | 1 | u8 | state | 0 always, 1 unfocused, 2 focused |
| 2 | 2 | u16 | focus | focus index the state refers to, 0xFFFF for none |
| 4 | 2 | i16 | x | left edge in pixels |
| 6 | 2 | i16 | y | top edge in pixels |
| 8 | 2 | u16 | w | width in pixels |
| 10 | 2 | u16 | h | height in pixels |
| 12 | 2 | u16 | tint | index into the live theme row |
| 14 | 2 | u16 | tint_focus | index used while focus is the focused node; the baker writes the same value as tint |
| 16 | 2 | u16 | tex | texture index for TEXQUAD, else 0xFFFF |
| 18 | 8 | u16 x4 | u0 v0 u1 v1 | texel source rect, u1 and v1 exclusive |
| 26 | 6 | u8 x6 | pad | zero |
Scissor commands carry no colour. The writer puts 0 in both tint fields of a scissor command and readers do not range-check them, see write_uib. A QUAD's colour is full-range RGB. A TEXQUAD's colour is in the modulate domain, where 0x80 is identity.
Focus node¶
Stride 24. The graph is solved at build time, so a D-pad press is one lookup.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 2 | u16 | id | equals the node's table index |
| 2 | 2 | u16 | up | neighbour index, 0xFFFF for none |
| 4 | 2 | u16 | down | neighbour index, 0xFFFF for none |
| 6 | 2 | u16 | left | neighbour index, 0xFFFF for none |
| 8 | 2 | u16 | right | neighbour index, 0xFFFF for none |
| 10 | 2 | u16 | pad | zero |
| 12 | 4 | u32 | name_off | blob offset of the NUL-terminated UTF-8 name |
| 16 | 2 | i16 | x | rect left, for debugging and the HUD |
| 18 | 2 | i16 | y | rect top |
| 20 | 2 | u16 | w | rect width |
| 22 | 2 | u16 | h | rect height |
Font entry¶
Stride 24. Present only with feature bit 0. The atlas must be a baked PSMT8 texture.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 2 | u16 | tex | atlas texture index |
| 2 | 2 | u16 | size | pixel size |
| 4 | 2 | u16 | weight | 400 or 700 |
| 6 | 2 | u16 | ascent | pixels, from the metrics JSON |
| 8 | 2 | u16 | line_height | pixels |
| 10 | 2 | u16 | glyph_count | glyph records at glyphs_off |
| 12 | 4 | u32 | glyphs_off | blob offset of the glyph records, sorted by codepoint |
| 16 | 2 | u16 | kern_count | kern records at kerns_off, 0 unless feature bit 1 |
| 18 | 2 | u16 | pad | zero |
| 20 | 4 | u32 | kerns_off | blob offset of the kern records, sorted by pair |
Glyph record¶
Stride 20, in the blob. Read as glyphs_off + j * 20.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | codepoint | Unicode scalar |
| 4 | 2 | u16 | u | atlas left |
| 6 | 2 | u16 | v | atlas top |
| 8 | 2 | u16 | w | atlas width |
| 10 | 2 | u16 | h | atlas height |
| 12 | 2 | i16 | bearing_x | from the pen x |
| 14 | 2 | i16 | bearing_y | from the line-box top |
| 16 | 2 | u16 | advance | pixels |
| 18 | 2 | u16 | pad | zero |
Kern record¶
Stride 12, in the blob. Pairs are ordered and pairs that round to zero are not stored.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | prev | first codepoint of the pair |
| 4 | 4 | u32 | cur | second codepoint of the pair |
| 8 | 2 | i16 | amount | pixels at this font's size, usually negative |
| 10 | 2 | u16 | pad | zero |
Slot entry¶
Stride 28. Present only with feature bit 0. The runtime copies app text into a per-slot buffer of capacity bytes and composes glyph quads each frame.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | name_off | blob offset of the NUL-terminated name |
| 4 | 4 | u32 | placeholder_off | blob offset of the text drawn until the app sets one |
| 8 | 2 | i16 | x | content left in pixels |
| 10 | 2 | i16 | text_y | glyph-box top in pixels |
| 12 | 2 | u16 | w | content width in pixels |
| 14 | 2 | u16 | font | font table index |
| 16 | 1 | u8 | align | 0 left, 1 center, 2 right |
| 17 | 1 | u8 | flags | bit 0 ellipsize overflow |
| 18 | 2 | u16 | capacity | maximum runtime bytes |
| 20 | 2 | u16 | focus | focus index or 0xFFFF |
| 22 | 2 | u16 | tint_base | index into the live theme row |
| 24 | 2 | u16 | tint_focus | index used while the slot's node is focused |
| 26 | 2 | i16 | letter_spacing | pixels per glyph junction; non-zero requires feature bit 2 |
Screen entry¶
Stride 24. Screens partition the command, focus and slot tables into contiguous ranges. Textures, palettes and fonts are shared.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | name_off | blob offset of the NUL-terminated name |
| 4 | 4 | u32 | cmd_first | first command index |
| 8 | 4 | u32 | cmd_count | commands in the screen |
| 12 | 2 | u16 | focus_first | first focus index |
| 14 | 2 | u16 | focus_count | focus nodes in the screen |
| 16 | 2 | u16 | slot_first | first slot index |
| 18 | 2 | u16 | slot_count | slots in the screen |
| 20 | 2 | u16 | initial_focus | global focus index or 0xFFFF |
| 22 | 2 | u8 x2 | pad | zero |
Tint entry¶
Stride 4. The table is theme-major: row t starts at off_tint + t * n_tint * 4. Every painting command and every slot indexes a row, so a themeless blob still has one row. How CSS becomes rows is on Theming.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | r | red |
| 1 | 1 | u8 | g | green |
| 2 | 1 | u8 | b | blue |
| 3 | 1 | u8 | a | alpha in the GS 0 to 128 domain |
Invariants¶
The writer establishes each property. ps2ui_load refuses a file that breaks one, with the codes listed on Errors and constants. ps2ui-check asserts the same properties offline, see ps2ui-check.
| invariant | writer | runtime | ps2ui-check |
|---|---|---|---|
off_blob is a multiple of 16 |
blob_pad in uib.py |
blob address with low bits set is PS2UI_ERR_ALIGN, ps2ui.c |
check 12 |
every baked data_off is a multiple of 16 |
_align16 after every texture |
PS2UI_ERR_ALIGN, ps2ui.c |
check 13 |
crc32 is the zlib CRC-32 of the file with bytes 48 to 51 zeroed |
zlib.crc32 patched in at offset 48 |
PS2UI_ERR_CRC from crc_file_with_hole, ps2ui.c |
reader raises before checks run |
| every table ends inside the file | offsets computed from counts and strides | PS2UI_ERR_TRUNCATED |
reader raises |
| every table starts at a multiple of 4 | an 84-byte header and entries whose sizes are multiples of 4 | PS2UI_ERR_ALIGN, tables_aligned |
reader raises |
| a font's glyph and kern tables start at a multiple of 4 in the blob | _align16 before each |
PS2UI_ERR_ALIGN |
reader raises |
n_screen is at least 1 |
default screen main when none is given |
PS2UI_ERR_BOUNDS, ps2ui.c |
check 5 |
n_theme is at least 1 |
n_theme defaults to 1 |
PS2UI_ERR_BOUNDS, ps2ui.c |
check 41 |
n_theme above 1 requires feature bit 4 |
bit set from n_theme |
PS2UI_ERR_TINTS, ps2ui.c |
check 44 |
a streamed texture requires bit 3, a name and a non-zero data_len |
bit set from the texture table | PS2UI_ERR_FEATURES or PS2UI_ERR_BOUNDS, ps2ui.c |
checks 6 to 9 |
4 * n_tint is below the painting command count |
interning in _tint |
not checked | check 43, once at least 100 commands paint |
The runtime checks the blob's address in memory, not its file offset. A file placed 16-aligned in memory has an aligned blob only because off_blob is a multiple of 16, so the two checks are one property. Run the arithmetic on the memcard blob.
python3 - <<'PY'
import struct, zlib
d = open("examples/memcard/build/ui.uib", "rb").read()
h = struct.unpack_from("<IHHHHHHIHHIIIIIIIHHIIHHIIHHHH", d, 0)
n_tex, n_cmd, off_tex, off_cmd, off_blob, blob_len, crc = h[5], h[7], h[10], h[12], h[14], h[15], h[16]
n_tint, off_tint, n_theme = h[22], h[24], h[25]
print("off_blob % 16 =", off_blob % 16)
print("baked data_off % 16 =", [struct.unpack_from("<BBHHHIII", d, off_tex + 20 * i)[5] % 16 for i in range(n_tex)])
zeroed = bytearray(d); struct.pack_into("<I", zeroed, 48, 0)
print("crc32 in header", hex(crc), "recomputed", hex(zlib.crc32(bytes(zeroed)) & 0xFFFFFFFF))
end = off_tint + n_theme * n_tint * 4
print("tables end", end, "pad", off_blob - end, "off_blob + blob_len", off_blob + blob_len, "file", len(d))
paint = sum(1 for i in range(n_cmd) if d[off_cmd + 32 * i] in (0, 1))
print("painting commands", paint, "n_tint", n_tint, "4 * n_tint < painting:", 4 * n_tint < paint)
PY
off_blob % 16 = 0
baked data_off % 16 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
crc32 in header 0x4d8c37e4 recomputed 0x4d8c37e4
tables end 35036 pad 4 off_blob + blob_len 176208 file 176208
painting commands 1030 n_tint 23 4 * n_tint < painting: True
Feature bits¶
A reader refuses any bit outside the known set. The known set is PS2UI_FEAT_KNOWN in ps2ui.h and FEAT_KNOWN in uib.py, both 0x1f. The writer sets each bit from the tables it wrote, in write_uib.
| bit | name | set when | gates |
|---|---|---|---|
| 0 | PS2UI_FEAT_DYNAMIC_TEXT |
any font or slot exists | the font and slot tables |
| 1 | PS2UI_FEAT_KERNING |
any font has kern pairs | the kern lookup in the pen |
| 2 | PS2UI_FEAT_SLOT_SPACING |
any slot has non-zero letter_spacing |
reading letter_spacing |
| 3 | PS2UI_FEAT_STREAMED_TEX |
any texture has kind 1 | loading a streamed entry |
| 4 | PS2UI_FEAT_ROLE_TINTS |
n_theme is above 1 |
loading more than one theme row |
The memcard blob above carries 0x3, bits 0 and 1. Its two fonts have 291 kern pairs between them, which ps2ui-check reports as check 57.
Versioning¶
version is 7. Readers refuse any other value: PS2UI_ERR_VERSION in the runtime, ValueError in the Python reader. PS2UI_VERSION in ps2ui.h is that format number, not a package version. check-versions.py holds it equal to uib.VERSION, and ps2ui vendor-runtime writes ps2ui.c and ps2ui.h from the same package that bakes the blob. What the pledge means for an installed app is on Compatibility.
The pledge¶
v7 is the last incompatible layout. An addition from here goes behind a new feature bit and never moves a stride. A reader that lacks the bit refuses the blob by name instead of misreading it. check-format-frozen.py records the v7 layout from the live Struct objects and runs in CI.
| frozen | not frozen |
|---|---|
| the format string and size of all eleven structs | the set of feature bits |
MAGIC |
new tables behind a new bit, recorded in the same change |
VERSION |
|
| the value of every assigned feature bit |
The check also fails when uib defines a Struct that the record does not hold. A field swap between two fields of the same type is invisible to it; the pen agreement tests in packages/baker/tests cover that case. Breaking the pledge is a deliberate change: bump both version numbers, update the record, and write the entry in the history. The check fails until the record matches.
ok - the format is v7, the version the pledge froze
ok - MAGIC is 0x31424955, unchanged
ok - _CLUT is '<HHI', 8 bytes
ok - _CMD is '<BBHhhHHHHHHHHH6x', 32 bytes
ok - _FOCUS is '<HHHHHHIhhHH', 24 bytes
ok - _FONT is '<HHHHHHIH2xI', 24 bytes
ok - _GLYF is '<IHHHHhhH2x', 20 bytes
ok - _HEADER is '<IHHHHHHIHHIIIIIIIHHIIHHIIHHHH', 84 bytes
ok - _KERN is '<IIh2x', 12 bytes
ok - _SCREEN is '<IIIHHHHH2x', 24 bytes
ok - _SLOT is '<IIhhHHBBHHHHh', 28 bytes
ok - _TEX is '<BBHHHIII', 20 bytes
ok - _TINT is '<BBBB', 4 bytes
ok - FEAT_DYNAMIC_TEXT is still bit 0
ok - FEAT_KERNING is still bit 1
ok - FEAT_SLOT_SPACING is still bit 2
ok - FEAT_STREAMED_TEX is still bit 3
ok - FEAT_ROLE_TINTS is still bit 4
ok - FEAT_KNOWN (0x1f) still admits every frozen bit
ok - all 11 Struct(s) in uib are in the record
The C side is held separately. The runtime test struct layout matches the on-disk format in test_runtime.c asserts every struct size in ps2ui.h against the strides above.
History¶
Every struct-size change bumped the version. The entries for v5 to v7 are in docs/format-uib.md; v1 to v4 are recorded in docs/PLAN.md, and check-versions.py holds that line to run v1 through the current version.
| version | change | stride moved | feature bit |
|---|---|---|---|
| v1 | initial layout | ||
| v2 | CRC-32, feature bits, dynamic-text tables | font and slot tables added | 0 |
| v3 | multi-screen and images | screen table added | |
| v4 | display aspect | header to 76 bytes | |
| v5 | kerning | font entry 16 to 24 | 1 |
| v6 | texture kinds and streamed textures | texture entry 16 to 20 | 3 |
| v7 | the tint table; colour bytes become u16 indices | slot entry 32 to 28, header to 84 | 4 |