Validates the rendering primitives of P96 RTG card drivers as well as the graphics.library accelerated rendering routines.
Each testcase renders a scene and compares it pixel by pixel against a golden reference which was generated by P96's software rasterizer.
Runs are non-interactive and the exit code reflects the result, so the suite works as an automated check -- including under an emulator with no display.
It also runs on AROS, on which it falls back to cybergraphics.library and
graphics.library instead of Picasso96API.library.
Run all tests for a particular monitor and video mode (WxHxD):
p96cts Z3660 640x480x8
Output looks like:
p96cts 0.13 (1.8.2026) by Bernie Innocenti
testing Z3660 640x480x8 clut, scene 320x200
PASS DrawLine-solid
PASS DrawLine-pattern
FAIL DrawLine-complement 4 of 64000 pixels differ
at 247, 72 golden 89, got 166
at 73,128 golden 202, got 53
... and 2 more
captured output/Z3660/320x200x8/DrawLine-complement.fail.png
wrote difference to output/Z3660/320x200x8/DrawLine-complement.diff.png
A palette run also works on native AGA screens, whose bitmaps are planar rather than chunky, which puts graphics.library's own rendering up against the same reference:
p96cts PAL 320x256x8
Reference images live in golden/WxHxD/. A failing test writes two
images to output/<monitor>/WxHxD/:
<test>.fail.png, what the run actually rendered, and<test>.diff.png, the differing pixels in red over the golden dimmed to gray.
To generate all golden images for a particular scene size and depth, run:
p96cts softrast 320x200x8 CAPTURE
MONITOR and MODE are positional, so the usual invocation is
p96cts <monitor> <WxHxD>. MODE is optional: plain p96cts <monitor> runs
every depth the monitor offers, each at the smallest mode that contains the
scene, and notes the depths it lacks.
| Argument | Meaning |
|---|---|
MONITOR |
Board to render on; softrast for the software rasterizer |
MODE |
Screen mode as WxHxD (default: every supported depth) |
TEST/K |
One testcase as <group>-<test>; all of them by default |
CAPTURE/S |
Write the reference instead of comparing against it |
LAYER/S |
Draw through a Layer covering the whole bitmap (see below) |
CLIP/S |
Also lay two layers over the scene, so drawing is really clipped |
SCENE/K |
Region rendered and compared, as WxH (default 320x200) |
GOLDENDIR/K |
Reference directory (default golden/<scene>x<depth>) |
OUTDIR/K |
Output directory (default output/<monitor>/<scene>x<depth>) |
REPORTFILE/K |
Also write the report to this file |
LISTMODES/S |
Dump the display database and exit |
LISTTESTS/S |
List the testcase names TEST accepts and exit |
HELP/S |
Print this table and exit; -h and --help work too |
By default scenes draw into a bare RastPort, the unclipped path. Applications
draw into windows, so their calls arrive through a Layer instead. LAYER/S
draws through one covering the whole bitmap, which clips nothing, so the same
goldens apply -- both paths must produce identical pixels:
p96cts PAL 320x256x8 LAYER
One scene disagrees on purpose: ScrollRaster-amounts scrolls by distances
that exceed the rectangle's extent, which AmigaOS 3.x handles differently
layered and unlayered -- in graphics.library itself, on native AGA and P96
alike. The failure is the reproducer.
CLIP/S (which implies LAYER) goes further: two thin layers cross the
scene, so every call reaches the driver genuinely split into fragments:
p96cts UAE CLIP
The ordinary goldens stay the reference -- the comparison just excludes the covered pixels. Scenes that cannot run clipped skip as "not clippable".
The tables below are from emulators. If you have access to an Amiga with an RTG board, please run the suite and open an issue to share your results.
| scene | PAL | uaegfx | CyberVision | ZZ9000 |
|---|---|---|---|---|
| DrawLine-solid | ✅ | ✅ | ❌ | ✅ |
| DrawLine-pattern | ✅ | ✅ | ✅ | ✅ |
| DrawLine-jam2 | ✅ | ✅ | ❌ | ✅ |
| DrawLine-inversvid | ✅ | ✅ | ❌ | ✅ |
| DrawLine-complement | ✅ | ✅ | ✅ | ✅ |
| RectFill-drawmodes | ✅ | ✅ | ✅ | ✅ |
| RectFill-edges | ✅ | ✅ | ✅ | ✅ |
| RectFill-invert | ✅ | ✅ | ✅ | ✅ |
| ClipBlit-overlap | ✅ | ✅ | ✅ | ✅ |
| ClipBlit-disjoint | ✅ | ✅ | ✅ | ✅ |
| BltTemplate-offsets | ✅ | ✅ | ✅ | ✅ |
| BltTemplate-sizes | ✅ | ✅ | ✅ | ✅ |
| BltTemplate-drawmodes | ✅ | ✅ | ✅ | ✅ |
| BltTemplate-masks | ✅ | ✅ | ✅ | ✅ |
| BltPattern-drawmodes | ✅ | ✅ | ✅ | ✅ |
| BltPattern-mask | ✅ | ✅ | ✅ | ✅ |
| BltPattern-phase | ✅ | ✅ | ✅ | ✅ |
| BltBitMap-minterms | ✅ | ✅ | ✅ | ✅ |
| BltBitMap-friend | ✅ | ✅ | ✅ | ✅ |
| BltBitMap-offsets | ✅ | ✅ | ✅ | ✅ |
| BltBitMap-sizes | ✅ | ✅ | ✅ | ✅ |
| BltBitMap-planemask | ✅ | ✅ | ✅ | ✅ |
| BltBitMap-writemask | ✅ | ✅ | ✅ | ✅ |
| BltBitMap-stencil | ✅ | ✅ | ✅ | ✅ |
| BltBitMap-shallow | ✅ | ✅ | ✅ | ✅ |
| BitMapScale-down | ✅ | ✅ | - | - |
| BitMapScale-up | ✅ | ✅ | - | - |
| BitMapScale-ratios | ✅ | ✅ | - | - |
| ScrollRaster-directions | ✅ | ✅ | ✅ | ✅ |
| ScrollRaster-drawmodes | ✅ | ✅ | ✅ | ✅ |
| ScrollRaster-backfill | ✅ | ✅ | ✅ | ✅ |
| ScrollRaster-amounts | ✅ | ✅ | ✅ | ✅ |
| PixelArray-pens8 | ✅ | ✅ | ✅ | ✅ |
| PixelArray-lut8 | ✅ | ✅ | ✅ | ✅ |
| PixelArray-rgb | - | ✅ | ✅ | ✅ |
A ❌ links to the bug it found. A - is untested.
| scene | PAL | Z3660 |
|---|---|---|
| DrawLine-solid | ✅ | ✅ |
| DrawLine-pattern | ✅ | ✅ |
| DrawLine-jam2 | ✅ | ✅ |
| DrawLine-inversvid | ✅ | ✅ |
| DrawLine-complement | ✅ | ✅ |
| RectFill-drawmodes | ✅ | ✅ |
| RectFill-edges | ✅ | ✅ |
| RectFill-invert | ✅ | ✅ |
| ClipBlit-overlap | ✅ | ✅ |
| ClipBlit-disjoint | ✅ | ✅ |
| BltTemplate-offsets | ✅ | ✅ |
| BltTemplate-sizes | ✅ | ✅ |
| BltTemplate-drawmodes | ✅ | ✅ |
| BltTemplate-masks | ✅ | ✅ |
| BltPattern-drawmodes | ✅ | ✅ |
| BltPattern-mask | ✅ | ✅ |
| BltPattern-phase | ✅ | ✅ |
| BltBitMap-minterms | ✅ | ✅ |
| BltBitMap-friend | - | - |
| BltBitMap-offsets | ✅ | ✅ |
| BltBitMap-sizes | ✅ | ✅ |
| BltBitMap-planemask | ✅ | ✅ |
| BltBitMap-writemask | - | - |
| BltBitMap-stencil | ✅ | ✅ |
| BltBitMap-shallow | ✅ | ✅ |
| BitMapScale-down | - | - |
| BitMapScale-up | - | - |
| BitMapScale-ratios | - | - |
| ScrollRaster-directions | ✅ | ✅ |
| ScrollRaster-drawmodes | ✅ | ✅ |
| ScrollRaster-backfill | ✅ | ✅ |
| ScrollRaster-amounts | ✅ | ✅ |
| PixelArray-pens8 | - | ✅ |
| PixelArray-lut8 | - | ✅ |
| PixelArray-rgb | - | ✅ |
Notes:
- The Z3660 column needs a
Z3660.cardbuilt from git: it depends on Z3660#18 and Z3660#19, merged after the last release. - Z3660#19 leaves
COMPLEMENTlines to Picasso96, as the ZZ9000 driver already does.struct Linecarries noFRST_DOT, so an acceleratedCOMPLEMENTline cannot tell a freshDraw()from one continuing at a vertex, and inverting a shared vertex twice restores it. - Z3660#18 uploads the one template line a patterned blit reads. P96 sends the
JAM2 | COMPLEMENTtiles as a template blit whoseTemplate->BytesPerRowis 0, so sizing the upload asBytesPerRow * hcopies nothing and leaves the board to blit whatever the previous operation left in the template buffer. Why P96 passes a zero stride here is unexplained.
AROS has no Picasso96API.library, so the suite falls back to
cybergraphics.library and graphics.library. What a red cell finds here is a
divergence in AROS itself rather than in a card driver. PAL is the native
planar path; Z3660 (Copperline) and ZZ9000 (Amiberry) are RTG, red if the
scene fails at any tested depth.
| scene | PAL | Z3660 | ZZ9000 |
|---|---|---|---|
| DrawLine-solid | ✅ | ✅ | ✅ |
| DrawLine-pattern | ✅ | ✅ | ✅ |
| DrawLine-jam2 | ✅ | ✅ | ✅ |
| DrawLine-inversvid | ✅ | ✅ | ✅ |
| DrawLine-complement | ✅ | ✅ | ✅ |
| RectFill-drawmodes | ✅ | ✅ | ✅ |
| RectFill-edges | ✅ | ✅ | ✅ |
| RectFill-invert | ✅ | ✅ | ✅ |
| ClipBlit-overlap | ✅ | ✅ | ✅ |
| ClipBlit-disjoint | ✅ | ✅ | ✅ |
| BltTemplate-offsets | ✅ | ✅ | ✅ |
| BltTemplate-sizes | ✅ | ✅ | ✅ |
| BltTemplate-drawmodes | ✅ | ✅ | ✅ |
| BltTemplate-masks | ✅ | ✅ | ✅ |
| BltPattern-drawmodes | ✅ | ✅ | ✅ |
| BltPattern-mask | ✅ | ✅ | ✅ |
| BltPattern-phase | ✅ | ✅ | ✅ |
| BltBitMap-minterms | ✅ | ✅ | ✅ |
| BltBitMap-friend | ✅ | ✅ | ✅ |
| BltBitMap-offsets | ✅ | ✅ | ✅ |
| BltBitMap-sizes | ✅ | ✅ | ✅ |
| BltBitMap-planemask | ✅ | ✅ | ✅ |
| BltBitMap-writemask | ✅ | ✅ | ✅ |
| BltBitMap-stencil | ✅ | ✅ | ✅ |
| BltBitMap-shallow | ✅ | ✅ | ✅ |
| BitMapScale-down | ✅ | ✅* | ✅* |
| BitMapScale-up | ✅ | ✅ | ✅ |
| BitMapScale-ratios | ✅ | ✅* | ✅* |
| ScrollRaster-directions | ✅ | ✅ | ✅ |
| ScrollRaster-drawmodes | ✅ | ✅ | ✅ |
| ScrollRaster-backfill | ✅ | ✅ | ✅ |
| ScrollRaster-amounts | ✅ | ✅ | ✅ |
| PixelArray-pens8 | ✅ | ✅ | ✅ |
| PixelArray-lut8 | ✅ | ✅ | ✅ |
| PixelArray-rgb | - | ✅ | ✅ |
The failures this table found were tracked in AROS issue #936, which has the per-fix breakdown, and in AROS issue #950 for BitMapScale, fixed by AROS PR #977.
* The truecolor down/ratios runs still count red pixels against the goldens, but every one of them is either a red flag square or the extra column that the goldens record from the reference's own truecolor overshoot (see Coverage below): AROS output is correct there, and the cells go fully green when the goldens are recaptured after the P96 fix.
Two exceptions to the green above: the ZZ9000 16-bit runs currently fail most scenes wholesale (under investigation; Z3660 16-bit is clean, so it is the ZZ9000 path rather than core graphics.library), and the table says nothing about 15-bit modes, which these boards do not expose under AROS.
What each group reaches. The P96 hook is what a card driver has to get right; a
Default implementation in P96's shared code stands in for any hook a driver
leaves out, so a missing hook still renders.
| group | graphics.library | P96 hook |
|---|---|---|
| DrawLine | Move(), Draw(), PolyDraw() |
DrawLine |
| RectFill | RectFill() |
FillRect, InvertRect |
| ClipBlit | ClipBlit() |
BlitRect |
| BltTemplate | BltTemplate() |
BlitTemplate |
| BltPattern | BltPattern() |
BlitPattern |
| BltBitMap | BltBitMap(), BltBitMapRastPort(), BltMaskBitMapRastPort() |
BlitRectNoMaskComplete |
| BitMapScale | BitMapScale(), ScalerDiv() |
- |
| ScrollRaster | ScrollRaster(), ScrollRasterBF() |
BlitRect, FillRect |
| PixelArray | WritePixelArray8(), p96WritePixelArray() |
WriteTrueColorData, WriteTrueColorPixels |
BltBitMap also reaches BlitPlanar2Chunky and BlitPlanar2Direct, which
nothing else here does: a planar source bitmap goes through the first on a CLUT
screen and the second on a truecolor one.
BitMapScale has no driver hook: P96 scales in common code, and the scenes
prove it by comparing bit-identically on the software rasterizer and on a
board. Each scale also cross-checks the autodoc rule that the size
BitMapScale() reports equals what ScalerDiv() computes, and paints a red
square at the destination origin where the two disagree. P96's truecolor
scaler makes every downscale one pixel wider than ScalerDiv says -- the
extra column samples one past the source row, which on a packed bitmap is
the leftmost column of the next one -- so the 15, 16 and 24-bit goldens all
carry those red flags: they record the reference faithfully, bug included,
and get recaptured when it is fixed. The CLUT path agrees with ScalerDiv,
so the 8-bit goldens carry none.
Runs compare pens at depth 8 and canonical R8G8B8 readback at 15, 16 and 24. A 15/16-bit run renders its off-board reference in the screen's own quantized format, so both sides lose channel precision identically, and the guard wall samples what its shades actually read back rather than predicting each format's rounding.
Hooks with no coverage: WriteYUVRect, ScrollPlanar, UpdatePlanar. AROS
calls none of them (see the TODO in p96gfx_rtg.h), so scenes for these would
only exercise AmigaOS.
EraseRect(): its no-layer path fills throughRectFill(); no font state to set up.Text(): renders throughBlitTemplate; wants a write-mask sweep.ClearEOL(),ClearScreen(): clear to pen 0, or BPen in JAM2. A golden pins one font's metrics.SetRast(),Flood(),AreaEnd(),DrawCircle(),DrawEllipse(): uncovered.- A caller-level group needs a font with identical metrics under AmigaOS and AROS.
The default include path is where the amiga-gcc toolchain ships the P96 headers, so a containerised build takes no arguments:
make docker-build
With a toolchain that does not bundle them, point at an unpacked
P96Develop.lha:
make CC=/path/to/bin/m68k-amigaos-gcc \
P96_CFLAGS=-I/path/to/Picasso96Develop/Include
Images are read and written with zlib and libpng, which are committed under
third_party/ already built for this target, so nothing needs fetching or
cross-building first. They rarely need rebuilding, but when they do, the same
container runs their build script:
make docker-thirdparty
The archives are reproducible, so a rebuild can be checked byte for byte
against the committed ones. third_party/README.md has the upstream versions,
checksums, and why both are built -noixemul.
A testcase renders a complete scene, clearing it first, and must keep all drawing inside the bitmap: the RastPort has no Layer, so graphics.library does not clip it and drawing outside corrupts memory.
Scenes should be built so that a wrong driver cannot pass by accident. Drawing
solid lines in one pen, for instance, cannot detect a pixel written twice --
it takes a mode like COMPLEMENT, where writing twice is not the same as
writing once, and a figure whose lines actually cross.
