Skip to content

Quick start

What you get

Eight commands turn one TTF into a baked blob and a browser preview, run from an empty directory. Install the packages first; every command below assumes ps2ui and ps2ui-fontgen are already on PATH.

1. Fonts from a TTF

mkdir -p browser/ui && cd browser
ps2ui fontgen "$TTF_REGULAR" "$TTF_BOLD"
ps2ui-fontgen: 115 glyphs, 284 kern pairs -> fonts/default.metrics.json
ps2ui-fontgen: 115 glyphs, 163 kern pairs -> fonts/default-bold.metrics.json
ps2ui-fontgen: manifest -> fonts/fonts.json

This rasterizes both faces. It writes fonts/fonts.json, the manifest every later command reads.

2. The screen

cat > ui/library.html <<'EOF'
<screen name="library">
  <div class="page">
    <div class="header">
      <span class="title">Game Library</span>
      <span class="count" data-slot="count" data-slot-capacity="16">0 titles</span>
    </div>
    <div class="row" data-repeat="6" id="row-{i}" focusable>
      <span class="name" data-slot="name-{i}" data-slot-capacity="40">--</span>
      <span class="size" data-slot="size-{i}" data-slot-capacity="12">--</span>
    </div>
  </div>
</screen>
EOF

data-repeat="6" stamps six rows at compile time. Each data-slot reserves runtime text, and focusable puts a row in the D-pad graph.

3. The style

cat > ui/library.css <<'EOF'
:root {
  --bg: #10141f; --panel: #1a2030;
  --text: #e8ecf4; --dim: #a8b2c4; --accent: #2f6fd0;
}
.page   { display: flex; flex-direction: column; padding: 32px 40px; background: var(--bg); }
.header { display: flex; flex-direction: row; padding-bottom: 12px; }
.title  { font-size: 20px; font-weight: 700; color: var(--text); flex-grow: 1; }
.count  { font-size: 14px; color: var(--dim); }
.row    { display: flex; flex-direction: row; padding: 7px 10px; background: var(--panel); margin-bottom: 3px; }
.row:focus { background: var(--accent); }
.name   { font-size: 14px; color: var(--text); flex-grow: 1; }
.size   { font-size: 14px; color: var(--dim); }
.row:focus .size { color: var(--text); }
EOF

flex-direction is required on any container with two or more children. Every var(--x) here becomes a row in the blob's tint table, not a fixed color.

4. The project file

cat > ps2ui.json <<'EOF'
{
  "screens": ["ui/library.html"],
  "css": "ui/library.css",
  "strict": true,
  "montage": "build/states.png"
}
EOF

The project file takes screens and css only; every other key defaults. strict and montage are already optional here.

5. Build

ps2ui build
ps2ui-layout: 14 paint commands, 6 focusables -> build/library.json
ps2ui-bake: 1 screen(s), 24 records, 2 textures (32 KiB baked), 1 CLUTs -> build/ui.uib
ps2ui-bake: arena 1516 bytes (static uint8_t arena[1516] __attribute__((aligned(16))))
ps2ui-bake: preview -> build/preview.png
ps2ui-bake: montage -> build/states.png

Two stages ran. The compiler solved layout and focus, then the baker wrote build/ui.uib and rendered build/preview.png from it. The arena line is the number to paste into a C program. This blob needs 1516 bytes; the runtime allocates nothing on its own.

the quick start library screen, root theme, 4:3, initial focus on row 0, six placeholder rows and a 0-title count

6. Check it

ps2ui check
# arena: 1516 bytes on the EE (1532 on a 64-bit host; GSTEXTURE holds pointers, so the two differ)
# build/ui.uib: 640x448 at 4:3, 1 screen(s), 24 commands, 2 textures, 13 slots
PASS: 51 checks, 0 error(s), 0 warning(s)

ps2ui check validates the blob against every assumption the C runtime makes: table bounds, texture residency, scissor depth, VRAM budget. It prints two arena figures because a GSTEXTURE pointer is 4 bytes on the EE and 8 bytes on a 64-bit host. ps2ui build only prints the EE figure.

7. Confirm the preview matches the server

ps2ui serve --selftest
ok - an unknown route is 404
ok - the framebuffer frame is byte-identical to --preview
PASS: 6 route(s)

This builds the project, binds an ephemeral port and fetches every route once. It asserts the served frame equals what --preview wrote. If those ever differ, everything judged in a browser is judged against a picture the console will not draw.

Two sizes, and they are both right. build/preview.png is the framebuffer, 640x448. The previewer shows 597x448 by default, because it applies the pixel aspect a 4:3 television applies to that framebuffer, and the self-test compares the framebuffer frame rather than the one on screen -- which is what its line says. Diffing the browser against build/preview.png therefore gives two different pictures and neither is broken. Video modes is where the stretch comes from, and the previewer's Aspect menu switches between them.

8. Serve it

ps2ui serve --port 8600
ps2ui-layout: 14 paint commands, 6 focusables -> build/serve/library.json
ps2ui serve: http://127.0.0.1:8600/ -- ctrl-c to stop

With no --port this tries 8080 first and walks up to the next free port. Open the URL and the previewer shows the frame, the command list, the focusables and the slot boxes. The server writes to build/serve/ rather than build/, so a ps2ui build in another terminal cannot collide with it.

the ps2ui serve page for the quick start project: the library screen with row 0 focused, the command list, the focusables list and the slots panel, no warnings

Next

  • Tutorial: a game browser builds this same screen out further, with a second screen and real list data.
  • ps2ui documents every subcommand and flag used above.
  • The project file lists every key ps2ui.json accepts, not only the four used here.