# GFX Peripheral (Graphics System) Domain: virtual hardware: graphics Function: normative Didactic companion: [`../learn/gfx-mental-model.md`](../learn/gfx-mental-model.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 - **320 × 180 pixels** - aspect ratio close to 16:9 - scalable by the host (nearest-neighbor) ### Pixel format - **RGB565** - 5 bits Red - 6 bits Green - 5 bits Blue - no alpha channel Transparency is handled via **color key**. --- ## 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: 1. The system draws to the back buffer 2. Calls `present()` 3. Buffers are swapped 4. 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 ### 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: 320×180 - 16×16 tiles → 20×12 visible - Cache: 22×14 (1-tile margin) 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. Projection to the Back Buffer For each frame: 1. For each Tile Layer, in order: - Rasterize visible tiles from the cache - Apply scroll, flip, and transparency - Write to the back buffer 2. Draw sprites: - With priority between layers - Drawing order defines depth 3. Draw HUD layer last --- ## 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 --- ## 12. Transparency (Color Key) - One RGB565 value is reserved as TRANSPARENT_KEY - Pixels with this color are not drawn ``` if src == TRANSPARENT_KEY: skip else: draw ``` --- ## 13. Color Math (Discrete Blending) Inspired by the SNES. Official modes: - `BLEND_NONE` - `BLEND_HALF` - `BLEND_HALF_PLUS` - `BLEND_HALF_MINUS` - `BLEND_FULL` No continuous alpha. No arbitrary blending. Everything is: - integer - cheap - deterministic --- ## 14. Where Blend is Applied - Blending occurs during drawing - The result goes directly to the back buffer - There is no automatic post-composition --- ## 15. What the GFX DOES NOT support By design: - Continuous alpha - RGBA framebuffer - Shaders - Modern GPU pipeline - HDR - Gamma correction --- ## 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) The fade is implemented without continuous per-pixel alpha and without floats. It uses a **discrete integer level** (0..31), which in practice produces an "almost continuous" visual result in 320×180 pixel art. --- ### 17.1 Fade Representation Each fade is represented by: - `fade_level: u8` in the range **[0..31]** - `0` → fully replaced by the fade color - `31` → fully visible (no fade) - `fade_color: RGB565` - color the image will be blended into Registers: - `SCENE_FADE_LEVEL` (0..31) - `SCENE_FADE_COLOR` (RGB565) - `HUD_FADE_LEVEL` (0..31) - `HUD_FADE_COLOR` (RGB565) Common cases: - Fade-out: `fade_color = BLACK` - Flash/teleport: `fade_color = WHITE` - Special effects: any RGB565 color --- ### 17.2 Fade Operation (Blending with Arbitrary Color) For each RGB565 pixel `src` and fade color `fc`, the final pixel `dst` is calculated per channel. 1) Extract components: - `src_r5`, `src_g6`, `src_b5` - `fc_r5`, `fc_g6`, `fc_b5` 2) Apply integer blending: ``` src_weight = fade_level // 0..31 fc_weight = 31 - fade_level r5 = (src_r5 * src_weight + fc_r5 * fc_weight) / 31 g6 = (src_g6 * src_weight + fc_g6 * fc_weight) / 31 b5 = (src_b5 * src_weight + fc_b5 * fc_weight) / 31 ``` - `src_r5`, `src_g6`, `src_b5` - `fc_r5`, `fc_g6`, `fc_b5` 3) Repack: ``` dst = pack_rgb565(r5, g6, b5) ``` Notes: - Deterministic operation - Integers only - Can be optimized via LUT --- ### 17.3 Order of Application in the Frame The frame composition follows this order: 1. Rasterize **Tile Layers 0–3** → Back Buffer 2. Rasterize **Sprites** according to priority 3. (Optional) Extra pipeline (Emission/Light/Glow etc.) 4. Apply **Scene Fade** using: - `SCENE_FADE_LEVEL` - `SCENE_FADE_COLOR` 5. Rasterize **HUD Layer** 6. Apply **HUD Fade** using: - `HUD_FADE_LEVEL` - `HUD_FADE_COLOR` 7. `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: - Index `0` = TRANSPARENT - Indices `1..15` = valid palette colors --- ### 18.3. Palette Structure Each **Tile Bank** contains: - Up to **256 palettes** - Each palette has: - **16 colors** - each color in **RGB565 (u16)** Size: - 1 palette = 16 × 2 bytes = **32 bytes** - 256 palettes = **8 KB per bank** - 16 banks = **128 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` #### Sprite Each sprite draw contains: - `bank_id` - `tile_id` - `palette_id (u8)` - `x`, `y` - `flip_x`, `flip_y` - `priority` --- ### 18.6. Color Resolution The pipeline works like this: 1. Read indexed pixel from tile (value 0..15) 2. If index == 0 → transparent pixel 3. Otherwise: - real_color = palette[palette_id][index] 4. Apply: - flip - discrete blend - writing to back buffer In other words: ``` pixel_index = tile_pixel(x,y) if pixel_index == 0: skip else: color = bank.palettes[palette_id][pixel_index] draw(color) ``` --- ### 18.7. Organization of Tile Banks Tile Banks are "strong assets": - Tiles and palettes live together - Export/import always carries: - tiles + palettes - 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. --- ### 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`