Compare commits

...

10 Commits

Author SHA1 Message Date
a07104aad1
refresh rgba8888 stress cartridge artifacts
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
2026-05-23 22:18:18 +01:00
d9c208ad4f
housekeep DSC-0037 2026-05-23 22:17:40 +01:00
f9a86fafb0
implements PLN-0072 2026-05-23 19:26:09 +01:00
537096057a
implements PLN-0071 2026-05-23 19:24:07 +01:00
f273745589
implements PLN-0070 2026-05-23 19:23:07 +01:00
c4c2dfb017
implements PLN-0069 2026-05-23 19:21:02 +01:00
668a061964
implements PLN-0068 2026-05-23 19:19:28 +01:00
1256d33a72
implements PLN-0067 2026-05-23 19:15:14 +01:00
4d5818c244
RGBA8888 Framebuffer and Pixel Format Direction 2026-05-23 19:12:53 +01:00
88ac752d9e
moved app_mode to its own file 2026-05-22 09:07:10 +01:00
43 changed files with 375 additions and 1123 deletions

View File

@ -64,7 +64,7 @@ impl GlyphAssetSlotIndex {
const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
const GLYPH_BANK_PALETTE_BYTES_V1: usize =
GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * size_of::<u16>();
GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * size_of::<u32>();
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
#[derive(Debug)]
@ -655,10 +655,13 @@ impl AssetManager {
[[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1];
for (p, pal) in palettes.iter_mut().enumerate() {
for (c, slot) in pal.iter_mut().enumerate() {
let offset = (p * 16 + c) * 2;
let color_raw =
u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
*slot = Color(color_raw);
let offset = (p * 16 + c) * 4;
*slot = Color::rgba(
palette_data[offset],
palette_data[offset + 1],
palette_data[offset + 2],
palette_data[offset + 3],
);
}
}
@ -687,10 +690,13 @@ impl AssetManager {
[[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1];
for (p, pal) in palettes.iter_mut().enumerate() {
for (c, slot) in pal.iter_mut().enumerate() {
let offset = (p * 16 + c) * 2;
let color_raw =
u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
*slot = Color(color_raw);
let offset = (p * 16 + c) * 4;
*slot = Color::rgba(
palette_data[offset],
palette_data[offset + 1],
palette_data[offset + 2],
palette_data[offset + 3],
);
}
}
@ -1306,14 +1312,13 @@ mod tests {
let entry = test_glyph_asset_entry("glyphs", 2, 2);
let mut data = vec![0x10, 0x23];
data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_BYTES_V1]);
data[2] = 0x34;
data[3] = 0x12;
data[2..6].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
let bank =
AssetManager::decode_glyph_bank_from_buffer(&entry, &data).expect("glyph decode");
assert_eq!(bank.pixel_indices, vec![1, 0, 2, 3]);
assert_eq!(bank.palettes[0][0], Color(0x1234));
assert_eq!(bank.palettes[0][0], Color::from_raw(0x12345678));
}
#[test]

View File

@ -40,9 +40,9 @@ pub struct Gfx {
/// Height of the internal framebuffer in pixels.
h: usize,
/// Front buffer: the "VRAM" currently being displayed by the Host window.
front: Vec<u16>,
front: Vec<u32>,
/// Back buffer: the working buffer where canonical game frames are composed
back: Vec<u16>,
back: Vec<u32>,
/// Shared access to graphical memory banks (tiles and palettes).
pub glyph_banks: Arc<dyn GlyphBankPoolAccess>,
@ -67,7 +67,7 @@ pub struct Gfx {
// const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7];
struct RenderTarget<'a> {
back: &'a mut [u16],
back: &'a mut [u32],
screen_w: usize,
screen_h: usize,
}
@ -206,7 +206,7 @@ impl GfxBridge for Gfx {
fn size(&self) -> (usize, usize) {
self.size()
}
fn front_buffer(&self) -> &[u16] {
fn front_buffer(&self) -> &[u32] {
self.front_buffer()
}
fn clear(&mut self, color: Color) {
@ -375,13 +375,13 @@ impl Gfx {
(self.w, self.h)
}
/// The buffer that the host should display (RGB565).
pub fn front_buffer(&self) -> &[u16] {
/// The buffer that the host should display (RGBA8888 in RGBA raw order).
pub fn front_buffer(&self) -> &[u32] {
&self.front
}
pub fn clear(&mut self, color: Color) {
self.back.fill(color.0);
self.back.fill(color.raw());
}
/// Rectangle with blend mode.
@ -394,7 +394,7 @@ impl Gfx {
color: Color,
mode: BlendMode,
) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
@ -406,14 +406,14 @@ impl Gfx {
let x1 = (x + w).clamp(0, fw);
let y1 = (y + h).clamp(0, fh);
let src = color.0;
let src = color.raw();
for yy in y0..y1 {
let row = (yy as usize) * self.w;
for xx in x0..x1 {
let idx = row + (xx as usize);
let dst = self.back[idx];
self.back[idx] = blend_rgb565(dst, src, mode);
self.back[idx] = blend_rgba8888(dst, src, mode);
}
}
}
@ -425,17 +425,17 @@ impl Gfx {
/// Draws a single pixel.
pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
if x >= 0 && x < self.w as i32 && y >= 0 && y < self.h as i32 {
self.back[y as usize * self.w + x as usize] = color.0;
self.back[y as usize * self.w + x as usize] = color.raw();
}
}
/// Draws a line between two points using Bresenham's algorithm.
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
@ -467,7 +467,7 @@ impl Gfx {
/// Draws a circle outline using Midpoint Circle Algorithm.
pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
@ -503,7 +503,7 @@ impl Gfx {
/// Draws a filled circle.
pub fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
@ -541,7 +541,7 @@ impl Gfx {
/// Draws a rectangle outline.
pub fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
@ -569,7 +569,7 @@ impl Gfx {
}
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
@ -582,12 +582,12 @@ impl Gfx {
return;
}
for x in start..=end {
self.back[y as usize * self.w + x as usize] = color.0;
self.back[y as usize * self.w + x as usize] = color.raw();
}
}
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
@ -600,7 +600,7 @@ impl Gfx {
return;
}
for y in start..=end {
self.back[y as usize * self.w + x as usize] = color.0;
self.back[y as usize * self.w + x as usize] = color.raw();
}
}
@ -703,7 +703,7 @@ impl Gfx {
}
fn draw_cache_layer_to_buffer(
back: &mut [u16],
back: &mut [u32],
screen_w: usize,
screen_h: usize,
cache: &SceneViewportCache,
@ -785,7 +785,7 @@ impl Gfx {
}
fn draw_bucket_on_buffer(
back: &mut [u16],
back: &mut [u32],
screen_w: usize,
screen_h: usize,
bucket: &[usize],
@ -802,7 +802,7 @@ impl Gfx {
}
fn draw_sprite_pixel_by_pixel(
back: &mut [u16],
back: &mut [u32],
screen_w: usize,
screen_h: usize,
sprite: &Sprite,
@ -841,24 +841,25 @@ impl Gfx {
/// Applies the fade effect to the entire back buffer.
/// level: 0 (full color) to 31 (visible)
fn apply_fade_to_buffer(back: &mut [u16], level: u8, fade_color: Color) {
fn apply_fade_to_buffer(back: &mut [u32], level: u8, fade_color: Color) {
if level >= 31 {
return;
} // Fully visible, skip processing
let weight = level as u16;
let inv_weight = 31 - weight;
let (fr, fg, fb) = Color::unpack_to_native(fade_color.0);
let (fr, fg, fb, fa) = unpack_rgba8888(fade_color.raw());
for px in back.iter_mut() {
let (sr, sg, sb) = Color::unpack_to_native(*px);
let (sr, sg, sb, sa) = unpack_rgba8888(*px);
// Formula: (src * weight + fade * inv_weight) / 31
let r = ((sr as u16 * weight + fr as u16 * inv_weight) / 31) as u8;
let g = ((sg as u16 * weight + fg as u16 * inv_weight) / 31) as u8;
let b = ((sb as u16 * weight + fb as u16 * inv_weight) / 31) as u8;
let a = ((sa as u16 * weight + fa as u16 * inv_weight) / 31) as u8;
*px = Color::pack_from_native(r, g, b);
*px = pack_rgba8888(r, g, b, a);
}
}
@ -885,7 +886,7 @@ impl Gfx {
}
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) {
if color == Color::COLOR_KEY {
if color.alpha() == 0 {
return;
}
@ -900,7 +901,7 @@ impl Gfx {
}
let glyph = glyph_for_char(c);
let raw = color.0;
let raw = color.raw();
let row_start = (-y).clamp(0, glyph_h) as usize;
let row_end = (screen_h - y).clamp(0, glyph_h) as usize;
@ -908,7 +909,7 @@ impl Gfx {
let col_start = (-x).clamp(0, glyph_w) as usize;
let col_end = (screen_w - x).clamp(0, glyph_w) as usize;
for row_idx in row_start..row_end {
for (row_idx, _) in glyph.iter().enumerate().take(row_end).skip(row_start) {
let row = glyph[row_idx];
let py = (y + row_idx as i32) as usize;
let base = py * self.w;
@ -925,56 +926,66 @@ impl Gfx {
}
}
/// Blends in RGB565 per channel with saturation.
/// `dst` and `src` are RGB565 pixels (u16).
fn blend_rgb565(dst: u16, src: u16, mode: BlendMode) -> u16 {
fn blend_rgba8888(dst: u32, src: u32, mode: BlendMode) -> u32 {
match mode {
BlendMode::None => src,
BlendMode::Half => {
let (dr, dg, db) = Color::unpack_to_native(dst);
let (sr, sg, sb) = Color::unpack_to_native(src);
let (dr, dg, db, da) = unpack_rgba8888(dst);
let (sr, sg, sb, sa) = unpack_rgba8888(src);
let r = ((dr as u16 + sr as u16) >> 1) as u8;
let g = ((dg as u16 + sg as u16) >> 1) as u8;
let b = ((db as u16 + sb as u16) >> 1) as u8;
Color::pack_from_native(r, g, b)
let a = ((da as u16 + sa as u16) >> 1) as u8;
pack_rgba8888(r, g, b, a)
}
BlendMode::HalfPlus => {
let (dr, dg, db) = Color::unpack_to_native(dst);
let (sr, sg, sb) = Color::unpack_to_native(src);
let (dr, dg, db, da) = unpack_rgba8888(dst);
let (sr, sg, sb, sa) = unpack_rgba8888(src);
let r = (dr as u16 + ((sr as u16) >> 1)).min(31) as u8;
let g = (dg as u16 + ((sg as u16) >> 1)).min(63) as u8;
let b = (db as u16 + ((sb as u16) >> 1)).min(31) as u8;
let r = (dr as u16 + ((sr as u16) >> 1)).min(255) as u8;
let g = (dg as u16 + ((sg as u16) >> 1)).min(255) as u8;
let b = (db as u16 + ((sb as u16) >> 1)).min(255) as u8;
let a = (da as u16 + ((sa as u16) >> 1)).min(255) as u8;
Color::pack_from_native(r, g, b)
pack_rgba8888(r, g, b, a)
}
BlendMode::HalfMinus => {
let (dr, dg, db) = Color::unpack_to_native(dst);
let (sr, sg, sb) = Color::unpack_to_native(src);
let (dr, dg, db, da) = unpack_rgba8888(dst);
let (sr, sg, sb, sa) = unpack_rgba8888(src);
let r = (dr as i16 - ((sr as i16) >> 1)).max(0) as u8;
let g = (dg as i16 - ((sg as i16) >> 1)).max(0) as u8;
let b = (db as i16 - ((sb as i16) >> 1)).max(0) as u8;
let a = (da as i16 - ((sa as i16) >> 1)).max(0) as u8;
Color::pack_from_native(r, g, b)
pack_rgba8888(r, g, b, a)
}
BlendMode::Full => {
let (dr, dg, db) = Color::unpack_to_native(dst);
let (sr, sg, sb) = Color::unpack_to_native(src);
let (dr, dg, db, da) = unpack_rgba8888(dst);
let (sr, sg, sb, sa) = unpack_rgba8888(src);
let r = (dr as u16 + sr as u16).min(31) as u8;
let g = (dg as u16 + sg as u16).min(63) as u8;
let b = (db as u16 + sb as u16).min(31) as u8;
let r = (dr as u16 + sr as u16).min(255) as u8;
let g = (dg as u16 + sg as u16).min(255) as u8;
let b = (db as u16 + sb as u16).min(255) as u8;
let a = (da as u16 + sa as u16).min(255) as u8;
Color::pack_from_native(r, g, b)
pack_rgba8888(r, g, b, a)
}
}
}
fn unpack_rgba8888(px: u32) -> (u8, u8, u8, u8) {
Color::unpack_to_native(px)
}
fn pack_rgba8888(r: u8, g: u8, b: u8, a: u8) -> u32 {
Color::pack_from_native(r, g, b, a)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -22,7 +22,7 @@ use std::sync::Arc;
///
/// ### Console Specifications:
/// - **Resolution**: 480x270 (16:9 Aspect Ratio).
/// - **Color Depth**: RGB565 (16-bit).
/// - **Color Format**: RGBA8888.
/// - **Audio**: Stereo, Command-based mixing.
/// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse.
pub struct Hardware {

View File

@ -175,7 +175,8 @@ mod tests {
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect;
use prometeu_hal::syscalls::caps;

View File

@ -2,7 +2,8 @@ use crate::firmware::firmware_state::{
AppCrashesStep, FirmwareState, GameRunningStep, ShellRunningStep,
};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::primitives::Rect;

View File

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum AppMode {
Game,
Shell,
}

View File

@ -1,3 +1,4 @@
use crate::app_mode::AppMode;
use crate::asset::{AssetEntry, PreloadEntry};
use crate::syscalls::CapFlags;
use serde::{Deserialize, Serialize};
@ -10,12 +11,6 @@ pub const ASSETS_PA_MAGIC: [u8; 4] = *b"ASPA";
pub const ASSETS_PA_SCHEMA_VERSION: u32 = 1;
pub const ASSETS_PA_PRELUDE_SIZE: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum AppMode {
Game,
Shell,
}
#[derive(Debug, Clone)]
pub struct Cartridge {
pub app_id: u32,

View File

@ -366,7 +366,7 @@ mod tests {
bank_type: BankType::GLYPH,
offset,
size,
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 4),
codec: AssetCodec::None,
metadata: json!({
"tile_size": 16,
@ -449,7 +449,7 @@ mod tests {
bank_type: BankType::GLYPH,
offset: 4,
size: 4,
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 4),
codec: AssetCodec::None,
metadata: json!({
"tile_size": 16,

View File

@ -1,75 +1,67 @@
/// Represents a 16-bit color in the RGB565 format.
///
/// The RGB565 format is a common high-color representation for embedded systems:
/// - **Red**: 5 bits (0..31)
/// - **Green**: 6 bits (0..63)
/// - **Blue**: 5 bits (0..31)
///
/// Prometeu does not have a hardware alpha channel. Transparency is achieved
/// by using a specific color key (Magenta / 0xF81F) which the GFX engine
/// skips during rendering.
/// Represents a canonical RGBA8888 color in RGBA channel order.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Color(pub u16);
pub struct Color(pub u32);
impl Color {
pub const BLACK: Self = Self::rgb(0, 0, 0); // 0x0000
pub const WHITE: Self = Self::rgb(255, 255, 255); // 0xFFFF
pub const RED: Self = Self::rgb(255, 0, 0); // 0xF800
pub const GREEN: Self = Self::rgb(0, 255, 0); // 0x07E0
pub const BLUE: Self = Self::rgb(0, 0, 255); // 0x001F
pub const YELLOW: Self = Self::rgb(255, 255, 0); // 0xFFE0
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const RED: Self = Self::rgb(255, 0, 0);
pub const GREEN: Self = Self::rgb(0, 255, 0);
pub const BLUE: Self = Self::rgb(0, 0, 255);
pub const YELLOW: Self = Self::rgb(255, 255, 0);
pub const ORANGE: Self = Self::rgb(255, 128, 0);
pub const INDIGO: Self = Self::rgb(75, 0, 130);
pub const GRAY: Self = Self::rgb(128, 128, 128);
pub const CYAN: Self = Self::rgb(0, 255, 255); // 0x07FF
pub const MAGENTA: Self = Self::rgb(255, 0, 255); // 0xF81F
pub const COLOR_KEY: Self = Self::MAGENTA;
pub const TRANSPARENT: Self = Self::MAGENTA;
pub const CYAN: Self = Self::rgb(0, 255, 255);
pub const MAGENTA: Self = Self::rgb(255, 0, 255);
pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
#[deprecated(note = "Use RGBA alpha transparency; COLOR_KEY is temporary migration residue.")]
pub const COLOR_KEY: Self = Self::TRANSPARENT;
/// Extracts channels in the native RGB565 range:
/// R: 0..31, G: 0..63, B: 0..31
/// Extracts RGBA8888 channels from canonical RGBA raw order.
#[inline(always)]
pub const fn unpack_to_native(px: u16) -> (u8, u8, u8) {
let r = ((px >> 11) & 0x1F) as u8;
let g = ((px >> 5) & 0x3F) as u8;
let b = (px & 0x1F) as u8;
(r, g, b)
pub const fn unpack_to_native(px: u32) -> (u8, u8, u8, u8) {
let r = ((px >> 24) & 0xFF) as u8;
let g = ((px >> 16) & 0xFF) as u8;
let b = ((px >> 8) & 0xFF) as u8;
let a = (px & 0xFF) as u8;
(r, g, b, a)
}
/// Packs channels from the native RGB565 range into a pixel:
/// R: 0..31, G: 0..63, B: 0..31
/// Packs RGBA8888 channels into canonical RGBA raw order.
#[inline(always)]
pub const fn pack_from_native(r: u8, g: u8, b: u8) -> u16 {
((r as u16 & 0x1F) << 11) | ((g as u16 & 0x3F) << 5) | (b as u16 & 0x1F)
pub const fn pack_from_native(r: u8, g: u8, b: u8, a: u8) -> u32 {
((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | a as u32
}
/// Creates an RGB565 color from 8-bit components (0..255).
/// Creates an opaque RGBA8888 color from 8-bit RGB components.
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
let r5 = (r as u16 >> 3) & 0x1F;
let g6 = (g as u16 >> 2) & 0x3F;
let b5 = (b as u16 >> 3) & 0x1F;
Self::rgba(r, g, b, 255)
}
Self((r5 << 11) | (g6 << 5) | b5)
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self(Self::pack_from_native(r, g, b, a))
}
pub const fn gray_scale(c: u8) -> Self {
Self::rgb(c, c, c)
}
pub const fn from_raw(raw: u16) -> Self {
pub const fn from_raw(raw: u32) -> Self {
Self(raw)
}
pub const fn raw(self) -> u16 {
pub const fn raw(self) -> u32 {
self.0
}
pub const fn hex(self) -> i32 {
let (r5, g6, b5) = Self::unpack_to_native(self.0);
let r8 = ((r5 as u32) << 3) | ((r5 as u32) >> 2);
let g8 = ((g6 as u32) << 2) | ((g6 as u32) >> 4);
let b8 = ((b5 as u32) << 3) | ((b5 as u32) >> 2);
let hex = r8 << 16 | g8 << 8 | b8;
let (r, g, b, _) = Self::unpack_to_native(self.0);
let hex = (r as u32) << 16 | (g as u32) << 8 | b as u32;
hex as i32
}
pub const fn alpha(self) -> u8 {
(self.0 & 0xFF) as u8
}
}

View File

@ -1,4 +1,4 @@
use crate::cartridge::AppMode;
use crate::app_mode::AppMode;
use prometeu_bytecode::Value;
use serde::{Deserialize, Serialize};

View File

@ -24,7 +24,7 @@ pub enum GfxOpStatus {
pub trait GfxBridge {
fn size(&self) -> (usize, usize);
fn front_buffer(&self) -> &[u16];
fn front_buffer(&self) -> &[u32];
fn clear(&mut self, color: Color);
fn fill_rect_blend(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode);
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color);

View File

@ -32,9 +32,10 @@ pub struct GlyphBank {
/// Decoded pixel data stored as one palette index per pixel.
/// Serialized payloads are packed; runtime memory is expanded.
/// Index 0 is always reserved for transparency.
/// Palette indices are ordinary indices; transparency is resolved through
/// the RGBA alpha channel of the palette entry.
pub pixel_indices: Vec<u8>,
/// Runtime-facing v1 palette table: 64 palettes of 16 RGB565 colors each.
/// Runtime-facing v1 palette table: 64 palettes of 16 RGBA8888 colors each.
pub palettes: [[Color; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
}
@ -45,7 +46,7 @@ impl GlyphBank {
tile_size,
width,
height,
pixel_indices: vec![0; width * height], // Index 0 = Transparent
pixel_indices: vec![0; width * height],
palettes: [[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
}
}
@ -65,22 +66,16 @@ impl GlyphBank {
if pixel_x < self.width && pixel_y < self.height {
self.pixel_indices[pixel_y * self.width + pixel_x]
} else {
0 // Default to transparent if out of bounds
0
}
}
/// Maps a 4-bit index to a real RGB565 Color using the specified palette.
/// Maps a 4-bit index to a real RGBA8888 Color using the specified palette.
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
// Hardware Rule: Index 0 is always transparent.
// We use Magenta as the 'transparent' signal color during composition.
if pixel_index == 0 {
return Color::COLOR_KEY;
}
self.palettes
.get(palette_id as usize)
.and_then(|palette| palette.get(pixel_index as usize))
.copied()
.unwrap_or(Color::COLOR_KEY)
.unwrap_or(Color::TRANSPARENT)
}
}

View File

@ -1,3 +1,4 @@
pub mod app_mode;
pub mod asset;
pub mod asset_bridge;
pub mod audio_bridge;

View File

@ -37,7 +37,6 @@ pub enum Syscall {
GfxDrawDisc = 0x1005,
GfxDrawSquare = 0x1006,
GfxDrawText = 0x1008,
GfxClear565 = 0x1010,
ComposerBindScene = 0x1101,
ComposerUnbindScene = 0x1102,
ComposerSetCamera = 0x1103,

View File

@ -29,8 +29,4 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
.args(4)
.caps(caps::GFX)
.cost(20),
SyscallRegistryEntry::builder(Syscall::GfxClear565, "gfx", "clear_565")
.args(1)
.caps(caps::GFX)
.cost(20),
];

View File

@ -21,7 +21,6 @@ impl Syscall {
0x1005 => Some(Self::GfxDrawDisc),
0x1006 => Some(Self::GfxDrawSquare),
0x1008 => Some(Self::GfxDrawText),
0x1010 => Some(Self::GfxClear565),
0x1101 => Some(Self::ComposerBindScene),
0x1102 => Some(Self::ComposerUnbindScene),
0x1103 => Some(Self::ComposerSetCamera),
@ -71,7 +70,6 @@ impl Syscall {
Self::GfxDrawDisc => "GfxDrawDisc",
Self::GfxDrawSquare => "GfxDrawSquare",
Self::GfxDrawText => "GfxDrawText",
Self::GfxClear565 => "GfxClear565",
Self::ComposerBindScene => "ComposerBindScene",
Self::ComposerUnbindScene => "ComposerUnbindScene",
Self::ComposerSetCamera => "ComposerSetCamera",

View File

@ -210,10 +210,6 @@ fn status_first_syscall_signatures_are_pinned() {
assert_eq!(draw_text.arg_slots, 4);
assert_eq!(draw_text.ret_slots, 0);
let clear_565 = meta_for(Syscall::GfxClear565);
assert_eq!(clear_565.arg_slots, 1);
assert_eq!(clear_565.ret_slots, 0);
let bind_scene = meta_for(Syscall::ComposerBindScene);
assert_eq!(bind_scene.arg_slots, 1);
assert_eq!(bind_scene.ret_slots, 1);

View File

@ -1,6 +1,7 @@
use crate::CrashReport;
use crate::os::SystemOS;
use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_vm::VirtualMachine;

View File

@ -2,8 +2,8 @@ use super::*;
use crate::fs::{FsState, VirtualFS};
use crate::services::memcard::{MemcardService, MemcardSlotState, MemcardStatus};
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef};
use prometeu_hal::cartridge::AppMode;
use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::log::{LogLevel, LogSource};
@ -95,8 +95,10 @@ impl VmRuntimeHost<'_> {
Ok(())
}
pub(crate) fn get_color(&self, value: i64) -> Color {
Color::from_raw(value as u16)
pub(crate) fn get_color(&self, value: i64) -> Result<Color, VmFault> {
let raw = u32::try_from(value)
.map_err(|_| VmFault::Trap(TRAP_OOB, "Color value out of bounds".into()))?;
Ok(Color::from_raw(raw))
}
fn int_arg_to_usize_status(value: i64) -> Result<usize, ComposerOpStatus> {
@ -126,7 +128,6 @@ impl VmRuntimeHost<'_> {
| Syscall::GfxDrawDisc
| Syscall::GfxDrawSquare
| Syscall::GfxDrawText
| Syscall::GfxClear565
| Syscall::ComposerBindScene
| Syscall::ComposerUnbindScene
| Syscall::ComposerSetCamera
@ -183,7 +184,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
Syscall::SystemHasCart => unreachable!(),
Syscall::SystemRunCart => unreachable!(),
Syscall::GfxClear => {
let color = self.get_color(expect_int(args, 0)?);
let color = self.get_color(expect_int(args, 0)?)?;
hw.gfx_mut().clear(color);
Ok(())
}
@ -192,7 +193,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
let y = expect_int(args, 1)? as i32;
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let color = self.get_color(expect_int(args, 4)?);
let color = self.get_color(expect_int(args, 4)?)?;
hw.gfx_mut().fill_rect(x, y, w, h, color);
Ok(())
}
@ -201,7 +202,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
let y1 = expect_int(args, 1)? as i32;
let x2 = expect_int(args, 2)? as i32;
let y2 = expect_int(args, 3)? as i32;
let color = self.get_color(expect_int(args, 4)?);
let color = self.get_color(expect_int(args, 4)?)?;
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
Ok(())
}
@ -209,7 +210,7 @@ impl NativeInterface for VmRuntimeHost<'_> {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let color = self.get_color(expect_int(args, 3)?);
let color = self.get_color(expect_int(args, 3)?)?;
hw.gfx_mut().draw_circle(x, y, r, color);
Ok(())
}
@ -217,8 +218,8 @@ impl NativeInterface for VmRuntimeHost<'_> {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let border_color = self.get_color(expect_int(args, 3)?);
let fill_color = self.get_color(expect_int(args, 4)?);
let border_color = self.get_color(expect_int(args, 3)?)?;
let fill_color = self.get_color(expect_int(args, 4)?)?;
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
Ok(())
}
@ -227,8 +228,8 @@ impl NativeInterface for VmRuntimeHost<'_> {
let y = expect_int(args, 1)? as i32;
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let border_color = self.get_color(expect_int(args, 4)?);
let fill_color = self.get_color(expect_int(args, 5)?);
let border_color = self.get_color(expect_int(args, 4)?)?;
let fill_color = self.get_color(expect_int(args, 5)?)?;
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
Ok(())
}
@ -236,18 +237,10 @@ impl NativeInterface for VmRuntimeHost<'_> {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let msg = expect_string(args, 2, "message")?;
let color = self.get_color(expect_int(args, 3)?);
let color = self.get_color(expect_int(args, 3)?)?;
hw.gfx_mut().draw_text(x, y, &msg, color);
Ok(())
}
Syscall::GfxClear565 => {
let color_val = expect_int(args, 0)? as u32;
if color_val > 0xFFFF {
return Err(VmFault::Trap(TRAP_OOB, "Color value out of bounds".into()));
}
hw.gfx_mut().clear(Color::from_raw(color_val as u16));
Ok(())
}
Syscall::ComposerBindScene => {
let scene_bank_id = match Self::int_arg_to_usize_status(expect_int(args, 0)?) {
Ok(id) => id,

View File

@ -6,7 +6,7 @@ mod tick;
use crate::CrashReport;
use prometeu_bytecode::string_materialization_count;
use prometeu_hal::cartridge::AppMode;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::log::LogService;
use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
use prometeu_vm::VirtualMachine;

View File

@ -113,11 +113,11 @@ fn serialized_single_function_module_with_consts(
}
fn test_glyph_payload_size(width: usize, height: usize) -> usize {
(width * height).div_ceil(2) + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
(width * height).div_ceil(2) + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u32>())
}
fn test_glyph_decoded_size(width: usize, height: usize) -> usize {
width * height + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
width * height + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u32>())
}
fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
@ -141,8 +141,8 @@ fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
fn test_glyph_asset_data() -> Vec<u8> {
let mut data =
vec![0x11u8; test_glyph_payload_size(16, 16) - (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 2)];
data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 2]);
vec![0x11u8; test_glyph_payload_size(16, 16) - (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 4)];
data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 4]);
data
}
@ -648,7 +648,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
let mut vm = VirtualMachine::default();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 65535\nHOSTCALL 0\nFRAME_SYNC\nHALT",
"PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 287454020\nHOSTCALL 0\nFRAME_SYNC\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module_with_consts(
@ -682,7 +682,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
assert!(report.is_none(), "no-scene overlay text must not crash");
hardware.gfx.present();
assert_eq!(hardware.gfx.front_buffer()[0], Color::WHITE.raw());
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
}
#[test]

View File

@ -2545,12 +2545,12 @@ mod tests {
#[test]
fn test_syscall_results_count_mismatch_panic() {
// GfxClear565 (0x1010) expects 0 results
// GfxClear (0x1001) expects 0 results
let rom = vec![
0x17, 0x00, // PushI32
0x00, 0x00, 0x00, 0x00, // value 0
0x70, 0x00, // Syscall + Reserved
0x10, 0x10, 0x00, 0x00, // Syscall ID 0x1010
0x01, 0x10, 0x00, 0x00, // Syscall ID 0x1001
];
struct BadNative;
@ -2562,7 +2562,7 @@ mod tests {
ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
// Wrong: GfxClear565 is void but we push something
// Wrong: GfxClear is void but we push something
ret.push_int(42);
Ok(())
}

View File

@ -38,13 +38,7 @@ impl FbViewport {
let viewport_x = ((win_w - viewport_w) * 0.5).floor();
let viewport_y = ((win_h - viewport_h) * 0.5).floor();
Some(FbViewport {
x: viewport_x,
y: viewport_y,
w: viewport_w,
h: viewport_h,
scale,
})
Some(FbViewport { x: viewport_x, y: viewport_y, w: viewport_w, h: viewport_h, scale })
}
}
@ -126,10 +120,7 @@ impl HostInputHandler {
let fb_x = (local_x / viewport.scale).floor() as i32;
let fb_y = (local_y / viewport.scale).floor() as i32;
Some((
fb_x.clamp(0, Hardware::W as i32 - 1),
fb_y.clamp(0, Hardware::H as i32 - 1),
))
Some((fb_x.clamp(0, Hardware::W as i32 - 1), fb_y.clamp(0, Hardware::H as i32 - 1)))
}
}
}
}

View File

@ -5,7 +5,7 @@ use crate::input::HostInputHandler;
use crate::log_sink::HostConsoleSink;
use crate::overlay::{capture_snapshot, draw_overlay};
use crate::stats::HostStats;
use crate::utilities::draw_rgb565_to_rgba8;
use crate::utilities::draw_rgba8888_to_rgba8;
use pixels::wgpu::PresentMode;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_drivers::AudioCommand;
@ -285,7 +285,7 @@ impl ApplicationHandler for HostRunner {
// Immutable borrow of prometeu-core (different field, ok)
let src = self.hardware.gfx.front_buffer();
draw_rgb565_to_rgba8(src, frame);
draw_rgba8888_to_rgba8(src, frame);
if let Some(snapshot) = overlay_snapshot.as_ref() {
draw_overlay(frame, Hardware::W, Hardware::H, snapshot);

View File

@ -1,26 +1,25 @@
/// Copies RGB565 (u16) -> RGBA8888 (u8[4]) to the pixels frame.
/// Pixels format: RGBA8.
pub fn draw_rgb565_to_rgba8(src: &[u16], dst_rgba: &mut [u8]) {
/// Copies RGBA8888 pixels in canonical RGBA raw order into the `pixels` RGBA8 frame.
pub fn draw_rgba8888_to_rgba8(src: &[u32], dst_rgba: &mut [u8]) {
for (i, &px) in src.iter().enumerate() {
let (r8, g8, b8) = rgb565_to_rgb888(px);
let o = i * 4;
dst_rgba[o] = r8;
dst_rgba[o + 1] = g8;
dst_rgba[o + 2] = b8;
dst_rgba[o + 3] = 0xFF;
dst_rgba[o] = ((px >> 24) & 0xFF) as u8;
dst_rgba[o + 1] = ((px >> 16) & 0xFF) as u8;
dst_rgba[o + 2] = ((px >> 8) & 0xFF) as u8;
dst_rgba[o + 3] = (px & 0xFF) as u8;
}
}
/// Expands RGB565 to RGB888 (high bit replication).
#[inline(always)]
pub fn rgb565_to_rgb888(px: u16) -> (u8, u8, u8) {
let r5 = ((px >> 11) & 0x1F) as u8;
let g6 = ((px >> 5) & 0x3F) as u8;
let b5 = (px & 0x1F) as u8;
#[cfg(test)]
mod tests {
use super::draw_rgba8888_to_rgba8;
let r8 = (r5 << 3) | (r5 >> 2);
let g8 = (g6 << 2) | (g6 >> 4);
let b8 = (b5 << 3) | (b5 >> 2);
#[test]
fn draw_rgba8888_to_rgba8_preserves_channel_order_and_alpha() {
let src = [0x12345678, 0xABCDEF01];
let mut dst = [0; 8];
(r8, g8, b8)
draw_rgba8888_to_rgba8(&src, &mut dst);
assert_eq!(dst, [0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01]);
}
}

View File

@ -43,7 +43,7 @@ pub fn generate() -> Result<()> {
let syscalls = vec![
SyscallDecl {
module: "gfx".into(),
name: "clear_565".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
@ -284,7 +284,7 @@ fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
bank_type: BankType::GLYPH,
offset: 0,
size: payload.len() as u64,
decoded_size: (8 * 8 + GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2)
decoded_size: (8 * 8 + GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 4)
as u64,
codec: AssetCodec::None,
metadata: serde_json::json!({
@ -301,11 +301,11 @@ fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
fn build_palette_bytes() -> Vec<u8> {
let mut bytes =
Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2);
Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 4);
for palette_id in 0..GLYPH_BANK_PALETTE_COUNT_V1 {
for color_index in 0..GLYPH_BANK_COLORS_PER_PALETTE {
let color = if color_index == 1 { stress_color(palette_id) } else { Color::BLACK };
bytes.extend_from_slice(&color.raw().to_le_bytes());
bytes.extend_from_slice(&color.raw().to_be_bytes());
}
}
bytes

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":37,"AGD":37,"DEC":29,"PLN":67,"LSN":46,"CLSN":1}}
{"type":"meta","next_id":{"DSC":38,"AGD":38,"DEC":30,"PLN":73,"LSN":47,"CLSN":1}}
{"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
@ -34,4 +34,5 @@
{"type":"discussion","id":"DSC-0031","status":"done","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0031-runtime-mode-separation-game-system/LSN-0040-system-pipeline-separation.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]}
{"type":"discussion","id":"DSC-0032","status":"done","ticket":"system-os-lifecycle-process-task-contract","title":"Agenda - SystemOS Lifecycle, Process and Task Contract","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","lifecycle","process","task","shell","firmware"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0032-system-os-lifecycle-process-task-contract/LSN-0041-systemos-lifecycle-authority.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0033","status":"done","ticket":"system-os-service-ownership-and-module-layout","title":"Agenda - SystemOS Service Ownership and Module Layout","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","services","module-layout","vm","window-manager","logging"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0042","file":"discussion/lessons/DSC-0033-system-os-service-ownership-and-module-layout/LSN-0042-systemos-service-ownership-boundary.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0036","status":"in_progress","ticket":"prometeu-hub-ui-direction","title":"Agenda - Prometeu Hub UI Direction","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["hub","ui","shell","system-apps","lifecycle","design-system"],"agendas":[{"id":"AGD-0036","file":"AGD-0036-prometeu-hub-ui-direction.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0028","file":"DEC-0028-prometeu-hub-initial-retro-shell-ui-slice.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0036"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-native-shell-launch-and-lifecycle-wiring.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0063","file":"PLN-0063-hub-mouse-input-and-click-routing.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0064","file":"PLN-0064-retro-hub-home-and-fake-shell-ui.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0065","file":"PLN-0065-pixel-operator-font-integration.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0066","file":"PLN-0066-hub-ui-slice-validation-and-lesson.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]}],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0036-prometeu-hub-ui-direction/LSN-0045-hub-ui-slices-should-prove-os-boundaries.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0036","status":"done","ticket":"prometeu-hub-ui-direction","title":"Agenda - Prometeu Hub UI Direction","created_at":"2026-05-15","updated_at":"2026-05-22","tags":["hub","ui","shell","system-apps","lifecycle","design-system"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0036-prometeu-hub-ui-direction/LSN-0045-hub-ui-slices-should-prove-os-boundaries.md","status":"done","created_at":"2026-05-22","updated_at":"2026-05-22"}]}
{"type":"discussion","id":"DSC-0037","status":"done","ticket":"rgba8888-framebuffer-and-pixel-format-direction","title":"Agenda - RGBA8888 Framebuffer and Pixel Format Direction","created_at":"2026-05-22","updated_at":"2026-05-23","tags":["gfx","framebuffer","rgb565","rgba8888","renderer","assets","host","backend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0037-rgba8888-framebuffer-and-pixel-format-direction/LSN-0046-pixel-format-contracts-must-move-as-one-surface.md","status":"done","created_at":"2026-05-23","updated_at":"2026-05-23"}]}

View File

@ -0,0 +1,78 @@
---
id: LSN-0046
ticket: rgba8888-framebuffer-and-pixel-format-direction
title: Pixel Format Contracts Must Move as One Surface
created: 2026-05-23
tags: [gfx, framebuffer, rgba8888, abi, assets, host]
decision: DEC-0029
---
## Context
The runtime migrated its graphics contract from RGB565 to RGBA8888. The
important discovery was that RGB565 was not only a renderer storage detail. It
had become a cross-cutting contract spanning VM values, syscalls, HAL types,
renderer buffers, asset palette bytes, host presentation, tests, and specs.
Treating the change as a local buffer replacement would have left contradictory
contracts behind. The successful migration split the work into small plans, but
each plan still respected one global rule: a pixel format is a published
surface, not an implementation token.
## Key Decisions
### RGBA8888 Runtime Pixel Format Contract
**What:** RGBA8888 became the single logical color and physical framebuffer
contract. RGB565 compatibility surfaces were removed instead of deprecated as
canonical aliases.
**Why:** Keeping RGB565 as compatibility would have preserved the old coupling
between logical color, framebuffer storage, asset palette encoding, and host
presentation. A single RGBA8888 contract made the runtime easier to reason
about and aligned with alpha-capable UI and modern host presentation.
**Trade-offs:** The migration intentionally broke existing RGB565 assets and
tests. It also doubled framebuffer byte width. The payoff was removing support
for two simultaneous color models and making alpha an ordinary property of
color instead of a special case.
## Patterns and Algorithms
Move published contracts before code. Specs, ABI docs, and public docs were
updated first so implementation work had a stable target.
Keep API names format-neutral when the runtime supports only one format.
Names such as `GfxClear565` create permanent compatibility residue. The
canonical surface should be `GfxClear`; RGBA8888 is the contract behind it.
Use the alpha channel as data, not a magic index. Palette indices are ordinary
indices. Transparency is represented by the RGBA alpha channel of the resolved
palette entry rather than by reserving palette index `0`.
Validate with residue scans. The final pass searched for RGB565, `Gfx*565`,
RGB565 palette sizing, host conversion helpers, and `u16` framebuffer surfaces.
Remaining hits had to be negative documentation or unrelated `u16` usage.
## Pitfalls
Pixel format names leak into places that look unrelated: generator fixtures,
cartridge payload size calculations, syscall metadata, host copy utilities, and
test assembly snippets.
Endian assumptions are easy to hide when using `u32`. The contract must say
RGBA channel order explicitly, and host presentation code must unpack channels
by shifts instead of relying on native memory layout.
Compatibility helpers are useful only as short-lived scaffolding. If they are
left in normal paths, they become a second contract.
## Takeaways
- A framebuffer format is a runtime-wide contract once it reaches ABI, assets,
host presentation, or tests.
- Format-suffixed public APIs should be removed when the format is no longer a
selectable backend.
- Alpha should be modeled as color data, not as a reserved palette index.
- A staged migration can still enforce one final contract when each plan has a
residue scan and acceptance criteria.

View File

@ -1,193 +0,0 @@
---
id: AGD-0036
ticket: prometeu-hub-ui-direction
title: Agenda - Prometeu Hub UI Direction
status: accepted
created: 2026-05-15
resolved:
decision:
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
---
## Contexto
O Prometeu Hub esta entrando em uma etapa em que a Shell precisa deixar de ser apenas uma superficie funcional e passar a expressar uma identidade visual propria do Prometeu OS.
A direcao desejada e uma UI retro/minimalista inspirada em console portatil, fantasy console e Homebrew Channel: baixa resolucao, tipografia pixelada, poucos tons, bordas marcadas, componentes simples, leitura clara e navegacao confortavel por controle.
O alvo inicial nao e criar um toolkit completo. A etapa deve estabelecer a linguagem visual e um conjunto pequeno de componentes suficientes para validar a Home do Hub e o fluxo de lifecycle ja construido.
Componentes iniciais esperados:
- Channel/Card
- Button
- Panel
- Tabs
- List
- Status bar
- Footer/help bar
- Focus highlight
O Hub deve continuar respeitando a arquitetura atual:
- Hub renderiza e navega.
- Hub emite acao.
- SystemOS/Firmware executa lifecycle, task, process e window.
Portanto, a UI do Hub nao deve manipular diretamente VM, process/task lifecycle ou window ownership fora das facades do SystemOS.
## Problema
A ausencia de uma direcao visual clara cria risco de o Hub virar uma UI desktop/mobile generica ou um conjunto de componentes sem coesao. Ao mesmo tempo, tentar resolver um design system completo agora pode atrasar a validacao do fluxo mais importante: Home -> abrir app Shell nativo fake -> fechar app -> retornar ao Hub.
O problema central e definir o primeiro contrato de direcao da UI: qual experiencia visual e interativa sera validada agora, quais componentes fazem parte do primeiro corte, e quais limites arquiteturais nao podem ser atravessados pela implementacao da Shell.
## Pontos Criticos
- A estetica retro precisa melhorar legibilidade e identidade, nao apenas adicionar decoracao.
- A baixa resolucao deve ser tratada como linguagem de composicao, escala e foco, sem comprometer leitura.
- Navegacao por controle precisa ser primeira classe: foco visivel, ordem previsivel e acoes claras.
- A Home precisa listar canais/apps e acionar apps sem assumir posse do lifecycle.
- Apps nativos fake devem existir para validar janela, retorno, fechamento e estado de foco, mas nao devem virar arquitetura definitiva de apps.
- Os componentes iniciais devem ser pequenos o bastante para evoluir depois para um toolkit, mas nao acoplados prematuramente a um toolkit completo.
- O Hub nao pode atravessar as facades do SystemOS para manipular VM, process/task lifecycle ou ownership de window.
## Opcoes
### Opcao A - Slice visual minimo dentro do Hub
Criar uma primeira Home retro consistente diretamente no Hub, com componentes locais simples e dois apps Shell nativos fake para validar navegacao, abertura, fechamento e retorno.
**Tradeoffs:**
- Pro: entrega rapidamente a experiencia desejada e valida o fluxo real de Shell/lifecycle.
- Pro: evita desenhar um toolkit abstrato antes de entender as necessidades concretas.
- Contra: pode gerar componentes locais que precisarao ser extraidos ou reorganizados depois.
- Risco: se os componentes nao tiverem limites claros, a implementacao pode misturar visual, navegacao e chamadas de lifecycle.
### Opcao B - Definir um mini toolkit antes da Home
Criar primeiro um modulo formal de componentes retro do Hub, depois construir a Home sobre ele.
**Tradeoffs:**
- Pro: melhora consistencia e reaproveitamento desde o inicio.
- Pro: facilita testes isolados de componentes e futuras telas do sistema.
- Contra: aumenta escopo antes de validar a experiencia principal.
- Risco: o toolkit pode cristalizar abstracoes erradas antes de haver uso suficiente.
### Opcao C - Prototipo descartavel de UI antes de integrar lifecycle
Criar uma tela visual isolada, focada apenas em look and feel, sem acoplar ainda ao fluxo de lifecycle, tasks, process e windows.
**Tradeoffs:**
- Pro: permite iterar visualmente com baixo risco arquitetural.
- Pro: reduz chance de alterar indevidamente SystemOS durante exploracao visual.
- Contra: nao valida a parte mais importante do Hub como Shell operacional.
- Risco: o prototipo pode parecer aprovado visualmente, mas falhar quando integrado ao fluxo real.
## Sugestao / Recomendacao
Seguir pela Opcao A, mas com escopo reduzido pelas respostas da agenda: implementar um slice visual minimo dentro do Hub, com Home manual em 270p, dois botoes clicaveis e dois apps Shell nativos fake.
A recomendacao e tratar essa etapa como uma direcao de Shell, nao como toolkit completo. O primeiro objetivo deve ser uma Home visualmente consistente que prove:
- dois botoes manuais para abrir ShellA e ShellB;
- ShellA como app/janela verde simples;
- ShellB como app/janela azul simples;
- abertura de app via acao emitida pelo Hub e executada pelo SystemOS/Firmware;
- fechamento de app como comando da propria app/janela;
- retorno ao Hub apos fechamento;
- estilo retro/minimalista consistente em viewport 270p;
- uso de mouse/click como input inicial;
- respeito estrito ao limite Hub -> SystemOS facade.
A extracao para toolkit, a lista/grid de canais, Tabs, Status bar e Footer/help bar devem ficar para waves futuras, guiadas por uso real dos componentes.
## Perguntas em Aberto
- [x] A Home deve priorizar grid de canais, lista vertical, ou alternar entre os dois via Tabs? na verdade eu tinha pensado em montar os dois shell fakes na mao mesmo, sem lista de canais e coisas mais sofisticadas
- [x] Qual resolucao logica deve orientar a composicao visual desta UI: a viewport interna atual do runtime, uma escala propria do Hub, ou uma convencao derivada da Shell? a viewport toda, 270p.
- [x] A fonte pixelada sera um asset proprio, uma fonte embutida existente, ou uma simulacao inicial via estilo/render atual? podemos ir de proprio mas temos um asset pronto em `assets/pixel_operator.zip`
- [x] Quais dois apps Shell nativos fake devem existir neste primeiro corte? ShellA e ShellB, uma simples tela verde e outro tela azul.
- [x] O fechamento de app deve ser exposto como comando da app, comando global da Shell, ou ambos? na primeira wave comando da app. toda app vai ser embrulhada em uma janela controle (como no windows)
- [x] O footer/help bar deve mapear acoes de controle de forma fixa ou depender do item/app focado? o footer fica para uma proxima wave
- [x] A navegacao inicial precisa suportar apenas controle/teclado, ou tambem mouse para debug/host desktop? vamos usar o mouse primeiramente (como clique)
- [x] Quais facades do SystemOS ja sao suficientes para abrir/fechar/retornar, e quais lacunas precisam virar trabalho separado? eu acredito que estamos bem com session e lifecycle, qq coisa a gente vai implementando no caminho
## Criterio para Encerrar
Esta agenda pode ser encerrada quando houver acordo sobre:
- direcao visual inicial da Shell;
- escopo exato da Home e dos dois apps fake;
- componentes minimos do primeiro corte;
- comportamento esperado de foco/navegacao;
- fronteira operacional entre Hub UI e SystemOS/Firmware;
- se a implementacao deve seguir como slice local do Hub ou exigir uma decisao separada para toolkit.
Com essas respostas, a agenda deve virar uma decisao normativa antes de qualquer plano ou edicao de codigo.
## Discussao
Entrada inicial: a UI deve ter aparencia de sistema embarcado/fantasy console, proxima em espirito a Switch/Homebrew Channel, mas com estetica propria de console retro. O objetivo pratico e inaugurar a identidade visual do Prometeu OS validando a Home do Hub e o lifecycle de apps Shell nativos fake.
### Analise apos respostas iniciais
As respostas mudam o primeiro corte de "Home com grid/lista de canais" para uma wave menor e mais direta: uma Shell/Home manual, em 270p, com dois apps nativos fake montados na mao. Isso reduz bastante o risco de criar um toolkit cedo demais.
O escopo recomendado para a primeira wave passa a ser:
- Hub em tela cheia 270p, com linguagem retro/minimalista consistente;
- dois affordances clicaveis para ShellA e ShellB, sem lista de canais generica;
- ShellA como janela/tela verde simples;
- ShellB como janela/tela azul simples;
- fonte pixelada propria usando o asset `assets/pixel_operator.zip`, se o pipeline de asset/font permitir sem abrir outro problema arquitetural;
- fechamento como comando da propria app/janela;
- mouse/click como input inicial;
- footer/help bar fora da primeira wave.
Isso enfraquece a necessidade de `Tabs`, `List`, `Status bar` e `Footer/help bar` no primeiro corte. Eles podem continuar como direcao de linguagem, mas nao devem ser criterios de aceite da wave inicial. Os componentes realmente necessarios agora sao:
- Panel ou frame de janela;
- Button/click target;
- Focus/hover/active highlight, mesmo que inicialmente guiado por mouse;
- Channel/Card apenas se os dois atalhos manuais da Home forem desenhados como cards.
Sobre as facades do SystemOS, o codigo atual ja tem pecas importantes:
- `os.sessions().create_native_shell_task(app_id, title)` cria process/task de Shell nativo e coloca a task em foreground.
- `os.lifecycle().close_task(task_id)` fecha task e marca o processo como stopped.
- `os.windows().add_window(...)`, `set_focus(...)`, `close_window(...)` e `focused_window_belongs_to_task(...)` permitem registrar, focar e observar janelas.
- `ShellRunningStep` ja retorna ao Hub quando a task fecha ou quando a janela focada nao pertence mais a task.
Mas a superficie ainda nao esta ideal para o contrato da agenda:
- o Hub atual cria janelas diretamente como `WindowOwner::Hub`, nao como `WindowOwner::Task(task_id)`;
- `PrometeuHub::update_shell_profile` retorna apenas crash, entao nao ha ainda um resultado explicito tipo "launch ShellA/ShellB" para o Firmware transicionar para `ShellRunningStep(task_id)`;
- `sessions().create_native_shell_task` cria task/process, mas nao cria uma janela owned-by-task em uma unica operacao de alto nivel;
- `WindowFacade::remove_all_windows()` e `WindowOwner` exposto no Hub facilitam um corte rapido, mas tambem permitem a UI atravessar demais o contrato de ownership se virarem padrao;
- fechar apenas a janela nao e semanticamente igual a fechar a app; para Shell nativa, o comando de fechar deve terminar em `lifecycle().close_task(task_id)`, e a janela deve ser removida como consequencia operacional ou mantida coerente pela mesma facade.
Sugestao para fechar a lacuna sem criar toolkit:
- manter a UI local no Hub;
- introduzir um resultado de Hub mais explicito, por exemplo `HubAction::LaunchNativeShell { app_id, title, kind }` ou equivalente, em vez de o Hub executar tudo diretamente;
- deixar Firmware/SystemOS criar a task nativa via `sessions().create_native_shell_task`;
- criar a janela com `WindowOwner::Task(task_id)` no mesmo fluxo de launch, preferencialmente por uma facade de sessao/janela de nivel mais alto, para nao espalhar ownership manual no Hub;
- fazer o comando de fechar da app chamar `lifecycle().close_task(task_id)` ou emitir uma acao equivalente para o SystemOS, nao apenas remover a janela;
- manter `ShellRunningStep` como responsavel pelo retorno ao Hub quando a task fechar.
Com isso, a arquitetura continua: Hub renderiza/navega e emite acao; SystemOS/Firmware executa lifecycle, task, process e window.
### Pontos que ainda precisam de resposta
- Confirmar se a primeira wave deve abandonar completamente grid/lista/tabs/footer, ou apenas deixar isso fora dos criterios de aceite. -- nao vamos nos ater no componentes nessa wave, mas caso seja necessario, podemos criar alguns.
- Confirmar se a Home manual tera dois cards/botoes clicaveis para ShellA/ShellB ou se as janelas fake aparecem direto na Home. -- Um btn para cada shell.
- Confirmar se `assets/pixel_operator` deve ser extraido/registrado como asset de fonte nesta wave ou se pode entrar como follow-up caso o pipeline de fonte nao esteja pronto. -- jah estah extraido
- Confirmar se a decisao deve exigir uma nova facade de alto nivel para launch de Shell nativa com janela owned-by-task, ou se o plano pode aceitar um uso transitorio de `sessions()` + `windows()` desde que fique encapsulado fora dos componentes visuais. -- vamos tentar usar o que temos.
## Resolucao
Validada para decisao, pendente apenas de confirmacao explicita para aceitar a agenda e iniciar o artefato de decisao.

View File

@ -1,153 +0,0 @@
---
id: DEC-0028
ticket: prometeu-hub-ui-direction
title: Prometeu Hub Initial Retro Shell UI Slice
status: accepted
created: 2026-05-15
ref_agenda: AGD-0036
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
---
## Context
AGD-0036 resolved the first visual direction for Prometeu Hub after the runtime gained the basic SystemOS, lifecycle, task, process, and task-owned window contracts.
The target is not a general UI toolkit yet. The target is the first executable Hub/Shell UI slice that establishes the Prometeu OS visual identity and validates the existing lifecycle flow with native fake Shell apps.
The accepted agenda narrowed the original "channel grid/list" idea into a smaller wave:
- a manual Hub/Home surface rendered in the full 270p viewport;
- two clickable entries, one for `ShellA` and one for `ShellB`;
- `ShellA` as a simple green native Shell fake app;
- `ShellB` as a simple blue native Shell fake app;
- mouse/click input first;
- app close initiated from the app/window itself;
- no Footer/help bar, Tabs, generic List, or complete Channel/Card system in this wave;
- use of the existing SystemOS session, lifecycle, and window facades unless implementation proves a narrower helper is required.
The decision depends on the existing operating model captured by the related lessons:
- System apps belong to the Runtime/Hub system pipeline, not the game pipeline.
- SystemOS owns lifecycle semantics.
- SystemOS owns shared OS services through domain facades.
- Shell window liveness is tied to a task-owned focused window.
## Decision
Prometeu Hub SHALL implement the first retro Shell UI as a narrow, local Hub slice rather than a reusable UI toolkit.
The first slice SHALL provide a full-viewport 270p Hub/Home with a retro/minimalist visual style inspired by portable console and fantasy-console UI: low-resolution composition, pixel-style typography, limited color use, strong borders, simple panels, and clear clickable affordances.
The first Hub/Home SHALL expose exactly two manual launch buttons:
- `ShellA`
- `ShellB`
The first slice SHALL launch two native fake Shell apps:
- `ShellA` SHALL render as a simple green app/window.
- `ShellB` SHALL render as a simple blue app/window.
The first slice SHALL use mouse/click input as the initial navigation and activation mechanism. Controller-first focus navigation MAY be added later, but it is not part of this decision's first acceptance surface.
The first slice SHALL treat app close as an app/window command. Closing a Shell app MUST result in the corresponding Shell task being closed through SystemOS lifecycle semantics and returning to Hub/Home.
The first slice MUST NOT introduce a complete UI toolkit, generic app catalog, channel grid, Tabs, List, Status bar, or Footer/help bar. Small local primitives such as button, panel/frame, text drawing, hover/active highlight, and simple window chrome MAY be created when needed to implement the slice.
The Hub MUST remain a visual Shell/Home program:
- Hub SHALL render and handle local UI navigation.
- Hub SHALL emit user-intent actions such as "launch ShellA", "launch ShellB", or "close current Shell app".
- SystemOS/Firmware SHALL execute lifecycle, task, process, and window ownership effects.
- Hub MUST NOT directly own VM execution, task/process lifecycle policy, or final window ownership semantics outside SystemOS facades.
The implementation SHOULD first attempt to use the existing `sessions()`, `lifecycle()`, and `windows()` facades. A new high-level facade is not required by this decision, but if implementation starts duplicating cross-domain launch/close policy in Hub components, the plan MUST introduce a small SystemOS/Firmware helper instead of spreading that policy through visual code.
## Rationale
This decision keeps the first UI wave small enough to execute while still validating the important architecture.
Building a complete toolkit now would force abstractions before there are enough Hub surfaces to justify them. A local slice lets the project discover real component needs from a concrete Home and two Shell apps.
Skipping lifecycle integration would validate only aesthetics and would not prove the console OS model. The first UI slice must exercise the route from Hub/Home to a native Shell task, task-owned window liveness, app close, lifecycle close, and return to Hub.
Starting with mouse/click is acceptable because this wave is primarily about visual identity and lifecycle integration. Controller navigation remains part of the broader Hub direction, but making it a hard requirement now would expand the first slice beyond the accepted agenda.
Using the existing facades respects the current architecture and avoids inventing a new OS API prematurely. The guardrail is that the Hub may compose calls at a local boundary, but visual components must not become lifecycle or ownership authorities.
## Invariants / Contract
- The internal composition target for this UI slice is the current 270p viewport.
- `assets/pixel_operator.zip` or its already-extracted content SHOULD be used for pixel-style typography when the existing rendering/font path can support it without unrelated architecture work.
- `ShellA` and `ShellB` are fake native Shell apps for lifecycle and UI validation. They MUST NOT be treated as a final app model or plugin/catalog contract.
- A launched Shell app MUST have task/process state created through SystemOS session/lifecycle pathways.
- A Shell app window used for liveness MUST be owned by `WindowOwner::Task(task_id)`, not by `WindowOwner::Hub`.
- Returning to Hub after app close MUST follow lifecycle closure. Removing a window alone is not a valid app-close semantic.
- The first wave MAY use local Hub UI primitives. Those primitives MUST remain scoped to the slice until a later decision defines a shared toolkit.
- Game cartridges MUST NOT be pulled into this windowed Shell model by implication. This decision applies to native fake Shell apps only.
## Impacts
### Specs
No canonical runtime spec change is required before the first implementation unless the plan introduces a new public SystemOS, Shell, or asset/font contract. If a new public contract is introduced, the canonical specs must be updated in English.
### Runtime / SystemOS
Implementation may need small integration work around native Shell launch and task-owned windows. The preferred path is:
1. Hub emits a launch intent for `ShellA` or `ShellB`.
2. Firmware/SystemOS creates the native Shell task through `sessions().create_native_shell_task(...)`.
3. The launch flow creates/focuses a window owned by `WindowOwner::Task(task_id)`.
4. `ShellRunningStep` drives the Shell app and returns to Hub when lifecycle/window liveness says the app is closed.
If this cannot be expressed cleanly with current facades, the plan may add a narrow helper/facade that coordinates session creation and task-owned window creation.
### Hub UI
The Hub UI should be reshaped from the current button-to-window debug surface into a first-person Shell/Home surface with:
- retro background/panel treatment;
- two clickable launch buttons;
- simple hover/active visual state for mouse input;
- app/window chrome sufficient to expose close from the app/window;
- clear visual distinction between Hub/Home and an opened Shell app.
### Host / Input
The first wave relies on existing host mouse mapping/click input if available. Keyboard/controller focus is not required by this decision.
### Firmware
Firmware remains the coordinator for state transitions between Hub/Home and Shell-running state. It must not delegate lifecycle authority to visual components.
### Tooling / Tests
Tests should cover the behavior that matters architecturally:
- launch `ShellA` creates a native Shell task and task-owned window;
- launch `ShellB` creates a native Shell task and task-owned window;
- closing the app closes lifecycle state rather than only removing the window;
- closed Shell returns to Hub/Home;
- Hub UI actions do not require direct task/process manager access.
## Propagation Targets
- specs: only if a new public Shell/SystemOS/font contract is introduced.
- plans: create an execution plan from this decision before code changes.
- code: `crates/console/prometeu-system`, `crates/console/prometeu-firmware`, and possibly host/input/rendering code if mouse/font integration requires it.
- tests: SystemOS/Firmware tests for launch/close/return behavior; UI rendering tests only if the repository already has an appropriate surface.
- docs: post-execution lesson in English after the implementation is complete.
## References
- Agenda: AGD-0036
- `LSN-0040` - System Pipeline Separation
- `LSN-0041` - SystemOS Lifecycle Authority
- `LSN-0042` - SystemOS Service Ownership Boundary
- `LSN-0043` - SystemOS Domain Facades
- `LSN-0044` - Task Window Liveness Belongs to the Task
## Revision Log
- 2026-05-15: Initial decision draft from AGD-0036.

View File

@ -1,107 +0,0 @@
---
id: PLN-0062
ticket: prometeu-hub-ui-direction
title: Native Shell Launch and Lifecycle Wiring
status: done
created: 2026-05-15
ref_decisions: [DEC-0028]
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
---
## Briefing
Implement the OS/Firmware wiring required by `DEC-0028` so the Hub can emit a launch intent for `ShellA` or `ShellB`, and SystemOS/Firmware can create a native Shell task, create a task-owned window, run the Shell state, close through lifecycle, and return to Hub/Home.
Source decision: `DEC-0028`.
## Objective
Make the launch/close path architecturally correct before the visual slice depends on it:
- Hub emits launch intent.
- SystemOS/Firmware creates native Shell task/process.
- The Shell app has a `WindowOwner::Task(task_id)` window.
- Close uses `os.lifecycle().close_task(task_id)`.
- `ShellRunningStep` returns to Hub/Home after close.
## Dependencies
- `DEC-0028` must remain accepted.
- This plan should execute before `PLN-0064`, because the UI buttons need a real launch target.
- This plan may execute before or alongside `PLN-0063` if tests can trigger Hub actions without mouse input.
## Scope
- Update the Hub update result shape so it can report launch/close actions, not only crashes.
- Add native fake Shell app identity for `ShellA` and `ShellB`.
- Wire Firmware/SystemOS launch handling from Hub intent to `sessions().create_native_shell_task(...)`.
- Create and focus a task-owned window for the launched native Shell task.
- Ensure app close routes to lifecycle close, not only window removal.
- Preserve the existing task-owned window liveness rule in `ShellRunningStep`.
## Non-Goals
- No generic app catalog.
- No reusable UI toolkit.
- No game cartridge window model.
- No background/minimize/overlay preservation semantics.
- No controller focus navigation.
- No public spec change unless implementation introduces a new public API.
## Execution Method
1. Inspect current launch state.
- What changes: identify the exact update path from `HubHomeStep` to `PrometeuHub::update_shell_profile` and `ShellRunningStep`.
- How: read the current firmware state transitions and Hub update result.
- Files: `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`, `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs`, `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`.
2. Replace crash-only Hub output with an action-bearing result.
- What changes: extend or replace `SystemProfileUpdate` so it can carry `LaunchNativeShell(ShellA|ShellB)` and close intent while preserving crash reporting.
- How: add a small enum local to the Hub/system profile boundary; keep visual components free of lifecycle mutation.
- Files: `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` and firmware call sites.
3. Handle launch in Firmware/SystemOS.
- What changes: when Hub emits launch intent, create the native Shell task through `os.sessions().create_native_shell_task(app_id, title)`.
- How: map `ShellA` and `ShellB` to stable local app ids/titles for this fake-app slice.
- Files: `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`; add helper code in `prometeu-system` only if needed to avoid duplication.
4. Create the task-owned Shell window.
- What changes: after task creation, create and focus a window with `WindowOwner::Task(task_id)`.
- How: use the existing window facade first; introduce a narrow helper only if the launch sequence becomes duplicated or leaks into visual components.
- Files: `crates/console/prometeu-system/src/os/facades/sessions.rs`, `crates/console/prometeu-system/src/os/facades/window.rs`, or firmware step depending on the smallest clean integration point.
5. Route close through lifecycle.
- What changes: app/window close must call `os.lifecycle().close_task(task_id)` or emit an equivalent action handled by Firmware/SystemOS.
- How: keep window removal separate from lifecycle semantics; do not treat `close_window` as app close.
- Files: `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`, `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs`.
6. Add behavior tests.
- What changes: add tests proving launch creates native Shell task/window and close returns to Hub.
- How: test at SystemOS/Firmware boundary rather than only manager internals.
- Files: existing firmware/system test modules, preferably near current lifecycle/window tests.
## Acceptance Criteria
- [x] Launching `ShellA` creates a native Shell task/process.
- [x] Launching `ShellB` creates a native Shell task/process.
- [x] Each launched Shell app receives a focused `WindowOwner::Task(task_id)` window.
- [x] Closing the Shell app closes lifecycle state through `os.lifecycle().close_task(task_id)`.
- [x] Closing the Shell app returns firmware to `HubHome`.
- [x] Hub visual code does not directly mutate task/process managers.
- [x] Existing shell liveness behavior remains intact.
## Tests
- Unit tests for any new Hub action enum or mapping logic.
- SystemOS tests for any new helper/facade that creates native Shell task plus task-owned window.
- Firmware tests for HubHome launch to ShellRunning and ShellRunning close to HubHome.
- Regression tests for `focused_window_belongs_to_task(task_id)` behavior if touched.
## Affected Artifacts
- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`
- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`
- `crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs`
- `crates/console/prometeu-system/src/os/facades/sessions.rs`
- `crates/console/prometeu-system/src/os/facades/window.rs`
- Existing SystemOS/Firmware tests

View File

@ -1,89 +0,0 @@
---
id: PLN-0063
ticket: prometeu-hub-ui-direction
title: Hub Mouse Input and Click Routing
status: done
created: 2026-05-15
ref_decisions: [DEC-0028]
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
---
## Briefing
Implement the input path needed by `DEC-0028`: Hub/Home must support mouse/click activation for the two manual Shell launch buttons and, in Shell app view, for the app/window close command.
Source decision: `DEC-0028`.
## Objective
Provide reliable 270p mouse hit testing and click routing for the initial Hub UI slice without introducing controller navigation or a generic input framework.
## Dependencies
- `DEC-0028` must remain accepted.
- This plan should execute before or alongside `PLN-0064`, because the visual buttons need clickable behavior.
- It depends on existing host pointer mapping if available; if unavailable, this plan must add the narrowest input bridge required for Hub clicks.
## Scope
- Confirm how host mouse coordinates are mapped into the internal framebuffer/viewport.
- Expose pointer position and click edge to the Hub update path.
- Add deterministic hit testing for `ShellA`, `ShellB`, and close controls.
- Preserve current game/controller input behavior.
## Non-Goals
- No controller focus navigation in this wave.
- No keyboard navigation beyond existing debug behavior.
- No drag/drop, resize, window move, scroll, hover menus, or double-click behavior.
- No generic retained-mode UI event system.
## Execution Method
1. Inspect current input bridge.
- What changes: identify existing pointer position and click signals.
- How: trace host `winit` input into HAL/runtime input signals.
- Files: `crates/host/prometeu-host-desktop-winit/src/input.rs`, HAL input signal definitions, firmware context wiring.
2. Add or expose a minimal pointer snapshot.
- What changes: make Hub update code able to read current pointer coordinates in 270p logical space and click pressed/released edge.
- How: reuse existing `window_to_fb` mapping and input structures if present; add a small field/method only if needed.
- Files: host input module, HAL input signal structures, firmware context.
3. Define Hub hit-test rectangles.
- What changes: create explicit rects for `ShellA`, `ShellB`, and close command.
- How: keep rect definitions close to the local Hub UI slice so visual and hit target stay synchronized.
- Files: `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`; optional local submodule if the file grows too large.
4. Route click results to Hub actions.
- What changes: clicking `ShellA` or `ShellB` emits the launch action from `PLN-0062`; clicking close emits close intent.
- How: hit test only on click edge; avoid repeated launch while button is held.
- Files: `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`.
5. Add tests around pure hit-test logic.
- What changes: test button rect boundaries and click-edge behavior without needing a desktop window.
- How: isolate hit-test/action selection into pure functions where practical.
- Files: Hub module tests or a local Hub UI submodule test.
## Acceptance Criteria
- [x] Pointer coordinates are interpreted in the internal 270p coordinate space.
- [x] Clicking the `ShellA` button emits the `ShellA` launch action exactly once per click.
- [x] Clicking the `ShellB` button emits the `ShellB` launch action exactly once per click.
- [x] Clicking the close command emits close intent for the active Shell app.
- [x] Holding the mouse button does not repeatedly launch apps.
- [x] Existing controller/game input paths are not regressed.
## Tests
- Unit tests for hit-test rectangles.
- Unit tests for click edge to action mapping.
- Existing host/system tests to ensure input changes compile across crates.
- Manual verification in desktop host: click `ShellA`, close, click `ShellB`, close.
## Affected Artifacts
- `crates/host/prometeu-host-desktop-winit/src/input.rs`
- HAL input signal definitions, if pointer data is not already exposed
- Firmware context/input plumbing, if needed
- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`

View File

@ -1,100 +0,0 @@
---
id: PLN-0064
ticket: prometeu-hub-ui-direction
title: Retro Hub Home and Fake Shell UI
status: done
created: 2026-05-15
ref_decisions: [DEC-0028]
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
---
## Briefing
Implement the visible Hub/Home and fake Shell app surfaces required by `DEC-0028`: a retro/minimalist 270p Hub with two manual buttons, plus green `ShellA` and blue `ShellB` native fake app/window views.
Source decision: `DEC-0028`.
## Objective
Replace the current debug-style window surface with the first Prometeu OS visual identity slice while keeping all UI primitives local and avoiding a reusable toolkit.
## Dependencies
- `PLN-0062` should provide the launch/close/lifecycle wiring.
- `PLN-0063` should provide click routing.
- `PLN-0065` may provide pixel font rendering; this plan must still remain executable with current text rendering if font integration is deferred.
## Scope
- Full 270p Hub/Home composition.
- Retro/minimalist palette, borders, panels, and two clickable launch buttons.
- Local drawing primitives for panel/frame/button/highlight only when needed.
- `ShellA` fake app view as green.
- `ShellB` fake app view as blue.
- App/window chrome with close affordance.
- Clear visual separation between Hub/Home and Shell app state.
## Non-Goals
- No full UI toolkit.
- No generic app catalog.
- No Tabs/List/Status bar/Footer/help bar.
- No controller focus UI.
- No production app plugin model.
- No game cartridge visual changes.
## Execution Method
1. Define the 270p layout constants.
- What changes: establish local coordinates for Hub background, title area, two buttons, and optional simple panel/frame.
- How: use fixed 480x270 logical viewport coordinates consistent with the runtime viewport.
- Files: `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs` or local Hub UI submodule.
2. Add local retro drawing primitives.
- What changes: add small helper functions for panel/frame, button, highlight, and simple text placement.
- How: keep them private/local to the Hub slice; do not expose as a toolkit.
- Files: Hub module or `crates/console/prometeu-system/src/programs/prometeu_hub/` submodules.
3. Render Hub/Home.
- What changes: draw a consistent retro/minimalist Home surface with exactly two launch buttons.
- How: use low-resolution composition, strong outlines, limited color use, and button labels `ShellA` and `ShellB`.
- Files: Hub render path.
4. Render native fake Shell app views.
- What changes: draw `ShellA` as a green app/window and `ShellB` as a blue app/window, with a visible close command.
- How: use the task-owned window information and app identity established by `PLN-0062`.
- Files: Hub render path and any local fake-app state/mapping.
5. Keep visual state separate from lifecycle state.
- What changes: UI code may know the current view/app identity but must not mutate lifecycle directly.
- How: return actions to the boundary defined by `PLN-0062`.
- Files: Hub update/render code.
6. Add lightweight tests for layout/action coupling.
- What changes: ensure visible button rects match clickable rects from `PLN-0063`.
- How: put button definitions in a shared local data structure used by render and hit testing.
- Files: Hub module tests.
## Acceptance Criteria
- [x] Hub/Home renders full-screen in the 270p viewport.
- [x] Hub/Home shows exactly two manual launch buttons: `ShellA` and `ShellB`.
- [x] The visual style is retro/minimalist with strong borders and limited palette.
- [x] `ShellA` renders as a simple green native fake Shell app/window.
- [x] `ShellB` renders as a simple blue native fake Shell app/window.
- [x] Each Shell app view exposes a visible close command.
- [x] Local primitives are not exported as a general toolkit.
- [x] No Tabs/List/Status bar/Footer/help bar are added in this wave.
## Tests
- Unit tests for shared local layout/button definitions.
- Compile and runtime tests from `PLN-0062` and `PLN-0063`.
- Manual desktop verification of the full flow: Hub -> ShellA -> close -> Hub -> ShellB -> close -> Hub.
- Screenshot/manual visual check at 480x270 internal composition if repository tooling supports it.
## Affected Artifacts
- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`
- Possible local modules under `crates/console/prometeu-system/src/programs/prometeu_hub/`
- Tests colocated with Hub module

View File

@ -1,103 +0,0 @@
---
id: PLN-0065
ticket: prometeu-hub-ui-direction
title: Pixel Operator Font Integration
status: done
created: 2026-05-15
ref_decisions: [DEC-0028]
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
---
## Briefing
Integrate the already-available Pixel Operator font asset into the first Hub UI slice when the existing rendering path can support it without creating a new public font system.
Source decision: `DEC-0028`.
## Objective
Use pixel-style typography for the Hub/Home and fake Shell app labels while keeping font work scoped to the initial UI slice.
## Dependencies
- `DEC-0028` must remain accepted.
- `PLN-0064` can proceed without this plan if font integration exposes unrelated architecture work.
- The asset is expected to exist already under `assets/pixel_operator` or from `assets/pixel_operator.zip`.
## Scope
- Locate and verify the extracted Pixel Operator asset.
- Determine the current text rendering path available to Hub/System UI.
- Add a local font binding or asset loading path if one already fits the runtime.
- Use the font for Hub button labels and simple Shell app text.
- Document any reason this must be deferred.
## Non-Goals
- No general font manager unless one already exists and only needs registration.
- No text layout engine.
- No multilingual shaping.
- No public asset/font spec change unless implementation requires a durable contract.
- No dependency download.
## Execution Method
1. Verify the asset.
- What changes: confirm exact files and format under `assets/pixel_operator`.
- How: inspect asset directory and identify usable font/bitmap files.
- Files: `assets/pixel_operator*`.
2. Inspect current text rendering.
- What changes: determine whether Hub can draw text through existing HAL/GFX primitives or asset-backed glyph rendering.
- How: search current text/glyph/font rendering APIs and examples.
- Files: `crates/console/prometeu-hal`, `crates/console/prometeu-system`, current asset/glyph code.
3. Add the narrowest local integration.
- What changes: make Hub text use Pixel Operator if the path is straightforward.
- How: register/load the asset in the same style as existing assets; keep code local to Hub UI if there is no general font contract.
- Files: Hub module and asset-loading modules identified in step 2.
4. Add fallback behavior.
- What changes: if font rendering is not currently suitable, keep the visual slice working with existing text/primitive rendering and record a follow-up.
- How: do not block `PLN-0064` on broad font architecture.
- Files: Hub module and this plan's execution notes.
5. Validate typography use.
- What changes: button labels and simple Shell app labels render legibly in 270p.
- How: manual visual verification plus any available render tests.
- Files: Hub render path.
## Acceptance Criteria
- [x] Pixel Operator asset location and format are verified.
- [x] Hub text rendering uses Pixel Operator when the existing rendering path supports it.
- [x] If deferred, the reason is recorded and `PLN-0064` remains executable.
- [x] No broad font/text toolkit is introduced.
- [x] Text remains legible at 270p.
## Tests
- Compile tests for any asset/font binding.
- Unit tests only if new parsing/registration logic is added.
- Manual visual verification of button labels and Shell labels.
## Affected Artifacts
- `assets/pixel_operator*`
- `crates/console/prometeu-system/src/programs/prometeu_hub/`
- Existing HAL/GFX/asset modules if they already host font or glyph rendering
## Execution Notes
The asset is present under `assets/pixel-operator/` as TrueType files, including:
- `PixelOperator.ttf`
- `PixelOperator8.ttf`
- `PixelOperatorMono.ttf`
- `PixelOperatorMono8.ttf`
The included license is CC0 1.0 Universal.
The current runtime text path does not have a TTF loader, font manager, glyph rasterizer, or asset-backed font registration path. Hub/System UI currently reaches text through `GfxBridge::draw_text`, backed by the built-in 3x5 bitmap glyph path in `crates/console/prometeu-drivers/src/gfx.rs`.
Because `DEC-0028` explicitly forbids expanding this wave into a broad toolkit and this plan forbids introducing a general font manager/text engine, Pixel Operator rendering is deferred. `PLN-0064` remains executable and tested with the existing bitmap text path, which is already pixel-style and legible at 270p.

View File

@ -1,114 +0,0 @@
---
id: PLN-0066
ticket: prometeu-hub-ui-direction
title: Hub UI Slice Validation and Lesson
status: done
created: 2026-05-15
ref_decisions: [DEC-0028]
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
---
## Briefing
Validate the completed `DEC-0028` implementation family, collect evidence, and prepare the post-execution lesson required by the repository workflow.
Source decision: `DEC-0028`.
## Objective
Prove that the initial Hub retro Shell UI slice satisfies the accepted decision and leave the discussion workflow ready for housekeeping after implementation.
## Dependencies
- `PLN-0062` must be implemented.
- `PLN-0063` must be implemented or explicitly documented as unnecessary because mouse/click was already available.
- `PLN-0064` must be implemented.
- `PLN-0065` must be implemented or explicitly deferred with evidence.
## Scope
- Run relevant crate tests.
- Perform manual desktop verification of the end-to-end flow.
- Check that no forbidden scope leaked into implementation.
- Record evidence in plan execution notes or final implementation summary.
- Draft an English lesson after execution is complete.
## Non-Goals
- No new UI implementation work except small fixes required to satisfy acceptance criteria.
- No new architecture decisions.
- No lesson before code/spec state is published.
- No housekeeping until plans are done.
## Execution Method
1. Run automated tests.
- What changes: execute the crates affected by plans `PLN-0062` through `PLN-0065`.
- How: run targeted cargo tests for firmware/system/host crates.
- Files: test output only; no source changes unless failures expose implementation bugs.
2. Verify end-to-end desktop behavior.
- What changes: manually exercise Hub -> ShellA -> close -> Hub -> ShellB -> close -> Hub.
- How: run the desktop host or project runner used for Hub boot, click the buttons, and observe lifecycle return.
- Files: none, unless bugs are found.
3. Check decision compliance.
- What changes: inspect implementation against `DEC-0028` invariants.
- How: verify no toolkit, no catalog, no Tabs/List/Footer, no direct task/process manager mutation from visual components, and task-owned Shell windows.
- Files: implementation files from the prior plans.
4. Record validation evidence.
- What changes: update the relevant plans or implementation summary with commands run, manual checks, failures, and fixes.
- How: keep evidence concise and tied to acceptance criteria.
- Files: plan artifacts if the workflow records execution notes there.
5. Prepare post-execution lesson.
- What changes: create an English lesson only after implementation is complete and published.
- How: use `discussion-housekeep` when all plans are done; lesson should explain how to think about Hub visual slices without making lessons normative.
- Files: `discussion/lessons/DSC-0036-prometeu-hub-ui-direction/...`.
## Acceptance Criteria
- [x] Automated tests for affected crates pass or failures are documented as unrelated.
- [x] Manual flow passes: Hub -> ShellA -> close -> Hub.
- [x] Manual flow passes: Hub -> ShellB -> close -> Hub.
- [x] Shell windows are task-owned.
- [x] App close uses lifecycle close.
- [x] No complete UI toolkit or catalog was introduced.
- [x] Pixel font integration outcome is recorded.
- [x] A post-execution lesson is ready for housekeeping after plans are marked done.
## Execution Notes
Validation evidence:
- `cargo test -p prometeu-system`: 83 passed.
- `cargo test -p prometeu-firmware`: 17 passed.
- `cargo test -p prometeu-host-desktop-winit`: 25 passed, 5 ignored.
- `discussion validate`: passed.
The ShellA/ShellB flows were verified through firmware integration tests that
simulate Hub click input and Shell close input. A live desktop manual click
session was not run in this environment; the host crate still compiles and its
test suite passes.
Lesson prepared:
- `discussion/lessons/DSC-0036-prometeu-hub-ui-direction/LSN-0045-hub-ui-slices-should-prove-os-boundaries.md`
## Tests
- `cargo test -p prometeu-system`
- `cargo test -p prometeu-firmware`
- `cargo test -p prometeu-host-desktop-winit` if host/input code changed
- Manual desktop host verification
- `discussion validate` after marking plans and housekeeping artifacts
## Affected Artifacts
- Implementation files touched by `PLN-0062` through `PLN-0065`
- `discussion/workflow/plans/PLN-0062-native-shell-launch-and-lifecycle-wiring.md`
- `discussion/workflow/plans/PLN-0063-hub-mouse-input-and-click-routing.md`
- `discussion/workflow/plans/PLN-0064-retro-hub-home-and-fake-shell-ui.md`
- `discussion/workflow/plans/PLN-0065-pixel-operator-font-integration.md`
- Future lesson under `discussion/lessons/DSC-0036-prometeu-hub-ui-direction/`

View File

@ -35,7 +35,7 @@ These are treated as VM values with stable layout semantics.
| Type | Description |
| ------- | --------------------------------- |
| `vec2` | 2D vector (x, y) |
| `color` | Packed color value |
| `color` | Packed RGBA8888 color value |
| `pixel` | Combination of position and color |
These values:
@ -44,6 +44,9 @@ These values:
- are copied by value;
- do not require heap allocation by themselves.
The canonical `color` raw channel order is RGBA. RGB565 is not a public VM
color contract.
### Heap values
Heap-resident entities are referenced indirectly through handles.

View File

@ -29,13 +29,20 @@ It is an explicit 2D graphics device based on:
### Pixel format
- **RGB565**
- 5 bits Red
- 6 bits Green
- 5 bits Blue
- no alpha channel
- **RGBA8888**
- 8 bits Red
- 8 bits Green
- 8 bits Blue
- 8 bits Alpha
- canonical raw channel order: **RGBA**
Transparency is handled via **color key**.
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.
---
@ -232,16 +239,21 @@ Normative boundary:
---
## 12. Transparency (Color Key)
## 12. Transparency
- One RGB565 value is reserved as TRANSPARENT_KEY
- Pixels with this color are not drawn
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 src == TRANSPARENT_KEY:
if resolved_color.alpha == 0:
skip
else:
draw
draw_or_blend(resolved_color)
```
@ -259,8 +271,9 @@ Official modes:
- `BLEND_HALF_MINUS`
- `BLEND_FULL`
No continuous alpha.
No arbitrary blending.
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:
@ -283,12 +296,13 @@ Everything is:
By design:
- Continuous alpha
- RGBA framebuffer
- Shaders
- Modern GPU pipeline
- HDR
- Gamma correction
- RGB565 compatibility framebuffers
- multi-format backend selection
- render-thread ownership as part of this contract
---
@ -314,8 +328,7 @@ controls:
In v1, deferred `gfx.*` overlay/debug primitives are drained after both fades and therefore are not themselves part of scene or HUD fade application.
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
The fade uses a **discrete integer level** (0..31), which in practice produces an
"almost continuous" visual result in Prometeu's `480x270` pixel-art framebuffer.
---
@ -327,32 +340,32 @@ 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`
- `fade_color: RGBA8888`
- color the image will be blended into
Registers:
- `SCENE_FADE_LEVEL` (0..31)
- `SCENE_FADE_COLOR` (RGB565)
- `SCENE_FADE_COLOR` (RGBA8888)
- `HUD_FADE_LEVEL` (0..31)
- `HUD_FADE_COLOR` (RGB565)
- `HUD_FADE_COLOR` (RGBA8888)
Common cases:
- Fade-out: `fade_color = BLACK`
- Flash/teleport: `fade_color = WHITE`
- Special effects: any RGB565 color
- Special effects: any RGBA8888 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.
For each RGBA8888 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`
- `src_r8`, `src_g8`, `src_b8`, `src_a8`
- `fc_r8`, `fc_g8`, `fc_b8`, `fc_a8`
2) Apply integer blending:
@ -360,17 +373,18 @@ For each RGB565 pixel `src` and fade color `fc`, the final pixel `dst` is calcul
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
r8 = (src_r8 * src_weight + fc_r8 * fc_weight) / 31
g8 = (src_g8 * src_weight + fc_g8 * fc_weight) / 31
b8 = (src_b8 * src_weight + fc_b8 * fc_weight) / 31
a8 = (src_a8 * src_weight + fc_a8 * fc_weight) / 31
```
- `src_r5`, `src_g6`, `src_b5`
- `fc_r5`, `fc_g6`, `fc_b5`
- `src_r8`, `src_g8`, `src_b8`, `src_a8`
- `fc_r8`, `fc_g8`, `fc_b8`, `fc_a8`
3) Repack:
```
dst = pack_rgb565(r5, g6, b5)
dst = pack_rgba8888_rgba(r8, g8, b8, a8)
```
@ -425,8 +439,8 @@ Each pixel of a tile or sprite is:
Fixed rule:
- Index `0` = TRANSPARENT
- Indices `1..15` = valid palette colors
- Indices `0..15` are ordinary valid palette indices
- Transparency comes from the alpha channel of the resolved palette entry
---
@ -437,13 +451,13 @@ 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)**
- each color in **RGBA8888** with canonical RGBA channel order
Size:
- 1 palette = 16 × 2 bytes = **32 bytes**
- 64 palettes = **2 KB per bank**
- 16 banks = **32 KB maximum palettes**
- 1 palette = 16 × 4 bytes = **64 bytes**
- 64 palettes = **4 KB per bank**
- 16 banks = **64 KB maximum palettes**
---
@ -496,22 +510,22 @@ Runtime-facing validity rule for v1:
The pipeline works like this:
1. Read indexed pixel from tile (value 0..15)
2. If index == 0 → transparent pixel
3. Otherwise:
2. Resolve:
- real_color = palette[palette_id][index]
4. Apply:
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)
if pixel_index == 0:
color = bank.palettes[palette_id][pixel_index]
if color.alpha == 0:
skip
else:
color = bank.palettes[palette_id][pixel_index]
draw(color)
draw_or_blend(color)
```
---
@ -578,7 +592,6 @@ Fault boundary:
| `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` | 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 |

View File

@ -116,7 +116,7 @@ Hardware differences:
The graphics system:
- operates on a logical framebuffer
- operates on a logical RGBA8888 framebuffer
- uses an indexed palette
- does not depend on a specific GPU
@ -127,6 +127,10 @@ The platform layer:
- **may overlay technical HUDs without modifying the logical framebuffer**
- may transport the logical framebuffer into a host presentation surface where a host-only overlay layer is composed
The host presentation layer MUST treat RGBA8888 as the canonical logical
framebuffer format. RGB565 conversion is not part of the normal host
presentation contract.
Host presentation SHOULD be driven by published logical frames and explicit host-owned invalidation, not by perpetual redraw polling.
In particular:

View File

@ -114,7 +114,7 @@ For `BankType::GLYPH`, the v1 runtime-facing contract is:
- `codec = NONE`
- serialized pixels use packed `u4` palette indices
- serialized palettes use `RGB565` (`u16`, little-endian)
- serialized palettes use `RGBA8888` with canonical RGBA channel order
- `palette_count = 64`
- runtime materialization may expand pixel indices to one `u8` per pixel
@ -152,18 +152,25 @@ Validation rules for `GLYPH` v1:
#### 4.1.2 Payload Layout
1. packed indexed pixels for the full sheet, using `ceil(width * height / 2)` bytes;
2. palette table, using `palette_count * 16 * 2` bytes.
2. palette table, using `palette_count * 16 * 4` bytes.
The tile-bank payload therefore separates serialized storage form from runtime memory form:
- serialized pixel plane: packed `4bpp`
- decoded pixel plane: expanded `u8` indices, one entry per pixel
- palette table: `64 * 16` colors in `RGB565`
- palette table: `64 * 16` colors in RGBA8888 channel order
For `GLYPH` v1:
- `size` must match `ceil(width * height / 2) + (palette_count * 16 * 2)`
- `decoded_size` must match `(width * height) + (palette_count * 16 * 2)`
- `size` must match `ceil(width * height / 2) + (palette_count * 16 * 4)`
- `decoded_size` must match `(width * height) + (palette_count * 16 * 4)`
RGB565 palette payloads are not compatible runtime input. Existing assets with
two-byte RGB565 palette entries must be regenerated or converted by tooling
before they are loaded by the runtime.
Palette indices are ordinary indices. Transparency is represented by the alpha
channel of the resolved RGBA8888 palette entry, not by reserving index `0`.
### 4.2 `SCENE` asset contract in v1

View File

@ -220,6 +220,30 @@ For `asset.load`:
- JSON-on-the-wire bank inspection payloads are not valid public ABI;
- `bank.info` returns stack values, not textual structured payloads.
### GFX surface (`gfx`, v1)
The public GFX ABI uses format-neutral operation names. API and syscall names
MUST NOT encode RGB565 or any other pixel format suffix.
Canonical operations in v1 include:
- `gfx.clear(color) -> void`
- `gfx.fill_rect(x, y, w, h, color) -> void`
- `gfx.draw_line(x0, y0, x1, y1, color) -> void`
- `gfx.draw_circle(x, y, radius, color) -> void`
- `gfx.draw_disc(x, y, radius, color) -> void`
- `gfx.draw_square(x, y, size, color) -> void`
- `gfx.draw_text(x, y, text, color) -> void`
Rules:
- `color` arguments are packed RGBA8888 values using RGBA channel order.
- RGB565 raw values are not a public ABI contract.
- `Gfx*565` syscall names and `gfx.*_565` canonical identities are not valid
current ABI.
- The runtime framebuffer exposed across the host boundary is RGBA8888, not
`u16` RGB565.
### Composition surface (`composer`, v1)
The canonical frame-orchestration public ABI uses module `composer`.

View File

@ -40,7 +40,7 @@
]
},
"firmware": {
"baseline_percent": 72,
"baseline_percent": 80,
"paths": [
"crates/console/prometeu-firmware/src/"
]