prometeu-runtime/docs/runtime/specs/04-gfx-peripheral.md

562 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GFX Peripheral (Graphics System)
Domain: virtual hardware: graphics
Function: normative
Didactic companion: [`../learn/mental-model-gfx.md`](../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
- **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 03 + 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 03** → 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`
---
## 19. Syscall Return and Fault Policy
`gfx` follows status-first policy for operations with operational failure modes.
Fault boundary:
- `Trap`: structural ABI misuse (type/arity/capability/shape mismatch);
- `status`: operational failure;
- `Panic`: internal runtime invariant break only.
### 19.1 `gfx.set_sprite`
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.set_sprite` | `status:int` | operational rejection must be explicit |
| `gfx.draw_text` | `void` | no real operational failure path in v1 |
| `gfx.clear_565` | `void` | no real operational failure path in v1 |
Only `gfx.set_sprite` is status-returning in v1.
All other `gfx` syscalls remain `void` unless a future domain revision introduces a real operational failure path.
### 19.2 `gfx.set_sprite`
`gfx.set_sprite` returns `status:int`.
Minimum status table:
- `0` = `OK`
- `1` = `ASSET_NOT_FOUND`
- `2` = `INVALID_SPRITE_INDEX`
- `3` = `INVALID_ARG_RANGE`
Operational notes:
- no fallback to default bank when the sprite asset name cannot be resolved;
- no silent no-op for invalid index/range;
- `palette_id` and `priority` must be validated against runtime-supported ranges.