Skip to content
RetrogamingDeep Dive Published Updated 12 min readViews unavailable

3DO Cel Engine: CCB Streams, Preambles, and Projected Quads

Follow 3DO cel data through unpacking, pixel decoding, PIXC processing, and quadrilateral projection, with a careful guide to variable-length CCB state.

The 3DO cel engine is more than a sprite scaler. It consumes a Cel Control Block (CCB), reads a source image according to a preamble, optionally unpacks and decodes its pixels, applies pixel-processor operations, and projects the result into a quadrilateral in the framebuffer. A CCB can carry geometry, source pointers, palette selection, pixel-combiner state, and links to further CCBs. Some fields are present only when particular flag bits are set, while omitted fields leave the cel engine using state loaded by an earlier cel. That combination makes CCB interpretation one of the most important details in accurate 3DO graphics work.

The 3DO Portfolio Graphics Programmer’s Guide describes the hardware path as a Data Unpacker (DUP), Pixel Decoder (PDC), Pixel Processor, and Projector. Keep these as explicit stages when debugging. A malformed packed stream is not a projection error; a stale PLUT is not a bad source pointer; a correct decoded pixel can still be combined with the wrong framebuffer value by PIXC.

The cel pipeline starts with a preamble

Cel source data lives in memory and may be coded or uncoded, packed or unpacked. The guide describes six coded pixel formats and two uncoded formats, with a preamble telling the DUP and PDC how to interpret the stream. The unpacker expands run-length packets when data is packed. The decoder maps encoded pixels into color and associated per-pixel controls. The pixel processor can scale color components and combine them with a second source. Finally, the projector writes pixels into the destination quadrilateral.

The preamble can normally sit at the start of source data. This is convenient when the cel is self-contained. A cel that reuses an existing bitmap region, framebuffer memory, or another source that has no cel preamble can instead carry the preamble in its CCB. The CCBPRE flag selects that location. Packed data uses one preamble word; unpacked data can use two. Changing the location without changing the corresponding flag makes valid pixel bytes look like format metadata, or vice versa.

Packed cel data is not just a compressed linear byte array. Each horizontal line has an offset to the next line and its own bit-packed run-length packet stream. Packets can describe literal pixels, a repeated pixel, a transparent run, or an end-of-line marker. The line stream is padded to a word boundary, and packet bits do not necessarily start on byte boundaries. An implementation that decodes each row independently must honor the row offset and bit cursor rather than rounding every packet up to the next byte.

Unpacked cels have their own row-addressing rules. Preamble values identify the source rectangle and the word offset between rows. This allows a cel to use a sub-rectangle of a larger image or framebuffer without copying the pixels into a separate packed cel. Log the source address, dimensions, bit depth, coded/uncoded mode, packing mode, row offset, and preamble source before examining projection coordinates.

CCBs are flag-shaped records, not fixed C structs

A CCB begins with six required 32-bit words: flags, next CCB pointer, source pointer, PLUT pointer, X position, and Y position. Additional words are included in a specific order only when their controlling flags request them. LDSIZE adds four projection-size words (HDX, HDY, VDX, VDY); LDPRS adds two perspective increments (HDDX, HDDY); LDPIXC adds a pixel-processor control word; and CCBPRE adds the preamble data, with its word count determined by the packed/unpacked format. The list of optional words is therefore a serialized, flag-dependent layout.

This is a common source of parser errors. A host-language struct CCB that always includes every optional field is useful as an authoring convenience only if the serialization layer writes or reads exactly the optional fields the hardware format specifies. It is unsafe to walk a hardware CCB using sizeof(struct CCB) or to infer that PLUTPTR is absent when a cel reuses a palette: the pointer word is part of the six-word base record even when LDPLUT says not to load from it.

A capacity check can calculate the optional word count before a serializer emits a CCB. The symbolic masks below are placeholders for the target SDK’s actual flag constants, and this helper does not replace parsing the words in the documented order:

#include <stddef.h>
#include <stdint.h>

