14 KiB
Display System
Source: src/dusk/display/
The display system is the rendering pipeline. It is abstracted across platforms via displayplatform.h — see architecture.md for the abstraction pattern. The current concrete backends are OpenGL (src/duskgl/) and GX/Dolphin (src/duskdolphin/).
For the planned render-queue refactor (required for Saturn), see display-refactor.md.
Render / ROP system (display/render/)
The ROP (Render OPcode) system is the low-level, backend-agnostic drawing API. All game drawing goes through this layer; backends (rendergl.c, renderpsp.c, renderdolphin.c) execute the commands at display flush time.
API (display/render/render.h)
/* Clear the framebuffer */
void renderClear(color_t color);
/* 2D textured quad at pixel coordinates */
void renderSprite(
int16_t x, int16_t y, int16_t w, int16_t h,
int16_t depth, /* 0=front … 32767=back */
rtexture_t texture, color_t tint
);
/* Set perspective projection for subsequent 3D draws */
void renderSetProjection(
fixed_t fovY, fixed_t aspect, fixed_t nearZ, fixed_t farZ
);
/* Set camera position/target for subsequent 3D draws */
void renderSetView(
int16_t eyeX, int16_t eyeY, int16_t eyeZ,
int16_t tgtX, int16_t tgtY, int16_t tgtZ
);
/* World-space quad: center point + right half-extent + up half-extent */
void renderQuad3D(
int16_t cx, int16_t cy, int16_t cz,
int16_t rx, int16_t ry, int16_t rz,
int16_t ux, int16_t uy, int16_t uz,
int16_t depth,
rtexture_t texture, color_t tint
);
/* Create / dispose an 8-bit indexed palette texture */
rtexture_t renderTextureCreate(
uint16_t w, uint16_t h,
const uint8_t *indices, /* w×h pixel indices (0-255) */
const color_t *palette /* 256 RGBA colour entries */
);
void renderTextureDispose(rtexture_t tex);
/* Mutable pointers to the texture's CPU-side data.
* Write directly to these; the next draw call picks up the changes.
* GL: dirty flag set on getter call; glTexSubImage2D at next bind.
* PSP: re-pads indices and converts palette → ABGR at bind time.
* Dolphin: re-tiles CI8 and converts palette → RGB5A3 at bind time. */
color_t *renderTextureGetPalette(rtexture_t tex); /* color_t[256] */
uint8_t *renderTextureGetIndices(rtexture_t tex); /* uint8_t[w*h] */
Coordinate conventions
| Domain | Type | Scale | Notes |
|---|---|---|---|
| 3D world positions | int16_t |
1 unit = 1 cm | Matches PS1 GTE / N64 RSP native format |
| Camera/projection params | fixed_t |
Q24.8 | FIXED(x) for literals |
| 2D screen positions | int16_t |
pixels | Origin top-left |
| UV coords | uint8_t |
0–255 → 0.0–1.0 | Stored in ROP structs |
Palettized textures
All textures are 8-bit indexed. renderTextureCreate takes:
indices: oneuint8_tper pixel (0–255), row-majorpalette: exactly 256color_tRGBA entries
Per-platform storage:
| Platform | CPU source of truth | GPU/native format | When derived |
|---|---|---|---|
| GL (Linux/Vita) | color_t palette[256] + uint8_t *cpuIndices in slot |
GL_R8 index tex + GL_RGBA 256×1 palette tex |
Lazy: dirty flag set by getter, glTexSubImage2D at next bind |
| PSP | color_t palette[256] + unpadded uint8_t *cpuIndices |
Stride-padded indices (POT ≥ 8) + ABGR8888 CLUT in shared pspAbgrBuf |
Every bindTexture call; dcache-flushed before GU reads |
| Dolphin/GC/Wii | color_t palette[256] + unpadded uint8_t *cpuIndices |
CI8 tiled (8×4 tiles, 32 B/tile) + RGB5A3 TLUT in tlutData |
Every bindTexture call; DCFlushRange before GX load |
GL palette shader detail: The fragment shader samples the R8 index texture, converts the normalised float back to an exact texel centre with raw*(255/256) + 0.5/256, then looks up the 256×1 palette texture. This gives pixel-exact results for all 256 index values and allows independent real-time updates to indices or palette.
Dolphin RGB5A3 encoding:
- Opaque (
a == 255): bit 15 = 1, RGB555 - Transparent: bit 15 = 0, A3RGB4 (alpha quantised to 3 bits — dithered transparency is planned for a future pass)
ROP buffer (display/render/ropbuffer.h / rop.h)
Commands are written into ROPBUFFER (a static byte array) then replayed by the backend at flush time. All ops are fixed-size aligned structs:
| Op | Struct | Size |
|---|---|---|
ROP_CLEAR |
ropclear_t |
32 bytes |
ROP_DRAW_SPRITE |
ropsprite_t |
32 bytes |
ROP_SET_PROJECTION |
ropprojection_t |
32 bytes |
ROP_SET_VIEW |
ropview_t |
32 bytes |
ROP_DRAW_QUAD_3D |
ropquad3d_t |
64 bytes |
ROP_DRAW_TILEMAP_CHUNK |
roptilemapc_t |
32 bytes |
ropOpSize(op) returns the byte size for any op. Backends iterate with offset += ropOpSize(op).
Texture handles (display/render/rtexture.h)
rtexture_t is a uint16_t index into the platform's texture table. RTEXTURE_NONE (0 or a sentinel) means "white fallback". Tables are platform-static; handles are valid until renderTextureDispose is called.
Tilemap chunk handles (display/render/rtilemapchunk.h)
rtilemapchunk_t is a uint16_t index into the platform's chunk table. RTILEMAPCHUNK_INVALID (0) means no-op. Chunks are pre-built at map load time; each backend constructs its native draw structure once (VAO+VBO on GL, display list on PSP/GX/N64) and the ROP entry costs only a handle lookup + single native draw call per frame.
/* Build once at map load */
rtilemapchunk_t chunk = renderTilemapChunkCreate(
chunkW, chunkH, /* size in tiles */
tileW, tileH, /* pixels per tile */
tileset, /* rtexture_t of the packed tileset */
tileIndices /* uint8_t[chunkW*chunkH], row-major tile indices */
);
/* Each frame for visible chunks */
renderTilemapChunk(screenX, screenY, depth, chunk);
/* At map unload */
renderTilemapChunkDispose(chunk);
Animated tiles should be drawn on top as separate renderSprite() calls; the chunk itself is treated as static geometry and never rebuilt at runtime.
Per-platform build:
| Platform | What's built at create time | Draw cost per frame |
|---|---|---|
| GL (Linux/Vita) | VAO + VBO (GL_STATIC_DRAW), uOffset uniform translates to screen pos |
1 glDrawArrays |
| PSP | GU display list in uncached EDRAM | 1 sceGuCallList |
| GC/Wii | Compiled GX display list | 1 GX_CallDispList |
| PS1 | Pre-linked POLY_FT4/SPRT chain | Linked into OT at one slot |
| N64 | RDP display list with pre-scheduled LOAD_TILE batches (TMEM-aware) |
1 gSPDisplayList |
| Saturn | VDP2 plane config + VRAM tilemap data | Scroll register write only |
Initialization order
Within the display system, init must follow this order (enforced in engine.c):
displayInit → uiInit → uiTextboxInit
Within displayInit, the platform typically initialises: framebuffer → screen → shader list → textures → spritebatch → text system.
display_t / displaystate_t
display_t DISPLAY is the global display instance (type alias for displayplatform_t).
displaystate_t carries per-draw-call render state flags:
DISPLAY_STATE_FLAG_CULL // face culling
DISPLAY_STATE_FLAG_DEPTH_TEST // depth testing
DISPLAY_STATE_FLAG_BLEND // alpha blending
Set state before drawing with displaySetState(state).
Screen (display/screen/)
screen_t SCREEN manages the logical viewport that game content renders into. On dynamic-size platforms (Linux/SDL2) the screen can differ from the native window/framebuffer resolution.
Screen modes:
SCREEN_MODE_BACKBUFFER — maps 1:1 to backbuffer
SCREEN_MODE_FIXED_SIZE — fixed pixel dimensions
SCREEN_MODE_ASPECT_RATIO — fixed aspect, scale to fit
SCREEN_MODE_FIXED_HEIGHT — fixed height, width scales
SCREEN_MODE_FIXED_WIDTH — fixed width, height scales
SCREEN_MODE_FIXED_VIEWPORT_HEIGHT — fixed viewport height
The linux target defines DUSK_DISPLAY_SCREEN_HEIGHT=240, producing a 240p fixed-height viewport.
Render loop usage:
screenBind(); // set up viewport, projection
// ... draw game content ...
screenUnbind();
screenRender(); // blit to backbuffer / current framebuffer
SCREEN.width / SCREEN.height are the logical dimensions used for world-to-screen math — always prefer these over the framebuffer dimensions.
Framebuffer (display/framebuffer/)
framebuffer_t FRAMEBUFFER_BACKBUFFER is the platform backbuffer. FRAMEBUFFER_BOUND points to the currently-bound framebuffer (or NULL for backbuffer).
frameBufferInitBackBuffer(); // called once at startup
frameBufferBind(fb); // NULL → backbuffer
frameBufferClear(FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH, COLOR_BLACK);
frameBufferGetWidth(fb) / frameBufferGetHeight(fb) / frameBufferGetAspect(fb);
On platforms with DUSK_DISPLAY_SIZE_DYNAMIC, off-screen framebuffers can be created with frameBufferInit(fb, w, h) and disposed with frameBufferDispose(fb). Fixed-resolution platforms (PSP, GameCube) only ever use the backbuffer.
Mesh (display/mesh/)
mesh_t is a vertex buffer. The type is meshplatform_t (e.g. a VAO+VBO on GL, a GX display list on Dolphin).
meshInit(&mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, vertexCount, verticesPtr);
meshFlush(&mesh, offset, count); // upload CPU vertices → GPU
meshDraw(&mesh, offset, count); // draw; pass -1 for count to draw all
meshDispose(&mesh);
Key distinction: meshFlush uploads data to GPU memory; meshDraw issues the draw call. For static geometry (chunk meshes) you call meshFlush once on load, then meshDraw every frame. For dynamic geometry (spritebatch) you meshFlush + meshDraw each frame.
meshvertex_t (display/mesh/meshvertex.h) contains:
float_t uv[2]— texture coordinatesfloat_t pos[3]— position- Optionally
color_t colorifMESH_ENABLE_COLORis defined (off by default)
Primitive mesh generators live alongside mesh.h: quad.h, plane.h, cube.h, sphere.h, capsule.h, triprism.h.
Shader (display/shader/)
shader_t is shaderplatform_t (GLSL program on GL, TEV state block on Dolphin).
shaderInit(&shader, &definition);
shaderBind(&shader);
shaderSetMatrix(&shader, "uModel", modelMat);
shaderSetTexture(&shader, "uTexture", &texture);
shaderSetColor(&shader, "uColor", COLOR_WHITE);
shaderSetMaterial(&shader, &material);
shaderDispose(&shader);
Shader list (display/shader/shaderlist.h)
The engine maintains a small set of built-in shaders in SHADER_LIST_DEFS[]. Currently only one is defined:
SHADER_LIST_SHADER_UNLIT→SHADER_UNLIT— unlit textured/colored rendering, used for all world and entity drawing.
shaderListInit() compiles/uploads all built-in shaders and sets shared projection/view matrices. Call once after display init.
Materials (display/shader/shadermaterial.h)
shadermaterial_t is a union of all shader-specific material structs. Currently only shaderunlitmaterial_t:
shadermaterial_t mat = {
.unlit = {
.color = COLOR_WHITE,
.texture = &myTexture, // NULL for solid color
}
};
shaderSetMaterial(&SHADER_UNLIT, &mat);
Texture (display/texture/)
textureInit(&texture, width, height, format, data);
textureDispose(&texture);
Width and height must be powers of two (asserted at init time).
textureformat_t is textureformatplatform_t. Supported formats vary by platform; the common ones are TEXTURE_FORMAT_RGBA and TEXTURE_FORMAT_PALETTE.
texturedata_t is a union:
// RGBA:
data.rgbaColors = colorArray;
// Paletted:
data.paletted.indices = indexArray;
data.paletted.palette = &palette; // palette color count must be power of two
Built-in textures (defined in texture.c, no asset loading needed):
TEXTURE_WHITE— 4×4 solid whiteTEXTURE_TEST— 4×4 black/magenta checkerboard
Palette (display/texture/palette.h)
Up to PALETTE_COUNT (6) global palettes in PALETTES[], each holding up to PALETTE_COLOR_COUNT (255) color_t entries.
Tileset (display/texture/tileset.h)
A tileset slices a texture into a grid of equal-sized tiles. Used by fonts and UI frames. The tileset does not own the texture — it references a texture_t *.
SpriteBatch (display/spritebatch/)
The primary 2D/billboard drawing primitive. Accumulates spritebatchsprite_t quads and flushes them in batches of SPRITEBATCH_FLUSH_COUNT (16) sprites at a time.
// Per frame:
spriteBatchClear();
spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material); // auto-flushes when batch full
spriteBatchFlush(); // flush remaining
// Low-level: write directly to an external mesh (for baking static geometry):
spriteBatchBufferToMesh(sprites, count, vertices, verticesSize);
spritebatchsprite_t:
typedef struct {
vec3 min, max; // 3D bounding box
vec2 uvMin, uvMax; // texture region
} spritebatchsprite_t;
The global SPRITEBATCH and its vertex backing array SPRITEBATCH_VERTICES[] are defined externally to the struct to satisfy alignment requirements on certain platforms.
Text (display/text/)
Text rendering uses FONT_DEFAULT (loaded during textInit()), which references a texture and a tileset. Characters start at ASCII ! (TEXT_CHAR_START).
textDraw(x, y, "Hello", COLOR_WHITE, &FONT_DEFAULT);
textMeasure("Hello", &FONT_DEFAULT, &outWidth, &outHeight);
// Single-char sprite for manual layout:
spritebatchsprite_t s = textGetSprite(pos, 'A', &FONT_DEFAULT);
font_t holds a texture_t * and a tileset_t * — both are owned by the asset system, not the font struct.