# GFX Peripheral (Graphics System) Domain: virtual hardware: graphics Function: normative Didactic companion: [`../learn/mental-model-gfx.md`](../runtime/learn/mental-model-gfx.md) ## 1. Overview The **GFX** peripheral is responsible for generating images in PROMETEU. `DEC-0030` defines the current logical render boundary. PROMETEU render production is split from render implementation: ```text domain buffers during logical frame -> RenderManager closes buffers -> RenderSubmission snapshot -> render surface / implementation consumes submission -> RGBA8888 surface publication ``` The GFX peripheral remains the classic local raster implementation for PROMETEU's 2D output. Logical render APIs do not target `Gfx` or the framebuffer directly. `RenderSubmission` is the closed snapshot passed across the render boundary. It MUST contain a frame id, the active app mode, and one typed packet: `Game2DFramePacket` for `AppMode::Game` or `ShellUiFramePacket` for `AppMode::Shell`. Once closed, producers MUST treat the submission as immutable. The runtime backpressure policy is latest-complete-submission-wins; it MUST NOT accumulate an unbounded frame queue. The current 2D graphics model is 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 render surface implementation maintains the concrete buffers needed to publish a frame. The classic software implementation uses: - **Back Buffer** — where the frame is built - **Front Buffer** — where the frame is displayed Per-frame flow: 1. Game or Shell code mutates mode-specific domain buffers during the logical frame. 2. `RenderManager` closes the active buffers into an immutable `RenderSubmission`. 3. The render surface implementation consumes the submission and rasterizes to its back buffer. 4. The render surface publishes the completed RGBA8888 surface. 5. The host displays the published surface. 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.pa` tile-bank payloads use a serialized representation distinct from runtime memory: - serialized pixels are `4bpp` packed in payload order - runtime memory may expand pixels to one `u8` palette index per pixel after decode ### 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_id` - `tile_size` - `tilemap` (large matrix) - `scroll_x`, `scroll_y` - `cache_origin_x`, `cache_origin_y` - `cache_tiles[w][h]` --- ## 6. Logical Tilemap The tilemap represents the world: Each cell contains: - `tile_id` - `flip_x` - `flip_y` - `priority` (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: 1. Calculate: - `tile_x = scroll_x / tile_size` - `tile_y = scroll_y / tile_size` - `offset_x = scroll_x % tile_size` - `offset_y = scroll_y % tile_size` 2. If `tile_x` changed: - Advance `cache_origin_x` - Reload only the new column 3. If `tile_y` changed: - 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_width` - `real_y = (cache_origin_y + logical_y) % cache_height` --- ## 10. Canonical Game Projection Game mode uses a typed Game 2D submission. `composer.*` owns high-level Game 2D frame composition: scene binding, camera, sprites, HUD, and frame orchestration. `gfx2d.*` owns Game 2D primitives only. Both domains are mutable while the logical frame is being produced and are closed by `RenderManager` into a `Game2DFramePacket`. For each Game 2D packet: 1. For each Tile Layer, in order: - Rasterize visible tiles from the cache - Apply scroll, flip, and transparency - Write to the render surface's working buffer 2. Draw sprites: - With priority between layers - Drawing order defines depth 3. Draw HUD layer last 4. Draw buffered `gfx2d.*` primitives according to the Game 2D packet contract. This section describes only the Game 2D packet rendering path. Shell/system UI uses `gfxui.*` and `ShellUiFramePacket`; it is never part of Game HUD or `composer.*`. --- ## 11. Drawing Order and Priority - There is no Z-buffer - There is no automatic sorting - Whoever draws later is in front Base order: 1. Tile Layer 0 2. Tile Layer 1 3. Tile Layer 2 4. Tile Layer 3 5. Sprites (by priority between layers) 6. HUD Layer 7. Buffered `gfx2d.*` Game primitives Normative boundary: - Items 1 through 6 belong to `composer.*` Game-frame composition. - Item 7 belongs to `gfx2d.*`. - `gfx2d.*` primitives MUST NOT be interpreted as scene, sprite, camera, HUD, or frame orchestration. - Shell/system UI belongs to `gfxui.*` and `ShellUiFramePacket`, not Game HUD. --- ## 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_NONE` - `BLEND_HALF` - `BLEND_HALF_PLUS` - `BLEND_HALF_MINUS` - `BLEND_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 `gfx2d.*` and `gfxui.*` primitives, the result is applied when the render surface consumes the closed packet for the active app mode - 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 - fade fields, fade syscalls, or fade packet members - 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. Transitions and Removed Fade Contract The previous special fade model is not part of the canonical render contract. Current render packets, syscalls, and ABI domains MUST NOT expose scene fade, HUD fade, fade levels, fade colors, or equivalent inherited fade state. Future visual transitions between Shell and Game belong to `RenderManager`. They are not owned by `composer.*`, `gfx2d.*`, `gfxui.*`, `Game2DFramePacket`, or `ShellUiFramePacket`. Rules: - render domain packets MUST NOT contain fade fields; - public render syscalls MUST NOT expose fade controls; - render implementations MUST NOT treat fade as an inherited post-processing habit; - transition work requires an explicit `RenderManager` contract or a later decision. --- ## 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..15` are 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_id` - `palette_id (u8)` - `flip_x` - `flip_y` Runtime-facing validity rule for v1: - `palette_id` values are valid only in the range `0..63` #### Sprite Each sprite draw contains: - `bank_id` - `tile_id` - `palette_id (u8)` - `x`, `y` - `flip_x`, `flip_y` - `priority` Runtime-facing validity rule for v1: - `palette_id` values are valid only in the range `0..63` --- ### 18.6. Color Resolution The pipeline works like this: 1. Read indexed pixel from tile (value 0..15) 2. Resolve: - real_color = palette[palette_id][index] 3. 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.pa` v1, 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 `u4` pixel indices - runtime may materialize the decoded bank as expanded `u8` pixel indices plus palette table --- ### 18.8. Metrics for Certification (CAP) The system can measure: - `palettes_loaded_total` - `palettes_referenced_this_frame` - `tiles_drawn_by_palette_id` - `sprites_drawn_by_palette_id` --- ## 19. Syscall Return and Fault Policy Graphics-related public ABI in v1 is split between: - `composer.*` for Game 2D high-level frame composition; - `gfx2d.*` for Game 2D primitives only; - `gfxui.*` for Shell UI primitives only. `composer.*` and `gfx2d.*` are available to `AppMode::Game`. `gfxui.*` is available to `AppMode::Shell`. Renderers and capabilities may differ by app mode. 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 | | ----------------------- | ------------- | --------------------------------------------------- | | `gfx2d.clear` | `void` | no real operational failure path in v1 | | `gfx2d.fill_rect` | `void` | no real operational failure path in v1 | | `gfx2d.draw_line` | `void` | no real operational failure path in v1 | | `gfx2d.draw_circle` | `void` | no real operational failure path in v1 | | `gfx2d.draw_disc` | `void` | no real operational failure path in v1 | | `gfx2d.draw_square` | `void` | no real operational failure path in v1 | | `gfx2d.draw_text` | `void` | no real operational failure path in v1 | | `gfxui.*` primitives | `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 Primitive domain semantics The primitive domains have stable operational meaning: - `gfx2d.*` is Game 2D primitive command buffering; - `gfx2d.*` is screen-space and primitive-only relative to `composer.*`; - `gfx2d.*` is outside scene, camera, sprites, HUD, and Game 2D frame orchestration; - `gfxui.*` is Shell UI primitive command buffering; - `gfxui.*` does not contain widget or layout policy. Callers MUST NOT rely on stable immediate writes to the working back buffer as the public contract for primitive drawing. Primitive calls mutate domain command buffers that close into the active typed submission. ### 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:int` case; - 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: 1. `glyph_id: int` — glyph index within the bank 2. `palette_id: int` — palette index 3. `x: int` — x coordinate 4. `y: int` — y coordinate 5. `layer: int` — composition layer reference 6. `bank_id: int` — glyph bank index 7. `flip_x: bool` — horizontal flip 8. `flip_y: bool` — vertical flip 9. `priority: int` — within-layer ordering priority Minimum status table: - `0` = `OK` - `1` = `SCENE_UNAVAILABLE` - `2` = `INVALID_ARG_RANGE` - `3` = `BANK_INVALID` - `4` = `LAYER_INVALID` - `5` = `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 `active` flag exists in the v1 canonical ABI; - overflow remains non-fatal and must not escalate to trap in v1.