dev/render-all-scene-cache-and-camera-integration #16

Merged
bquarkz merged 21 commits from dev/render-all-scene-cache-and-camera-integration into master 2026-04-18 16:20:50 +00:00
4 changed files with 153 additions and 11 deletions
Showing only changes of commit aaed1e95dd - Show all commits

View File

@ -52,6 +52,8 @@ pub struct Gfx {
pub glyph_banks: Arc<dyn GlyphBankPoolAccess>,
/// 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]

View File

@ -32,4 +32,8 @@ impl DeferredGfxOverlay {
pub fn command_count(&self) -> usize {
self.commands.len()
}
pub fn take_commands(&mut self) -> Vec<OverlayCommand> {
std::mem::take(&mut self.commands)
}
}

View File

@ -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 {

View File

@ -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);