prometeu-runtime/docs/specs/runtime/04-gfx-peripheral.md
bQUARKz 9cf9fa6163
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
implements PLN-0109
2026-06-15 10:47:34 +01:00

22 KiB
Raw Blame History

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.

DEC-0030 defines the current logical render boundary. PROMETEU render production is split from render implementation:

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 platform layer is the runtime-facing implementation boundary for render handoff. Runtime and firmware code publish completed frames through typed platform services such as RenderSubmissionSink, and they access Game 2D composition through Game2DFrameComposer. They MUST NOT depend on a monolithic hardware bridge, mutable Hardware, mutable Gfx, or live FrameComposer reference as the render handoff contract.

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. Asynchronous Render Boundary

DEC-0031 defines the asynchronous render boundary. The contract applies even when the current implementation consumes submissions synchronously.

4.1 Complete packet contract

RenderSubmissionPacket::Game2D MUST be a complete frame description for the Game 2D render consumer. The consumer MUST NOT bypass the packet by consulting live VM state, live FrameComposer state, or mutable runtime state.

For Game 2D, the required composition order is:

composer scene/layers/sprites/HUD
-> buffered gfx2d primitives
-> publication/present

gfx2d.* primitives are therefore a final Game 2D overlay. This is true for both active-scene frames and no-scene frames.

4.2 Resource boundary

Submissions MUST remain small and owned. Heavy resident resources such as glyph banks, scene banks, asset payloads, and viewport cache materializations MUST NOT be copied into each submission.

Submissions SHALL carry stable resource IDs or handles. The render consumer may resolve those IDs through read-only resource APIs. Resource installation, resolver updates, viewport cache refreshes, and bank residency changes belong to the logical/runtime side before handoff, or to an owning service. They MUST NOT require the render consumer to hold mutable VM, Hardware, Gfx, or FrameComposer references.

Local host implementations may keep a concrete hardware object internally as a platform implementation detail. That object is not part of the runtime-facing render boundary; the boundary is the typed platform service set.

The runtime does not guarantee visual integrity if a developer or framework replaces resources behind an in-flight submission in a way that violates asset discipline.

4.3 Handoff

The first asynchronous Game render model uses a single pending slot with latest-wins semantics:

  • publishing a new submission replaces the previous pending submission;
  • replacement before consumption is counted as a render drop;
  • the render consumer takes ownership of the pending submission;
  • the VM/producer MUST NOT block on render consumption, raster completion, or present completion.

The consumer status is telemetry. It is not a semantic ACK to the VM.

4.4 Frame pacing

Game logical frames are paced by the runtime frame scheduler, not by render worker ACK. The intended cadence is one Game logical frame per frame tick and at most one pending submission ahead of the consumer.

FRAME_SYNC remains the canonical end of a VM logical frame. Cycle/time budget is evidence for certification, watchdog, diagnostics, and overrun reporting; it MUST NOT be used as the normal mechanism for cutting a logical frame short.

If a Game logical frame overruns the display cadence, logical frames remain sequential. The render consumer may repeat the last valid frame, and telemetry records the overrun/repeat. The VM MUST NOT produce catch-up frames to skip from frame N to frame N+k.

4.5 AppMode policy

Render execution policy is explicit by pipeline/AppMode:

  • AppMode::Game is frame-paced and may use a render worker when the host/runtime supports it, with local synchronous fallback.
  • AppMode::Shell is lifecycle-driven and local/synchronous by default.
  • Shell VM-backed apps follow Shell lifecycle; they do not declare an independent frame-paced game workload under this contract.
  • Splash, crash, and hub/system screens follow Shell/local policy unless a later decision defines a more specific policy.

4.6 Ownership and epoch

Every submission that may cross an asynchronous boundary MUST carry render ownership metadata: at minimum frame identity, app mode, app identity where available, and render epoch/generation.

Foreground visual-owner transitions MUST advance the active render epoch or equivalent generation through a central runtime render manager. The render consumer MUST check ownership before present and MUST discard stale submissions whose ownership no longer matches the active owner.

Transitions that invalidate stale render work include:

  • Game to Shell/Hub;
  • Shell/Hub to Game;
  • crash screen takeover;
  • splash or system screen takeover;
  • cartridge or app swap, even if AppMode remains unchanged;
  • shutdown/stop.

The same physical surface may be reused by multiple visual owners, but logical ownership MUST remain explicit.

Game pause/resume, foreground stack behavior, and coexistence of a paused Game with VM-backed Shell apps are outside this contract and are tracked by the foreground/lifecycle discussion.

4.7 Render telemetry

Asynchronous render is best-effort observable, not a VM-visible handshake. Render drops, stale epoch discards, repeated presents, render errors, and present errors MUST be recorded for host diagnostics, debugging, profiling, and certification evidence. VM program semantics MUST NOT depend on whether a submission was consumed or presented.

Minimum render telemetry includes:

  • produced submissions;
  • replaced-before-consume submissions;
  • consumed submissions;
  • presented frames;
  • repeated presents;
  • render errors;
  • present errors;
  • stale epoch discards;
  • shutdown discards;
  • last produced, consumed, presented, dropped, and error frame IDs;
  • active render epoch.

5. 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

5.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

5.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

6. 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]

7. 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.


8. 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.


9. 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
  1. If tile_x changed:
  • Advance cache_origin_x
  • Reload only the new column
  1. If tile_y changed:
  • Advance cache_origin_y
  • Reload only the new line

Only one row and/or column is updated per frame.


10. 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

11. 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
  1. Draw sprites:
  • With priority between layers
  • Drawing order defines depth
  1. Draw HUD layer last

  2. 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.*.


12. 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.

13. 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)

14. 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

15. 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

16. 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

17. 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

18. 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.

19. Palette System

19.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.


19.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

19.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

19.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.


19.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

19.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]
  1. 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)

19.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

19.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

20. 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.

20.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

20.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.

20.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.

20.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.