353 lines
14 KiB
Markdown
353 lines
14 KiB
Markdown
# Display System
|
||
|
||
Source: `src/dusk/display/`
|
||
|
||
The display system is the rendering pipeline. It is abstracted across platforms via `displayplatform.h` — see [architecture.md](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](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`)
|
||
|
||
```c
|
||
/* 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`: one `uint8_t` per pixel (0–255), row-major
|
||
- `palette`: exactly **256** `color_t` RGBA 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.
|
||
|
||
```c
|
||
/* 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:
|
||
|
||
```c
|
||
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:
|
||
```c
|
||
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).
|
||
|
||
```c
|
||
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).
|
||
|
||
```c
|
||
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 coordinates
|
||
- `float_t pos[3]` — position
|
||
- Optionally `color_t color` if `MESH_ENABLE_COLOR` is 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).
|
||
|
||
```c
|
||
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`:
|
||
|
||
```c
|
||
shadermaterial_t mat = {
|
||
.unlit = {
|
||
.color = COLOR_WHITE,
|
||
.texture = &myTexture, // NULL for solid color
|
||
}
|
||
};
|
||
shaderSetMaterial(&SHADER_UNLIT, &mat);
|
||
```
|
||
|
||
---
|
||
|
||
## Texture (`display/texture/`)
|
||
|
||
```c
|
||
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:
|
||
```c
|
||
// 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 white
|
||
- `TEXTURE_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.
|
||
|
||
```c
|
||
// 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`:
|
||
```c
|
||
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`).
|
||
|
||
```c
|
||
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.
|