P8X Graphics Engine — Programmer's Guide¶
How to draw things, from every seat in the house. The engine has four doors, cheapest first:
| From | You write | Best for |
|---|---|---|
| the shell | gl DRAW3 90,-90,300 or gl FILE.GL |
exploring, scene files, demos |
| BASIC | native verbs: MDROTY 20 : POLY3 3,... |
programs, animation, teaching |
| C | //#use gfx / //#use g3d / GL bytes |
commands, tools |
| assembly | ;#use gfx equates + the same registers |
when it must be small |
All four reach the same silicon; the same scene renders identically
from any of them, and identically in the emulator and on the board.
Hardware internals: p8x-graphics-theory.md.
Reference cards on-target: man gl, man gfx, man g3d, man basic.
1. First light (any door)¶
Probe first — an absent display floats the bus:
BASIC: IF PIXELR(0,0) ... only after the OS booted with a display
C: if (!gpresent()) { puts("?No display"); return 1; }
shell: gl (prints ?No display / ?No GL engine itself)
Then, at the shell:
gl VWPORT 0 479 0 271 WINDOW 0 479 0 271
gl COLOR 0 63 0 PRMFIL 1
gl MOVE 100,100 RECT 380,171
Everything after gl is one ASCII GL line (wrapped CA ... CX for
you). Numbers separate on spaces or commas; man gl is the verb card.
2. Coordinate spaces — the one thing to internalize¶
- Screen space: 480x272, origin top-left, y DOWN. The C gfx
library and the shell
imagecommand keep this historical contract as an API convention — underneath they emit GL and map y through the identity flip (271−y); nothing speaks the old $FF20 device door any more. BASIC doesn't use screen space at all: since 2026-08-30/31 EVERY BASIC graphics statement takes window coordinates (y up). The drawing statements emit GL; PIXELR() is the GLPIXRDverb (2026-08-31) and maps through the CURRENT window; text is PGCTEXTwith the boot-loaded font (GTEXT is pure-GL 2D sugar over it); IMAGE is the GLBLITverb (2026-09-01, one per row, bottom-left anchor mapped through the current window). - Window space: the GL 2D world, y UP.
WINDOW x1 x2 y1 y2(PGC order: both x's first!) declares the world extent;VWPORT x1 x2 y1 y2maps it to screen pixels, flipping y. GL 2D primitives (MOVE/DRAW/POLY/RECT) are clipped to the window — off-window geometry is CUT, not discarded. - Model space: 3D verbs (MOVE3/DRAW3/POLY3) pass through the modeling matrix, the viewing matrix, projection, and near/far clipping before landing in window space.
A faithful PGC stream may set WINDOW and assume the PGC's power-on
full-screen viewport; the P8X powers up with a degenerate viewport, so
lead scene files with VWPORT 0 479 0 271 (BACKLOG has the note).
3. Drawing 2D¶
gl COLOR 31 0 0 red pen (r 0-31, g 0-63, b 0-31)
gl MOVE 0,0 DRAW 239,135 line in window space
gl PRMFIL 1 POLY 4 10 10 100 10 100 80 10 80 filled quad
gl RECT 200,100 rect from current point
gl FLOOD 0 0 0 erase the viewport
BASIC's statements ARE this language now (migrated 2026-08-30/31):
LINE 0,0,479,271 is MOVE+DRAW in window space, corner to corner
under the default window and re-mapped by WINDOW/VWPORT like any
GL drawing. The C gfx library remains the raw screen-space path.
4. Drawing 3D¶
gl RESETF
gl VWPORT 104 375 0 271 WINDOW -120 120 -120 120
gl COLOR 0 63 0 PRMFIL 1
gl MDY 30 compose: rotate model 30 deg
gl POLY3 3 -80 -80 300 80 -80 300 0 40 420
Rules of thumb:
- Matrices compose.
MDY 20twice is 40 degrees.MDIDENresets the modeling matrix;VWIDENthe camera;RESETFeverything. - The camera:
VWRPT x y zpicks the point the viewer orbits,VWX/VWY/VWZ degorbit it,DISTAN dbacks the viewer off,PROJCT anglesets the lens (0 = orthographic; power-up = the native focal-256 camera, which is also whatDISTAN 0means). - Fills are fans:
PRMFIL 1+POLY3 n ...fan-fills convex polygons through the triangle engine. Outlines clip per edge. - Depth: near clipping is always on (z >= 16 in eye space); yon
arrives with
DISTY d CLIPY 1.
5. Scenes that persist: command lists¶
Immediate commands draw and are gone. A LIST is a recorded byte stream on the card (64 slots x 4KB):
gl CLBEG 2 CLEARS 0 0 0 POLY3 3 ... FLIP CLEND
gl CLRUN 2 one full frame
gl MDIDEN MDY 40 CLRUN 2 redraw at a new angle
Record the erase and the FLIP INSIDE the list and every CLRUN is a complete frame. For self-running animation, put the matrix delta inside and loop — deltas accumulate per pass, the CPU is idle:
gl CLBEG 1 MDY 5 CLRUN 2 CLEND (a list may not run a list --
gl CLOOP 1 72 so spin via a second list that
redraws the scene... see below)
(Nesting is refused, so the idiomatic spinner records the WHOLE frame
— delta, erase, draw, FLIP, WAIT 1 — in one list and CLOOPs it; cube
is the worked example, man cube.) tri/rotate/camera maintain
list 0 as "the scene"; CLAPP grows a list in place; CLDEL frees a
slot. Lists survive anything except power (SDRAM) and RESETF.
6. Drawing modes (LINFUN)¶
gl LINFUN 4 XOR mode
gl MOVE 10,10 DRAW 200,200 draw...
gl MOVE 10,10 DRAW 200,200 ...and un-draw: ground restored
gl LINFUN 0 back to replace
Modes: 0 replace, 1 complement (invert dest, pen ignored), 2 OR,
3 AND, 4 XOR. They apply to lines, points and outlines from EVERY door
(the mode lives in the display device), take effect between
primitives, and fills always replace. XOR is the rubber-band idiom;
complement is visible on any background. RESETF, reset, and power-up
restore replace mode.
7. Filling arbitrary shapes (AREA)¶
gl COLOR 31,0,0 red pen
gl MOVE 100,100 RECT 200,150 an outline...
gl MOVE 150,125 AREA ...seed-filled from inside
AREA flood-fills from the 2D current point with the pen, bounded by
pen-coloured pixels. AREABC r g b bounds on a stated colour instead,
so the fill and the outline can differ:
gl COLOR 0,0,31 blue outline
gl POLY 4 300,200 350,150 400,200 350,250
gl COLOR 0,63,0 MOVE 350,200 green pen, seed at centre
gl AREABC 0,0,31 fill up to the blue
The fill walks real framebuffer pixels (a scanline flood), so anything
already drawn is a boundary candidate. A seed outside the window is
error 2; a seed sitting ON the boundary colour (or on pen-coloured
pixels) quietly fills nothing. PRMFIL 1 remains the right tool for
filled primitives you are about to draw; AREA is for shapes that
exist only as outlines — and it always paints in replace mode,
whatever LINFUN says.
8. Curves and patterns¶
gl M 240,136 CI 60 a circle at the current point
gl PF 1 CI 40 a filled disc
gl EL 80,30 an ellipse
CIRCLE/ELIPSE rasterize on the device with radii mapped through the
window→viewport scale (they clip to the screen; radii cap at 255 device
pixels). The PGC's ARC and SECTOR were removed 2026-08-30 for card
placement headroom (opcodes 3C/3D report error 1) — draw a partial arc
as a chain of short D segments; the card stepped 4° per segment, and
so can you.
gl LPT -21846 dashed lines ($AAAA)
gl LPT -1 solid again
LINPAT lives in the device like the LINFUN mode — every line from
every door is patterned, BASIC's LINE included, restarting at the
pattern's MSB each primitive. AREA forces solid+replace to protect
its own fill invariant, and RESETF restores everything. (The PGC's
AREAPT patterned fill mask was removed 2026-08-30 to buy placement
headroom on the full card — a successor-board candidate; its opcode
E7 reports error 1.)
9. Vector text (TEXT)¶
gl /FONT.GL load the font (once per power-up)
gl PRO 0 text lives in window space
gl M3 40,200,0 TX "HELLO WORLD!" draw at the current 3D point
A glyph is a command list of relative strokes in a second 64-slot
bank — TDEFIN c records one exactly like CLBEG…CLEND records a
list, and the shipped /FONT.GL defines ASCII 32–95 (lowercase folds
to uppercase). Because strokes ride the ordinary 3D pipeline, TSIZE
(an alias of MDSCAL s s s, 8.8 fixed point: 256 = design size) and
TANGLE (an alias of MDROTZ deg) scale and rotate the letterforms
and the baseline walk together:
gl MDI MDO 240,136,0 TS 1024 TA 20 4x, tilted 20 degrees,
gl M3 240,136,0 TX "BIG" anchored by MDORG
The aliases compose like every matrix verb — MDIDEN resets, and big
or tilted text wants MDORG at its anchor (scaling happens about the
model origin). Use PROJCT 0 (ortho): at the perspective camera's
power-up settings, z=0 geometry sits behind the near plane and nothing
draws. Chars without a glyph skip silently; a font survives RESETF
but not power loss (it lives in SDRAM — stream /FONT.GL again).
Justify with TJUST h v (1/2/3 = left/centre/right and
bottom/middle/top): the offset applies in model units, so size and
angle transform it with the string. TEXTP is the same engine under
its PGC name, and since 10k TEXT records into command lists and
replays with the scene.
10. From BASIC¶
Every GL verb is a native statement (no quotes, expressions allowed):
10 RESETF : CLEARS 0,0,0
20 WINDOW -120,120,-120,120 : VWPORT 104,375,0,271
30 COLOR RGB(0,63,0) : PRMFIL 1
40 FOR A=0 TO 350 STEP 10
50 MDIDEN : MDROTY A
60 POLY3 3,-80,-80,300,80,-80,300,0,40,420
70 NEXT A
COLOR is the GL pen, nothing else (GTEXT's old GPEN colour shadow retired 2026-09-01 -- GTEXT itself lives on as pure-GL 2D sugar that draws with the COLOR pen); GL s$ sends a raw ASCII line when you need
string-building (GL "MDY "+STR$(A)); native list verbs
(CLBEG/CLEND/CLRUN) are synchronous, GL "CLOOP 1 72" is the
non-blocking spin. PIXELR(x,y) reads pixels through the GL PIXRD verb (single-interface, 2026-08-31): window coordinates through the CURRENT window map, fully symmetric with PIXELW in any window. The full
statement list: man basic, GRAPHICS; the language guide chapter in
basic/p8x-basic-guide.md.
11. From C¶
//#use gfx screen-space primitives, GL underneath (man gfx)
//#use g3d the software 3D pipeline / GL-era compatibility (man g3d)
For the GL port itself the idiom is three lines (as used by gl.c, tri.c, cube.c):
int glb(int v) { while (peek(0xFF51) & 128) { } poke(0xFF50, v); return 0; }
int glw(int v) { glb(v & 255); glb((v / 256) & 255); return 0; }
/* hex: glb(opcode); params glw(...) -- drain GLERR when done */
Wait for idle with while (peek(0xFF51) & 64) { } before reading
results or exiting. Named constants: //#use abi + the //#define
header pattern; never raw magic numbers in shipped code.
12. Scene files¶
A .GL file is the ASCII language verbatim — gl FILE.GL streams it.
Start files with CA (the hex-mode escape is the literal
space-terminated three bytes) and end with CX; lead with a VWPORT.
The PG-640A manual's examples convert mechanically (its hex files
carry 4-byte coordinates; re-emit as ASCII). docs/reference/pg640a.pdf
chapter 3 is effectively this engine's extended manual.
13. Performance model¶
- Command bytes are cheap; PIXELS are the cost. A fullscreen fill is ~65k pixel-pairs through the burst filler; a spinning cube is ~40 bytes per frame.
- Matrix verbs cost a compose run at COMMAND time (microseconds); vertices then transform at fixed per-vertex cost.
- LINFUN modes add a read-modify-write per pixel — noticeable only in principle; invisible at panel rates.
WAIT ninside lists paces to real frames; poll GLSTAT bit6 from outside rather than sleeping.- The serial console is slower than everything above: stream scene FILES rather than typing long lines (the line editor keeps 63 chars).