16 KiB
GFX Peripheral (Graphics System)
Domain: virtual hardware: graphics Function: normative
Didactic companion: ../learn/mental-model-gfx.md
1. Overview
The GFX peripheral is responsible for generating images in PROMETEU.
It is an explicit 2D graphics device based on:
- framebuffer
- tilemaps
- tile banks
- priority-based sprites
- composition by drawing order
2. Resolution and Framebuffer
Base resolution
- 480 × 270 pixels
- aspect ratio close to 16:9
- scalable by the host (nearest-neighbor)
Pixel format
- RGBA8888
- 8 bits Red
- 8 bits Green
- 8 bits Blue
- 8 bits Alpha
- canonical raw channel order: RGBA
The framebuffer alpha channel may carry meaningful runtime output. The runtime
MUST NOT force the published front buffer alpha channel to 255 as a blanket
contract.
Color values in the runtime, HAL, host-facing framebuffer, and GFX ABI are logical RGBA8888 values. RGB565 is not a supported runtime framebuffer, palette, host presentation, or compatibility contract.
3. Double Buffering
The GFX maintains two buffers:
- Back Buffer — where the frame is built
- Front Buffer — where the frame is displayed
Per-frame flow:
- The system prepares the logical frame
- Canonical game composition is rendered into the back buffer
- Deferred final overlay/debug primitives are drained on top of the completed game frame
- Calls
present() - Buffers are swapped
- The host displays the front buffer
This guarantees:
- no tearing
- clear per-frame synchronization
- deterministic behavior
4. PROMETEU Graphical Structure
The graphical world is composed of:
- Up to 16 Tile Banks
- 4 Tile Layers (scrollable)
- 1 HUD Layer (fixed, always on top)
- Sprites with priority between layers
4.1 Tile Banks
- There are up to 16 banks
- Each bank has a fixed tile size:
- 8×8, 16×16, or 32×32
- A bank is a graphics library:
- environment
- characters
- UI
- effects
assets.patile-bank payloads use a serialized representation distinct from runtime memory:- serialized pixels are
4bpppacked in payload order - runtime memory may expand pixels to one
u8palette index per pixel after decode
- serialized pixels are
4.2 Layers
- There are:
- 4 Tile Layers
- 1 HUD Layer
- Each layer points to a single bank
- Sprites can use any bank
- HUD:
- does not scroll
- maximum priority
- generally uses 8×8 tiles
5. Internal Model of a Tile Layer
A Tile Layer is not a bitmap of pixels.
It is composed of:
- A logical Tilemap (tile indices)
- A Border Cache (window of visible tiles)
- A Scroll Offset
Structure:
bank_idtile_sizetilemap(large matrix)scroll_x,scroll_ycache_origin_x,cache_origin_ycache_tiles[w][h]
6. Logical Tilemap
The tilemap represents the world:
Each cell contains:
tile_idflip_xflip_ypriority(optional)palette_id(optional)
The tilemap can be much larger than the screen.
7. Border Cache (Tile Cache)
The cache is a window of tiles around the camera.
Example:
- Screen: 480×270
- 16×16 tiles → approximately 30×17 visible
- Current runtime cache sizing with 16×16 layers: 35×21 tiles
It stores tiles already resolved from the tilemap.
8. Cache Update
Every frame:
- Calculate:
tile_x = scroll_x / tile_sizetile_y = scroll_y / tile_sizeoffset_x = scroll_x % tile_sizeoffset_y = scroll_y % tile_size
- If
tile_xchanged:
- Advance
cache_origin_x - Reload only the new column
- If
tile_ychanged:
- Advance
cache_origin_y - Reload only the new line
Only one row and/or column is updated per frame.
9. Cache as Ring Buffer
The cache is circular:
- Does not physically move data
- Only moves logical indices
Access:
real_x = (cache_origin_x + logical_x) % cache_widthreal_y = (cache_origin_y + logical_y) % cache_height
10. Canonical Game Projection to the Back Buffer
For each frame:
- For each Tile Layer, in order:
- Rasterize visible tiles from the cache
- Apply scroll, flip, and transparency
- Write to the back buffer
- Draw sprites:
- With priority between layers
- Drawing order defines depth
- Draw HUD layer last
This section describes only the canonical game composition path.
gfx.* primitives such as draw_text, draw_line, and draw_disc are not part of this canonical game projection order. In v1 they belong to a deferred final overlay/debug stage that is drained after canonical game composition is complete.
11. Drawing Order and Priority
- There is no Z-buffer
- There is no automatic sorting
- Whoever draws later is in front
Base order:
- Tile Layer 0
- Tile Layer 1
- Tile Layer 2
- Tile Layer 3
- Sprites (by priority between layers)
- HUD Layer
- Scene Fade
- HUD Fade
- Deferred
gfx.*overlay/debug primitives
Normative boundary:
- Items 1 through 8 belong to canonical game-frame composition.
- Item 9 is a separate overlay/debug stage.
- Deferred
gfx.*primitives MUST NOT be interpreted as scene, sprite, or canonical HUD content.
12. Transparency
Transparency is represented by the alpha channel of the resolved RGBA8888 color.
Palette indices are ordinary indices. The runtime MUST NOT reserve palette
index 0 as a special transparent index. A palette entry may still be authored
as transparent by setting its alpha channel to 0, but that is ordinary palette
data rather than a special index rule.
if resolved_color.alpha == 0:
skip
else:
draw_or_blend(resolved_color)
13. Color Math (Discrete Blending)
Inspired by the SNES.
Official modes:
BLEND_NONEBLEND_HALFBLEND_HALF_PLUSBLEND_HALF_MINUSBLEND_FULL
Alpha is available through RGBA8888 colors. Discrete blend modes remain part of the classic GFX contract, and later optimization work may specialize opaque, masked, and alpha paths.
Everything is:
- integer
- cheap
- deterministic
14. Where Blend is Applied
- Blending occurs during drawing
- For canonical game composition, the result goes to the back buffer during composition
- For deferred
gfx.*overlay/debug primitives, the result is applied during the final overlay/debug drain stage - There is no automatic GPU-style post-processing pipeline
15. What the GFX DOES NOT support
By design:
- Shaders
- Modern GPU pipeline
- HDR
- Gamma correction
- RGB565 compatibility framebuffers
- multi-format backend selection
- render-thread ownership as part of this contract
16. Performance Rule
- Layers:
- only update the border when crossing a tile
- never redraw the entire world
- Rasterization:
- always per frame, only the visible area
- Sprites:
- always redrawn per frame
17. Special PostFX — Fade (Scene and HUD)
PROMETEU supports gradual fade as a special PostFX, with two independent controls:
- Scene Fade: affects the entire scene (Tile Layers 0–3 + Sprites)
- HUD Fade: affects only the HUD Layer (always composed last)
In v1, deferred gfx.* overlay/debug primitives are drained after both fades and therefore are not themselves part of scene or HUD fade application.
The fade uses a discrete integer level (0..31), which in practice produces an
"almost continuous" visual result in Prometeu's 480x270 pixel-art framebuffer.
17.1 Fade Representation
Each fade is represented by:
fade_level: u8in the range [0..31]0→ fully replaced by the fade color31→ fully visible (no fade)
fade_color: RGBA8888- color the image will be blended into
Registers:
SCENE_FADE_LEVEL(0..31)SCENE_FADE_COLOR(RGBA8888)HUD_FADE_LEVEL(0..31)HUD_FADE_COLOR(RGBA8888)
Common cases:
- Fade-out:
fade_color = BLACK - Flash/teleport:
fade_color = WHITE - Special effects: any RGBA8888 color
17.2 Fade Operation (Blending with Arbitrary Color)
For each RGBA8888 pixel src and fade color fc, the final pixel dst is calculated per channel.
- Extract components:
src_r8,src_g8,src_b8,src_a8fc_r8,fc_g8,fc_b8,fc_a8
- Apply integer blending:
src_weight = fade_level // 0..31
fc_weight = 31 - fade_level
r8 = (src_r8 * src_weight + fc_r8 * fc_weight) / 31
g8 = (src_g8 * src_weight + fc_g8 * fc_weight) / 31
b8 = (src_b8 * src_weight + fc_b8 * fc_weight) / 31
a8 = (src_a8 * src_weight + fc_a8 * fc_weight) / 31
src_r8,src_g8,src_b8,src_a8fc_r8,fc_g8,fc_b8,fc_a8
- Repack:
dst = pack_rgba8888_rgba(r8, g8, b8, a8)
Notes:
- Deterministic operation
- Integers only
- Can be optimized via LUT
17.3 Order of Application in the Frame
The frame composition follows this order:
- Rasterize Tile Layers 0–3 → Back Buffer
- Rasterize Sprites according to priority
- (Optional) Extra pipeline (Emission/Light/Glow etc.)
- Apply Scene Fade using:
SCENE_FADE_LEVELSCENE_FADE_COLOR
- Rasterize HUD Layer
- Apply HUD Fade using:
HUD_FADE_LEVELHUD_FADE_COLOR
present()
Rules:
- Scene Fade never affects the HUD
- HUD Fade never affects the scene
18. Palette System
18.1. Overview
PROMETEU uses exclusively palette-indexed graphics.
There is no direct RGB-per-pixel mode.
Every graphical pixel is an index pointing to a real color in a palette.
18.2. Pixel Format
Each pixel of a tile or sprite is:
- 4 bits per pixel (4bpp)
- values:
0..15
Fixed rule:
- Indices
0..15are ordinary valid palette indices - Transparency comes from the alpha channel of the resolved palette entry
18.3. Palette Structure
Each Tile Bank contains:
- 64 palettes in runtime-facing v1
- Each palette has:
- 16 colors
- each color in RGBA8888 with canonical RGBA channel order
Size:
- 1 palette = 16 × 4 bytes = 64 bytes
- 64 palettes = 4 KB per bank
- 16 banks = 64 KB maximum palettes
18.4. Palette Association
Fundamental Rule
- Each tile uses a single palette
- Each sprite uses a single palette
- The palette must be provided explicitly in every draw
There is no palette swap within the same tile or sprite.
18.5. Where the Palette is Defined
Tilemap
Each tilemap cell contains:
tile_idpalette_id (u8)flip_xflip_y
Runtime-facing validity rule for v1:
palette_idvalues are valid only in the range0..63
Sprite
Each sprite draw contains:
bank_idtile_idpalette_id (u8)x,yflip_x,flip_ypriority
Runtime-facing validity rule for v1:
palette_idvalues are valid only in the range0..63
18.6. Color Resolution
The pipeline works like this:
- Read indexed pixel from tile (value 0..15)
- Resolve:
- real_color = palette[palette_id][index]
- Apply:
- flip
- discrete blend
- alpha/skip behavior from the resolved RGBA8888 color
- writing to back buffer
In other words:
pixel_index = tile_pixel(x,y)
color = bank.palettes[palette_id][pixel_index]
if color.alpha == 0:
skip
else:
draw_or_blend(color)
18.7. Organization of Tile Banks
Tile Banks are "strong assets":
- Tiles and palettes live together
- Export/import always carries:
- tiles + palettes
- In
assets.pav1, the serialized payload is:- packed indexed pixels for the whole sheet
- followed by the palette table for the same bank
- The hardware does not impose semantic organization:
- grouping is the creator's decision
- Tooling and scripts can create conventions:
- e.g.: palettes 0..15 = enemies
- 16..31 = scenery
- etc.
Runtime-facing v1 baseline:
- sheet pixels are authored and resolved as indexed values
0..15 - serialized tile-bank payload uses packed
u4pixel indices - runtime may materialize the decoded bank as expanded
u8pixel indices plus palette table
18.8. Metrics for Certification (CAP)
The system can measure:
palettes_loaded_totalpalettes_referenced_this_frametiles_drawn_by_palette_idsprites_drawn_by_palette_id
19. Syscall Return and Fault Policy
Graphics-related public ABI in v1 is split between:
gfx.*for direct drawing/backend-oriented operations;composer.*for frame orchestration operations.
Only operations with real operational rejection paths return explicit status values.
Fault boundary:
Trap: structural ABI misuse (type/arity/capability/shape mismatch);status: operational failure;Panic: internal runtime invariant break only.
19.1 Return-shape matrix in v1
| Syscall | Return | Policy basis |
|---|---|---|
gfx.clear |
void |
no real operational failure path in v1 |
gfx.fill_rect |
void |
no real operational failure path in v1 |
gfx.draw_line |
void |
no real operational failure path in v1 |
gfx.draw_circle |
void |
no real operational failure path in v1 |
gfx.draw_disc |
void |
no real operational failure path in v1 |
gfx.draw_square |
void |
no real operational failure path in v1 |
gfx.draw_text |
void |
no real operational failure path in v1 |
composer.bind_scene |
status:int |
status-returning API, but missing scene glyph dependencies are fatal runtime errors |
composer.unbind_scene |
status:int |
explicit orchestration-domain operational result |
composer.set_camera |
void |
no real operational failure path in v1 |
composer.emit_sprite |
status:int |
explicit orchestration-domain operational rejection |
19.1.a Deferred overlay/debug semantics for gfx.*
The public gfx.* primitive family remains valid in v1, but its stable operational meaning is:
- deferred final overlay/debug composition;
- screen-space and pipeline-agnostic relative to
composer.*; - outside
FrameComposer; - above scene, sprites, and canonical HUD;
- drained after
hud_fade.
This means callers MUST NOT rely on stable immediate writes to the working back buffer as the public contract for gfx.draw_text(...) or sibling primitives.
19.1.b Scene dependency fatal boundary
composer.bind_scene remains a status-returning syscall in the public ABI, but scene glyph dependency absence is outside the accepted passive operational-error model.
Rules:
- the target scene slot may still be rejected through ordinary status-returning behavior when the scene itself is unavailable;
- missing glyph dependencies referenced by a resident scene are not a passive
status:intcase; - if scene activation discovers that a layer dependency cannot be resolved to a committed glyph asset, the machine MUST fail fatally and emit a clear log;
- if scene composition later discovers that a layer dependency can no longer be resolved, the machine MUST fail fatally and emit a clear log;
- runtime MUST NOT continue canonical scene composition after such a dependency failure.
19.2 composer.emit_sprite
composer.emit_sprite returns status:int.
ABI:
glyph_id: int— glyph index within the bankpalette_id: int— palette indexx: int— x coordinatey: int— y coordinatelayer: int— composition layer referencebank_id: int— glyph bank indexflip_x: bool— horizontal flipflip_y: bool— vertical flippriority: int— within-layer ordering priority
Minimum status table:
0=OK1=SCENE_UNAVAILABLE2=INVALID_ARG_RANGE3=BANK_INVALID4=LAYER_INVALID5=SPRITE_OVERFLOW
Operational notes:
- the canonical public sprite contract is frame-emission based;
- no caller-provided sprite index exists in the v1 canonical ABI;
- no
activeflag exists in the v1 canonical ABI; - overflow remains non-fatal and must not escalate to trap in v1.