From 105a6de11e661386604fc1a2873ef14bd55568e0 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 25 May 2026 11:37:58 +0100 Subject: [PATCH] implements PLN-0074 HAL render submissions --- crates/console/prometeu-hal/src/lib.rs | 6 + .../prometeu-hal/src/render_submission.rs | 203 ++++++++++++++++++ discussion/index.ndjson | 2 +- .../PLN-0074-hal-render-submission-types.md | 2 +- 4 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 crates/console/prometeu-hal/src/render_submission.rs diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index fe0cd2e9..a9e65eef 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -20,6 +20,7 @@ pub mod native_helpers; pub mod native_interface; pub mod pad_bridge; pub mod primitives; +pub mod render_submission; pub mod sample; pub mod scene_bank; pub mod scene_layer; @@ -45,4 +46,9 @@ pub use input_signals::InputSignals; pub use native_helpers::{expect_bool, expect_int}; pub use native_interface::{NativeInterface, SyscallId}; pub use pad_bridge::PadBridge; +pub use render_submission::{ + BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket, + Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderSubmission, RenderSubmissionPacket, + ShellUiFramePacket, +}; pub use touch_bridge::TouchBridge; diff --git a/crates/console/prometeu-hal/src/render_submission.rs b/crates/console/prometeu-hal/src/render_submission.rs new file mode 100644 index 00000000..3427f581 --- /dev/null +++ b/crates/console/prometeu-hal/src/render_submission.rs @@ -0,0 +1,203 @@ +use crate::app_mode::AppMode; +use crate::color::Color; +use crate::primitives::Rect; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct FrameId(pub u64); + +impl FrameId { + pub const ZERO: Self = Self(0); + + pub const fn new(value: u64) -> Self { + Self(value) + } + + pub const fn get(self) -> u64 { + self.0 + } + + pub const fn next(self) -> Self { + Self(self.0.wrapping_add(1)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RenderSubmission { + pub frame_id: FrameId, + pub app_mode: AppMode, + pub packet: RenderSubmissionPacket, +} + +impl RenderSubmission { + pub const fn game2d(frame_id: FrameId, packet: Game2DFramePacket) -> Self { + Self { frame_id, app_mode: AppMode::Game, packet: RenderSubmissionPacket::Game2D(packet) } + } + + pub const fn shell_ui(frame_id: FrameId, packet: ShellUiFramePacket) -> Self { + Self { frame_id, app_mode: AppMode::Shell, packet: RenderSubmissionPacket::ShellUi(packet) } + } + + pub const fn is_coherent(&self) -> bool { + matches!( + (self.app_mode, &self.packet), + (AppMode::Game, RenderSubmissionPacket::Game2D(_)) + | (AppMode::Shell, RenderSubmissionPacket::ShellUi(_)) + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RenderSubmissionPacket { + Game2D(Game2DFramePacket), + ShellUi(ShellUiFramePacket), +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Game2DFramePacket { + pub composer: ComposerFramePacket, + pub gfx2d: Vec, +} + +impl Game2DFramePacket { + pub fn new(composer: ComposerFramePacket, gfx2d: Vec) -> Self { + Self { composer, gfx2d } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ShellUiFramePacket { + pub commands: Vec, +} + +impl ShellUiFramePacket { + pub fn new(commands: Vec) -> Self { + Self { commands } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ComposerFramePacket { + pub bound_scene: Option, + pub camera: Camera2D, + pub sprites: Vec, + pub hud: HudPacket, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BoundScenePacket { + pub bank_id: i32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Camera2D { + pub x: i32, + pub y: i32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GameSpritePacket { + pub glyph_id: i32, + pub palette_id: i32, + pub x: i32, + pub y: i32, + pub layer: i32, + pub bank_id: i32, + pub flip_x: bool, + pub flip_y: bool, + pub priority: i32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct HudPacket { + pub commands: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HudCommand { + Clear { color: Color }, + FillRect { rect: Rect, color: Color }, + DrawText { x: i32, y: i32, text: String, color: Color }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Gfx2dCommand { + Clear { color: Color }, + FillRect { rect: Rect, color: Color }, + DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color }, + DrawCircle { x: i32, y: i32, radius: i32, color: Color }, + DrawDisc { x: i32, y: i32, radius: i32, border_color: Color, fill_color: Color }, + DrawSquare { rect: Rect, border_color: Color, fill_color: Color }, + DrawText { x: i32, y: i32, text: String, color: Color }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GfxUiCommand { + Clear { color: Color }, + FillRect { rect: Rect, color: Color }, + DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color }, + DrawCircle { x: i32, y: i32, radius: i32, color: Color }, + DrawDisc { x: i32, y: i32, radius: i32, border_color: Color, fill_color: Color }, + DrawSquare { rect: Rect, border_color: Color, fill_color: Color }, + DrawText { x: i32, y: i32, text: String, color: Color }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn constructors_pin_app_mode_to_packet_kind() { + let game = RenderSubmission::game2d(FrameId::new(7), Game2DFramePacket::default()); + assert_eq!(game.app_mode, AppMode::Game); + assert!(matches!(game.packet, RenderSubmissionPacket::Game2D(_))); + assert!(game.is_coherent()); + + let shell = + RenderSubmission::shell_ui(FrameId::new(8), ShellUiFramePacket::new(Vec::new())); + assert_eq!(shell.app_mode, AppMode::Shell); + assert!(matches!(shell.packet, RenderSubmissionPacket::ShellUi(_))); + assert!(shell.is_coherent()); + } + + #[test] + fn coherence_check_rejects_mismatched_envelope() { + let submission = RenderSubmission { + frame_id: FrameId::new(1), + app_mode: AppMode::Game, + packet: RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(Vec::new())), + }; + + assert!(!submission.is_coherent()); + } + + #[test] + fn closed_submission_owns_command_payload() { + let mut commands = vec![Gfx2dCommand::DrawText { + x: 1, + y: 2, + text: "before".to_string(), + color: Color::WHITE, + }]; + let submission = RenderSubmission::game2d( + FrameId::new(2), + Game2DFramePacket::new(Default::default(), commands.clone()), + ); + + commands.push(Gfx2dCommand::Clear { color: Color::BLACK }); + + let RenderSubmissionPacket::Game2D(packet) = submission.packet else { + panic!("expected game packet"); + }; + assert_eq!(packet.gfx2d.len(), 1); + assert_eq!( + packet.gfx2d[0], + Gfx2dCommand::DrawText { x: 1, y: 2, text: "before".to_string(), color: Color::WHITE } + ); + } + + #[test] + fn frame_id_advances_monotonically_with_wrapping_arithmetic() { + assert_eq!(FrameId::new(41).next(), FrameId::new(42)); + assert_eq!(FrameId::new(u64::MAX).next(), FrameId::ZERO); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 44cf0312..e6882b90 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":39,"AGD":39,"DEC":31,"PLN":87,"LSN":47,"CLSN":1}} -{"type":"discussion","id":"DSC-0038","status":"in_progress","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-05-25","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25"}],"decisions":[{"id":"DEC-0030","file":"DEC-0030-logical-render-pipeline-command-boundary.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0073","file":"PLN-0073-render-contract-specs.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0074","file":"PLN-0074-hal-render-submission-types.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0075","file":"PLN-0075-rendermanager-core.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0076","file":"PLN-0076-capabilities-and-abi-domain-split.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0077","file":"PLN-0077-composer-buffer-and-game2d-packet.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0078","file":"PLN-0078-classic2d-game-renderer-consumer.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0079","file":"PLN-0079-gfx2d-primitive-domain.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0080","file":"PLN-0080-shell-ui-packet-and-gfxui-domain.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0081","file":"PLN-0081-shell-hub-migration.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0082","file":"PLN-0082-frame-publication-and-present-boundary.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0083","file":"PLN-0083-fade-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0084","file":"PLN-0084-host-debug-overlay-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0085","file":"PLN-0085-pbs-stdlib-and-syscall-declarations.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0086","file":"PLN-0086-end-to-end-render-boundary-validation.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0038","status":"in_progress","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-05-25","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25"}],"decisions":[{"id":"DEC-0030","file":"DEC-0030-logical-render-pipeline-command-boundary.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0073","file":"PLN-0073-render-contract-specs.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0074","file":"PLN-0074-hal-render-submission-types.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0075","file":"PLN-0075-rendermanager-core.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0076","file":"PLN-0076-capabilities-and-abi-domain-split.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0077","file":"PLN-0077-composer-buffer-and-game2d-packet.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0078","file":"PLN-0078-classic2d-game-renderer-consumer.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0079","file":"PLN-0079-gfx2d-primitive-domain.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0080","file":"PLN-0080-shell-ui-packet-and-gfxui-domain.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0081","file":"PLN-0081-shell-hub-migration.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0082","file":"PLN-0082-frame-publication-and-present-boundary.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0083","file":"PLN-0083-fade-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0084","file":"PLN-0084-host-debug-overlay-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0085","file":"PLN-0085-pbs-stdlib-and-syscall-declarations.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0086","file":"PLN-0086-end-to-end-render-boundary-validation.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]}],"lessons":[]} {"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"}]} diff --git a/discussion/workflow/plans/PLN-0074-hal-render-submission-types.md b/discussion/workflow/plans/PLN-0074-hal-render-submission-types.md index 9909f207..d405e426 100644 --- a/discussion/workflow/plans/PLN-0074-hal-render-submission-types.md +++ b/discussion/workflow/plans/PLN-0074-hal-render-submission-types.md @@ -2,7 +2,7 @@ id: PLN-0074 ticket: render-frame-packet-boundary title: HAL Render Submission Types -status: open +status: done created: 2026-05-25 ref_decisions: [DEC-0030] tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]