static size_t ccb_word_count(uint32_t flags,
                             uint32_t load_size_mask,
                             uint32_t load_perspective_mask,
                             uint32_t load_pixc_mask,
                             uint32_t ccb_preamble_mask,
                             uint32_t packed_mask) {
    size_t words = 6; /* Required base words. */

    if (flags & load_size_mask)
        words += 4;    /* HDX, HDY, VDX, VDY */
    if (flags & load_perspective_mask)
        words += 2;    /* HDDX, HDDY */
    if (flags & load_pixc_mask)
        words += 1;    /* PIXC */
    if (flags & ccb_preamble_mask)
        words += (flags & packed_mask) ? 1 : 2;

    return words;
}

The production parser should additionally validate that the entire variable-length record lies in readable memory, that each pointer resolves in the active address space, and that optional state words appear in the manual’s required order. Treat malformed flags or truncated records as explicit errors instead of silently reading the next cel’s header as a missing projection word.

Omitted CCB words preserve device state

The CCB flags do more than describe which values belong to the current record. They decide which values are loaded into cel-engine registers. When LDSIZE is clear, the engine keeps the previous horizontal and vertical projection values. When LDPRS is clear, the previous per-row perspective increments remain active. With LDPIXC clear, the pixel-processor control word is not replaced. LDPLUT controls whether a new table is loaded; if it is clear, the decoder reuses the current PLUT. YOXY decides whether to load the CCB’s origin coordinates or continue from the current cel-engine origin.

This is deliberate state reuse, not random behavior. It can reduce repeated CCB data and allow a group of cels to share scale, palette, or pixel-processing state. It also means that an emulator cannot treat every CCB as a self-contained draw call with default state. The same CCB bytes can produce different pixels depending on the engine state left by the previous cel.

For deterministic debugging, store an explicit cel-engine register snapshot alongside each rendered cel: origin, horizontal/vertical steps, perspective increments, PIXC modes, PLUT contents, clipping, and format state. A useful trace compares the incoming flags, words actually consumed, state values before and after the load, and the resulting destination coordinates. When a cel is wrong only after another cel, inspect omitted-word state before changing its source image.

Fixed-point vectors define a quadrilateral

XPOS and YPOS establish a fixed-point origin for the cel projection. The horizontal vectors HDX and HDY determine where the next source pixel in a row lands. The vertical vectors VDX and VDY determine the first pixel position of the next source row. Together, these vectors set the scale, orientation, and skew of the projected cel. A regular rectangular image can therefore become enlarged, reduced, rotated, sheared, reflected, or mapped into a non-axis-aligned quadrilateral without first transforming the source bitmap on the CPU.

The fields do not all use the same fixed-point format. The guide specifies XPOS and YPOS as 16.16; HDX and HDY as 12.20; VDX and VDY as 16.16; and HDDX and HDDY as 12.20. The HDD pair changes the horizontal step from row to row, adding a perspective-like change to the projection. Reading every field as one generic 16.16 coordinate produces systematic drift and incorrect scaling even if the sign and integer parts look plausible.

Use signed arithmetic with explicit fixed-point conversion helpers in tooling. Avoid converting through floating-point when comparing low-level emulator output, because rounding differences can change which framebuffer coordinate receives a pixel. A regression set should include fractional origins, negative steps for reflections, nonzero HDD values, and geometry that lands just across a clipping boundary.

The CCB also controls front/back-face pixel selection and super-clipping behavior. ACW and ACCW govern clockwise and counterclockwise projected pixels; the super-clip flags combine with engine-wide clipping state. TWD can skip a cel based on its first projected pixel’s orientation, but the manual warns its behavior is not ideal or predictable. Do not turn an obscure cel flag into a universal polygon-culling rule without validating the exact mode and silicon behavior.

Pixel decoding, PLUT, and per-pixel controls

Coded cel pixels index color entries in the PDC’s Pixel Lookup Table (PLUT). The guide describes 32 lookup registers in the pixel decoder; pixel encodings with fewer than five index bits can take their missing high bits from CCB PLUTA state. Some formats also carry an Alternate Multiplier Value (AMV), which lets the pixel processor vary color scaling per pixel. Other formats carry a P-mode bit or VH corner-weight information. The chosen source format therefore affects much more than palette depth.

Uncoded 16-bit pixels are a straightforward format when the source is already in a framebuffer-compatible representation. Uncoded and coded formats also differ in which per-pixel controls they can carry. A decoder should expose the intermediate fields separately - color components, AMV, P-mode, and VH - rather than collapsing each source pixel into a single RGB value too early.

