diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index 56b6814d..a39e0658 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -52,6 +52,8 @@ pub struct Gfx { pub glyph_banks: Arc, /// Deferred overlay/debug capture kept separate from canonical game composition. overlay: DeferredGfxOverlay, + /// Internal guard to replay deferred overlay commands without re-enqueueing them. + is_draining_overlay: bool, /// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory). pub sprites: [Sprite; 512], @@ -292,6 +294,7 @@ impl Gfx { back: vec![0; len], glyph_banks, overlay: DeferredGfxOverlay::default(), + is_draining_overlay: false, sprites: [EMPTY_SPRITE; 512], sprite_count: 0, scene_fade_level: 31, @@ -319,6 +322,34 @@ impl Gfx { &self.overlay } + pub fn drain_overlay_debug(&mut self) { + let commands = self.overlay.take_commands(); + self.is_draining_overlay = true; + + for command in commands { + match command { + OverlayCommand::FillRectBlend { x, y, w, h, color, mode } => { + self.fill_rect_blend(x, y, w, h, color, mode) + } + OverlayCommand::DrawLine { x0, y0, x1, y1, color } => { + self.draw_line(x0, y0, x1, y1, color) + } + OverlayCommand::DrawCircle { x, y, r, color } => self.draw_circle(x, y, r, color), + OverlayCommand::DrawDisc { x, y, r, border_color, fill_color } => { + self.draw_disc(x, y, r, border_color, fill_color) + } + OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color } => { + self.draw_square(x, y, w, h, border_color, fill_color) + } + OverlayCommand::DrawText { x, y, text, color } => { + self.draw_text(x, y, &text, color) + } + } + } + + self.is_draining_overlay = false; + } + /// The buffer that the host should display (RGB565). pub fn front_buffer(&self) -> &[u16] { &self.front @@ -338,7 +369,10 @@ impl Gfx { color: Color, mode: BlendMode, ) { - self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode }); + return; + } if color == Color::COLOR_KEY { return; } @@ -380,7 +414,10 @@ impl Gfx { /// 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) { - self.overlay.push(OverlayCommand::DrawLine { x0, y0, x1, y1, color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawLine { x0, y0, x1, y1, color }); + return; + } if color == Color::COLOR_KEY { return; } @@ -413,7 +450,10 @@ impl Gfx { /// Draws a circle outline using Midpoint Circle Algorithm. pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { - self.overlay.push(OverlayCommand::DrawCircle { x: xc, y: yc, r, color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawCircle { x: xc, y: yc, r, color }); + return; + } if color == Color::COLOR_KEY { return; } @@ -482,7 +522,10 @@ impl Gfx { /// Draws a disc (filled circle with border). pub fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) { - self.overlay.push(OverlayCommand::DrawDisc { x, y, r, border_color, fill_color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawDisc { x, y, r, border_color, fill_color }); + return; + } self.fill_circle(x, y, r, fill_color); self.draw_circle(x, y, r, border_color); } @@ -512,7 +555,10 @@ impl Gfx { border_color: Color, fill_color: Color, ) { - self.overlay.push(OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color }); + return; + } self.fill_rect(x, y, w, h, fill_color); self.draw_rect(x, y, w, h, border_color); } @@ -800,7 +846,10 @@ impl Gfx { } pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { - self.overlay.push(OverlayCommand::DrawText { x, y, text: text.to_string(), color }); + if !self.is_draining_overlay { + self.overlay.push(OverlayCommand::DrawText { x, y, text: text.to_string(), color }); + return; + } let mut cx = x; for c in text.chars() { self.draw_char(cx, y, c, color); @@ -947,12 +996,10 @@ mod tests { let mut gfx = Gfx::new(10, 10, banks); gfx.begin_overlay_frame(); gfx.draw_line(0, 0, 9, 9, Color::WHITE); + gfx.drain_overlay_debug(); assert_eq!(gfx.back[0], Color::WHITE.0); assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0); - assert_eq!( - gfx.overlay().commands(), - &[OverlayCommand::DrawLine { x0: 0, y0: 0, x1: 9, y1: 9, color: Color::WHITE }] - ); + assert_eq!(gfx.overlay().command_count(), 0); } #[test] @@ -980,11 +1027,12 @@ mod tests { let mut gfx = Gfx::new(10, 10, banks); gfx.begin_overlay_frame(); gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK); + gfx.drain_overlay_debug(); // Border assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0); // Fill assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0); - assert_eq!(gfx.overlay().command_count(), 2); + assert_eq!(gfx.overlay().command_count(), 0); } #[test] diff --git a/crates/console/prometeu-drivers/src/gfx_overlay.rs b/crates/console/prometeu-drivers/src/gfx_overlay.rs index 6b9641ad..be77e9c4 100644 --- a/crates/console/prometeu-drivers/src/gfx_overlay.rs +++ b/crates/console/prometeu-drivers/src/gfx_overlay.rs @@ -32,4 +32,8 @@ impl DeferredGfxOverlay { pub fn command_count(&self) -> usize { self.commands.len() } + + pub fn take_commands(&mut self) -> Vec { + std::mem::take(&mut self.commands) + } } diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index 42d02f34..a7f09aec 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -70,6 +70,7 @@ impl HardwareBridge for Hardware { fn render_frame(&mut self) { self.frame_composer.render_frame(&mut self.gfx); + self.gfx.drain_overlay_debug(); } fn has_glyph_bank(&self, bank_id: usize) -> bool { diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index 68780ef9..6ca08d2b 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -354,6 +354,95 @@ fn tick_renders_scene_through_public_composer_syscalls() { assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw()); } +#[test] +fn tick_draw_text_survives_scene_backed_frame_composition() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let signals = InputSignals::default(); + let code = assemble( + "PUSH_I32 0\nHOSTCALL 0\nPOP_N 1\n\ + PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 1\n\ + PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 65535\nHOSTCALL 2\n\ + FRAME_SYNC\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("I".into())], + vec![ + SyscallDecl { + module: "composer".into(), + name: "bind_scene".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "set_camera".into(), + version: 1, + arg_slots: 2, + ret_slots: 0, + }, + SyscallDecl { + module: "gfx".into(), + name: "draw_text".into(), + version: 1, + arg_slots: 4, + ret_slots: 0, + }, + ], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE))); + banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8))); + let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks)); + hardware.gfx.scene_fade_level = 31; + hardware.gfx.hud_fade_level = 31; + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + + assert!(report.is_none(), "scene-backed overlay text must not crash"); + hardware.gfx.present(); + assert_eq!(hardware.gfx.front_buffer()[0], Color::WHITE.raw()); +} + +#[test] +fn tick_draw_text_survives_no_scene_frame_path() { + let mut runtime = VirtualMachineRuntime::new(None); + 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", + ) + .expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("I".into())], + vec![SyscallDecl { + module: "gfx".into(), + name: "draw_text".into(), + version: 1, + arg_slots: 4, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + let mut hardware = Hardware::new(); + hardware.gfx.scene_fade_level = 31; + hardware.gfx.hud_fade_level = 31; + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + + assert!(report.is_none(), "no-scene overlay text must not crash"); + hardware.gfx.present(); + assert_eq!(hardware.gfx.front_buffer()[0], Color::WHITE.raw()); +} + #[test] fn initialize_vm_success_clears_previous_crash_report() { let mut runtime = VirtualMachineRuntime::new(None);