From 5e3cb87b57930d20e4e0ef6a4971c33e8f8fa77c Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 25 May 2026 14:05:28 +0100 Subject: [PATCH] implements PLN-0077 composer buffer packet --- .../prometeu-drivers/src/frame_composer.rs | 123 +++++++++++++++++- .../console/prometeu-drivers/src/hardware.rs | 5 + crates/console/prometeu-drivers/src/lib.rs | 2 +- .../prometeu-hal/src/hardware_bridge.rs | 2 + .../src/services/vm_runtime/tick.rs | 12 +- discussion/index.ndjson | 2 +- ...-0077-composer-buffer-and-game2d-packet.md | 2 +- 7 files changed, 142 insertions(+), 6 deletions(-) diff --git a/crates/console/prometeu-drivers/src/frame_composer.rs b/crates/console/prometeu-drivers/src/frame_composer.rs index 55d424be..0ddc660e 100644 --- a/crates/console/prometeu-drivers/src/frame_composer.rs +++ b/crates/console/prometeu-drivers/src/frame_composer.rs @@ -1,6 +1,5 @@ use crate::asset::GlyphAssetSlotIndex; use crate::memory_banks::SceneBankPoolAccess; -use prometeu_hal::GfxBridge; use prometeu_hal::glyph::Glyph; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::scene_viewport_cache::SceneViewportCache; @@ -8,6 +7,10 @@ use prometeu_hal::scene_viewport_resolver::{ CacheRefreshRequest, ResolverUpdate, SceneViewportResolver, }; use prometeu_hal::sprite::Sprite; +use prometeu_hal::{ + BoundScenePacket, Camera2D, ComposerFramePacket, Game2DFramePacket, GameSpritePacket, + GfxBridge, HudPacket, +}; use std::sync::Arc; const EMPTY_SPRITE: Sprite = Sprite { @@ -132,6 +135,28 @@ pub struct FrameComposer { sprite_controller: SpriteController, } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ComposerBuffer { + pub bound_scene: Option, + pub camera: Camera2D, + pub sprites: Vec, + pub hud: HudPacket, +} + +impl ComposerBuffer { + pub fn close(self) -> Game2DFramePacket { + Game2DFramePacket::new( + ComposerFramePacket { + bound_scene: self.bound_scene, + camera: self.camera, + sprites: self.sprites, + hud: self.hud, + }, + Vec::new(), + ) + } +} + impl FrameComposer { pub fn new( viewport_width_px: usize, @@ -246,6 +271,35 @@ impl FrameComposer { self.sprite_controller.ordered_sprites() } + pub fn composer_buffer_snapshot(&self) -> ComposerBuffer { + ComposerBuffer { + bound_scene: self + .active_scene_id + .map(|scene_bank_id| BoundScenePacket { bank_id: scene_bank_id as i32 }), + camera: Camera2D { x: self.camera_x_px, y: self.camera_y_px }, + sprites: self + .ordered_sprites() + .into_iter() + .map(|sprite| GameSpritePacket { + glyph_id: sprite.glyph.glyph_id as i32, + palette_id: sprite.glyph.palette_id as i32, + x: sprite.x, + y: sprite.y, + layer: sprite.layer as i32, + bank_id: sprite.bank_id as i32, + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + priority: sprite.priority as i32, + }) + .collect(), + hud: HudPacket::default(), + } + } + + pub fn close_game2d_packet(&self) -> Game2DFramePacket { + self.composer_buffer_snapshot().close() + } + pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) { let ordered_sprites = self.ordered_sprites(); gfx.load_frame_sprites(&ordered_sprites); @@ -759,6 +813,73 @@ mod tests { assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw()); } + #[test] + fn composer_buffer_snapshot_closes_into_game2d_packet_without_fade_state() { + let mut frame_composer = FrameComposer::new( + Hardware::W, + Hardware::H, + Arc::new(MemoryBanks::default()), + make_glyph_slot_index(&[]), + ); + + frame_composer.set_camera(10, 20); + frame_composer.begin_frame(); + assert!(frame_composer.emit_sprite(Sprite { + glyph: Glyph { glyph_id: 7, palette_id: 3 }, + x: 4, + y: 5, + layer: 2, + bank_id: 1, + active: false, + flip_x: true, + flip_y: false, + priority: 9, + })); + + let packet = frame_composer.close_game2d_packet(); + + assert_eq!(packet.composer.bound_scene, None); + assert_eq!(packet.composer.camera, Camera2D { x: 10, y: 20 }); + assert_eq!(packet.composer.sprites.len(), 1); + assert_eq!(packet.composer.sprites[0].glyph_id, 7); + assert_eq!(packet.composer.sprites[0].palette_id, 3); + assert_eq!(packet.composer.sprites[0].layer, 2); + assert!(packet.composer.sprites[0].flip_x); + assert!(packet.composer.hud.commands.is_empty()); + assert!(packet.gfx2d.is_empty()); + } + + #[test] + fn composer_buffer_preserves_integer_layer_ordering() { + let mut frame_composer = FrameComposer::new( + Hardware::W, + Hardware::H, + Arc::new(MemoryBanks::default()), + make_glyph_slot_index(&[]), + ); + frame_composer.begin_frame(); + + for (glyph_id, layer, priority) in [(1, 3, 1), (2, 1, 2), (3, 1, 1), (4, 0, 9)] { + assert!(frame_composer.emit_sprite(Sprite { + glyph: Glyph { glyph_id, palette_id: 0 }, + x: 0, + y: 0, + layer, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority, + })); + } + + let packet = frame_composer.close_game2d_packet(); + let glyph_order: Vec = + packet.composer.sprites.iter().map(|sprite| sprite.glyph_id).collect(); + + assert_eq!(glyph_order, vec![4, 3, 2, 1]); + } + #[test] fn render_frame_with_scene_applies_refreshes_before_composition() { let banks = Arc::new(MemoryBanks::new()); diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 71720ce1..590002f0 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -8,6 +8,7 @@ use crate::memory_banks::{ }; use crate::pad::Pad; use crate::touch::Touch; +use prometeu_hal::Game2DFramePacket; use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::sprite::Sprite; use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge}; @@ -67,6 +68,10 @@ impl HardwareBridge for Hardware { self.frame_composer.emit_sprite(sprite) } + fn close_game2d_packet(&self) -> Game2DFramePacket { + self.frame_composer.close_game2d_packet() + } + fn render_frame(&mut self) { self.frame_composer.render_frame(&mut self.gfx); } diff --git a/crates/console/prometeu-drivers/src/lib.rs b/crates/console/prometeu-drivers/src/lib.rs index 9a088424..c0e4b650 100644 --- a/crates/console/prometeu-drivers/src/lib.rs +++ b/crates/console/prometeu-drivers/src/lib.rs @@ -9,7 +9,7 @@ mod touch; pub use crate::asset::AssetManager; pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; -pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController}; +pub use crate::frame_composer::{ComposerBuffer, FrameComposer, SceneStatus, SpriteController}; pub use crate::gfx::Gfx; pub use crate::memory_banks::{ GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess, diff --git a/crates/console/prometeu-hal/src/hardware_bridge.rs b/crates/console/prometeu-hal/src/hardware_bridge.rs index 28ccb77f..639cbffa 100644 --- a/crates/console/prometeu-hal/src/hardware_bridge.rs +++ b/crates/console/prometeu-hal/src/hardware_bridge.rs @@ -2,6 +2,7 @@ use crate::asset_bridge::AssetBridge; use crate::audio_bridge::AudioBridge; use crate::gfx_bridge::GfxBridge; use crate::pad_bridge::PadBridge; +use crate::render_submission::Game2DFramePacket; use crate::sprite::Sprite; use crate::touch_bridge::TouchBridge; @@ -11,6 +12,7 @@ pub trait HardwareBridge { fn unbind_scene(&mut self); fn set_camera(&mut self, x: i32, y: i32); fn emit_sprite(&mut self, sprite: Sprite) -> bool; + fn close_game2d_packet(&self) -> Game2DFramePacket; fn render_frame(&mut self); fn has_glyph_bank(&self, bank_id: usize) -> bool; diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index a4e189e7..43510f52 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -5,7 +5,7 @@ use crate::fs::{FsState, VirtualFS}; use crate::services::memcard::MemcardService; use prometeu_hal::asset::{BankTelemetry, BankType}; use prometeu_hal::log::{LogLevel, LogService, LogSource}; -use prometeu_hal::{HardwareBridge, HostContext, InputSignals}; +use prometeu_hal::{HardwareBridge, HostContext, InputSignals, RenderSubmissionPacket}; use prometeu_vm::LogicalFrameEndingReason; use std::collections::HashMap; use std::panic::{AssertUnwindSafe, catch_unwind}; @@ -221,7 +221,15 @@ impl VirtualMachineRuntime { || run.reason == LogicalFrameEndingReason::EndOfRom { self.render_manager.set_active_app_mode(self.current_cartridge_app_mode); - self.render_manager.close_compat_frame(); + if self.current_cartridge_app_mode == AppMode::Game { + self.render_manager + .close_frame_with_packet(RenderSubmissionPacket::Game2D( + hw.close_game2d_packet(), + )) + .expect("game packet must match Game app mode"); + } else { + self.render_manager.close_compat_frame(); + } if let Err(payload) = catch_unwind(AssertUnwindSafe(|| hw.render_frame())) { let message = Self::host_panic_payload_to_string(payload); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 660dc750..5be067d1 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":"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":"done","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":"done","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":"done","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":"done","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":"done","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-0077-composer-buffer-and-game2d-packet.md b/discussion/workflow/plans/PLN-0077-composer-buffer-and-game2d-packet.md index 2adb510b..6d9b1c23 100644 --- a/discussion/workflow/plans/PLN-0077-composer-buffer-and-game2d-packet.md +++ b/discussion/workflow/plans/PLN-0077-composer-buffer-and-game2d-packet.md @@ -2,7 +2,7 @@ id: PLN-0077 ticket: render-frame-packet-boundary title: Composer Buffer and Game2D Packet -status: open +status: done created: 2026-05-25 ref_decisions: [DEC-0030] tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]