599 lines
13 KiB
Markdown
599 lines
13 KiB
Markdown
# 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.
|
||
|
||
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
|
||
- `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: 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:
|
||
|
||
- **64 palettes** in runtime-facing v1
|
||
- Each palette has:
|
||
- **16 colors**
|
||
- each color in **RGB565 (`u16`, little-endian in serialized assets)**
|
||
|
||
Size:
|
||
|
||
- 1 palette = 16 × 2 bytes = **32 bytes**
|
||
- 64 palettes = **2 KB per bank**
|
||
- 16 banks = **32 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. 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
|
||
- 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:
|
||
|
||
- `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 |
|
||
| `gfx.clear_565` | `void` | no real operational failure path in v1 |
|
||
| `composer.bind_scene` | `status:int` | explicit orchestration-domain operational result |
|
||
| `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.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.
|