Skip to content

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 image command 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 GL PIXRD verb (2026-08-31) and maps through the CURRENT window; text is PGC TEXT with the boot-loaded font (GTEXT is pure-GL 2D sugar over it); IMAGE is the GL BLIT verb (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 y2 maps 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 20 twice is 40 degrees. MDIDEN resets the modeling matrix; VWIDEN the camera; RESETF everything.
  • The camera: VWRPT x y z picks the point the viewer orbits, VWX/VWY/VWZ deg orbit it, DISTAN d backs the viewer off, PROJCT angle sets the lens (0 = orthographic; power-up = the native focal-256 camera, which is also what DISTAN 0 means).
  • 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 n inside 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).