PIXC contains two pixel-processor modes. The primary source can be a decoded pixel or the current framebuffer pixel. The secondary source can be a constant, the framebuffer, or another decoded value. Multiplier, divider, add/subtract, XOR, sign extension, and wrap controls determine how those sources combine. This is the basis for effects such as translucency, shading, and framebuffer-dependent pixel operations. A generic alpha blend is not an adequate substitute because the machine’s two-source arithmetic and P-mode selection are explicitly controlled by PIXC and pixel data.

Transparency has another state-sensitive edge. When a decoded value of zero is treated as transparent, the engine skips the framebuffer write. With BGND set, that zero value instead proceeds through the pixel processor as a color. NOBLK controls how written zero-like values are represented relative to the display generator’s background interpretation. Testing only opaque pixels misses these distinctions. Use patterns with palette index zero over multiple framebuffer colors and inspect both whether a write occurred and the resulting stored pixel value.

NEXTPTR links a CCB to the next cel in a group. The LAST flag ends the group without following the pointer. The next, source, and PLUT pointers can each be absolute or relative according to the corresponding flags. The pointer’s address mode is part of the CCB interpretation; applying absolute-pointer arithmetic to relative offsets can send a valid-looking cel list far outside its buffer.

Linked CCBs make it practical to render a scene as an ordered set of cels and to animate source images without rebuilding all geometry. The SKIP flag can avoid drawing one cel while continuing to the next linked CCB. For dynamic images, the guide describes alternating source buffers and changing the source pointer so one buffer can be written while the other is displayed.

Pointer validation should include both the CCB record and the source/palette data it references. Add cycle and maximum-count guards when processing untrusted or corrupted lists in an emulator. A bad next pointer must not create an infinite host loop, and a relative pointer must be based on the address of the correct pointer field as documented, not the start of an unrelated allocation.

Accuracy tests that isolate one stage at a time

Start with an uncoded, unpacked cel and axis-aligned projection. Verify source stride, origin, and output pixels before introducing palettes or PIXC. Then add one feature per test so a failure can be assigned to a specific engine stage:

  • Decode the same image as uncoded and as indexed coded data with a known PLUT.
  • Use packed literal, repeated-pixel, transparent-run, and row-offset cases, including packet boundaries that cross word boundaries.
  • Put the preamble in source data, then move it to the CCB and set the corresponding flag; confirm that no source bytes are consumed twice.
  • Omit LDSIZE, LDPRS, LDPIXC, and LDPLUT independently after a cel that establishes non-default state, and verify documented state reuse.
  • Project fractional and negative fixed-point steps, shear, reflect, and vary HDDX/HDDY by row.
  • Exercise each pointer’s absolute and relative mode independently, then test linked lists, SKIP, and LAST.
  • Compare zero decoded pixels with transparent/background modes while preserving the same PLUT and PIXC state.
  • Save and restore halfway through a cel-list submission and verify engine register state as well as framebuffer output.

Record CCB address and byte extent, flags, parsed word count, link target, preamble source, source format, PLUT identity, fixed-point vectors, clipping decisions, PIXC state, and framebuffer writes. Keep a raw source-data dump and a post-decode pixel trace. Those artifacts make it possible to distinguish a bit-packing bug from a stale register or a projection math error.

Treat each cel as a stateful graphics transaction

The 3DO cel engine transforms a source stream into framebuffer pixels through multiple hardware stages controlled by a variable-length CCB. Correct rendering depends on interpreting the preamble, CCB flags, inherited engine state, fixed-point projection vectors, palette, pixel-processing modes, and pointer chain together. A production emulator should model those stages and state transitions explicitly rather than flattening every cel into a host sprite with position, scale, and alpha.

That model is also the best debugging tool. If source decode is correct but pixels land in the wrong shape, inspect the fixed-point vectors. If only some linked cels have wrong colors, inspect inherited PLUT or PIXC state. If corruption begins at one CCB, validate its optional-word count and pointer mode. The CCB is not incidental metadata around the image; it is the program that tells the cel engine what image means and where its pixels go.

Related:

Sources:

Comments