fixes all around gfx and prometeu render
Some checks failed
Intrepid/Prometeu/Runtime/pipeline/head There was a failure building this commit

This commit is contained in:
bQUARKz 2026-05-16 13:02:47 +01:00
parent 74ef393294
commit eb398846a0
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
9 changed files with 83 additions and 281 deletions

View File

@ -1,4 +1,3 @@
use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
use crate::memory_banks::GlyphBankPoolAccess;
use prometeu_hal::GfxBridge;
use prometeu_hal::color::Color;
@ -34,9 +33,7 @@ pub enum BlendMode {
///
/// `Gfx` owns the framebuffer backend and the canonical game-frame raster path
/// consumed by `FrameComposer`. That canonical path covers scene composition,
/// sprite composition, and fades. Public `gfx.*` primitives remain valid, but
/// they do not define the canonical game composition contract; they belong to a
/// separate final overlay/debug stage.
/// sprite composition, and fades. Public `gfx.*` primitives remain valid.
pub struct Gfx {
/// Width of the internal framebuffer in pixels.
w: usize,
@ -45,15 +42,10 @@ pub struct Gfx {
/// Front buffer: the "VRAM" currently being displayed by the Host window.
front: Vec<u16>,
/// Back buffer: the working buffer where canonical game frames are composed
/// before any final overlay/debug drain.
back: Vec<u16>,
/// Shared access to graphical memory banks (tiles and palettes).
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],
@ -298,8 +290,6 @@ impl Gfx {
front: vec![0; len],
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,42 +309,6 @@ impl Gfx {
(self.w, self.h)
}
pub fn begin_overlay_frame(&mut self) {
self.overlay.begin_frame();
}
pub fn overlay(&self) -> &DeferredGfxOverlay {
&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
@ -374,10 +328,6 @@ impl Gfx {
color: Color,
mode: BlendMode,
) {
if !self.is_draining_overlay {
self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode });
return;
}
if color == Color::COLOR_KEY {
return;
}
@ -419,10 +369,6 @@ 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) {
if !self.is_draining_overlay {
self.overlay.push(OverlayCommand::DrawLine { x0, y0, x1, y1, color });
return;
}
if color == Color::COLOR_KEY {
return;
}
@ -455,10 +401,6 @@ 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 !self.is_draining_overlay {
self.overlay.push(OverlayCommand::DrawCircle { x: xc, y: yc, r, color });
return;
}
if color == Color::COLOR_KEY {
return;
}
@ -527,10 +469,6 @@ 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) {
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);
}
@ -560,10 +498,6 @@ impl Gfx {
border_color: Color,
fill_color: 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);
}
@ -863,10 +797,6 @@ impl Gfx {
}
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: 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);
@ -961,9 +891,7 @@ fn blend_rgb565(dst: u16, src: u16, mode: BlendMode) -> u16 {
#[cfg(test)]
mod tests {
use super::*;
use crate::FrameComposer;
use crate::asset::GlyphAssetSlotIndex;
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess};
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks};
use prometeu_hal::asset::AssetId;
use prometeu_hal::glyph_bank::TileSize;
use prometeu_hal::scene_bank::SceneBank;
@ -1047,16 +975,6 @@ mod tests {
}
}
fn make_glyph_slot_index(bindings: &[(AssetId, usize)]) -> GlyphAssetSlotIndex {
let index = GlyphAssetSlotIndex::new();
let mut slots = [None; 16];
for (asset_id, slot) in bindings {
slots[*slot] = Some(*asset_id);
}
index.rebuild_from_slots(&slots);
index
}
#[test]
fn test_draw_pixel() {
let banks = Arc::new(MemoryBanks::new());
@ -1073,12 +991,9 @@ mod tests {
fn test_draw_line() {
let banks = Arc::new(MemoryBanks::new());
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().command_count(), 0);
}
#[test]
@ -1104,56 +1019,9 @@ mod tests {
fn test_draw_square() {
let banks = Arc::new(MemoryBanks::new());
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();
assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0);
assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0);
assert_eq!(gfx.overlay().command_count(), 0);
}
#[test]
fn draw_text_captures_overlay_command() {
let banks = Arc::new(MemoryBanks::new());
let mut gfx = Gfx::new(32, 18, banks);
gfx.begin_overlay_frame();
gfx.draw_text(4, 5, "HUD", Color::WHITE);
assert_eq!(
gfx.overlay().commands(),
&[OverlayCommand::DrawText { x: 4, y: 5, text: "HUD".into(), color: Color::WHITE }]
);
}
#[test]
fn overlay_state_is_separate_from_frame_composer_sprite_state() {
let banks = Arc::new(MemoryBanks::new());
let mut gfx = Gfx::new(32, 18, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
let mut frame_composer = FrameComposer::new(
32,
18,
Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>,
make_glyph_slot_index(&[]),
);
gfx.begin_overlay_frame();
frame_composer.begin_frame();
frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 0 },
x: 1,
y: 2,
layer: 0,
bank_id: 0,
active: true,
flip_x: false,
flip_y: false,
priority: 0,
});
gfx.draw_text(1, 1, "X", Color::WHITE);
assert_eq!(frame_composer.sprite_controller().sprite_count(), 1);
assert_eq!(gfx.overlay().command_count(), 1);
}
#[test]

View File

@ -1,39 +0,0 @@
use crate::gfx::BlendMode;
use prometeu_hal::color::Color;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OverlayCommand {
FillRectBlend { x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode },
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
DrawCircle { x: i32, y: i32, r: i32, color: Color },
DrawDisc { x: i32, y: i32, r: i32, border_color: Color, fill_color: Color },
DrawSquare { x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color },
DrawText { x: i32, y: i32, text: String, color: Color },
}
#[derive(Debug, Clone, Default)]
pub struct DeferredGfxOverlay {
commands: Vec<OverlayCommand>,
}
impl DeferredGfxOverlay {
pub fn begin_frame(&mut self) {
self.commands.clear();
}
pub fn push(&mut self, command: OverlayCommand) {
self.commands.push(command);
}
pub fn commands(&self) -> &[OverlayCommand] {
&self.commands
}
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

@ -48,7 +48,6 @@ impl Default for Hardware {
impl HardwareBridge for Hardware {
fn begin_frame(&mut self) {
self.gfx.begin_overlay_frame();
self.frame_composer.begin_frame();
}
@ -70,7 +69,6 @@ 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

@ -2,7 +2,6 @@ mod asset;
mod audio;
mod frame_composer;
mod gfx;
mod gfx_overlay;
pub mod hardware;
mod memory_banks;
mod pad;
@ -12,7 +11,6 @@ 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::gfx::Gfx;
pub use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
pub use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,

View File

@ -11,6 +11,7 @@ pub struct HubHomeStep;
impl HubHomeStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.log(LogLevel::Info, LogSource::Hub, 0, "Entering HubHome".to_string());
ctx.os.windows().remove_all_windows();
ctx.hub.init();
}

View File

@ -67,12 +67,7 @@ impl ShellRunningStep {
}
if outcome.action == Some(SystemProfileAction::CloseShell) {
if let Err(error) = ctx.os.lifecycle().close_task(self.task_id) {
ctx.os.error(
LogSource::Hub,
format!("Failed to close shell task {:?}: {:?}", self.task_id, error),
);
}
self.close_current_shell(ctx);
return Some(FirmwareState::HubHome(HubHomeStep));
}
@ -84,18 +79,22 @@ impl ShellRunningStep {
self.task_id,
),
);
if let Err(error) = ctx.os.lifecycle().close_task(self.task_id) {
ctx.os.error(
LogSource::Hub,
format!("Failed to close shell task {:?}: {:?}", self.task_id, error),
);
}
self.close_current_shell(ctx);
return Some(FirmwareState::HubHome(HubHomeStep));
}
None
}
fn close_current_shell(&mut self, ctx: &mut PrometeuContext) {
if let Err(error) = ctx.os.lifecycle().close_task(self.task_id) {
ctx.os.error(
LogSource::Hub,
format!("Failed to close shell task {:?}: {:?}", self.task_id, error),
);
}
}
pub fn on_exit(&mut self, ctx: &mut PrometeuContext) {
ctx.os.info(LogSource::Hub, "Exiting ShellRunning".to_string());
}

View File

@ -101,7 +101,7 @@ impl PrometeuHub {
os: &mut SystemOS,
hw: &mut dyn HardwareBridge,
) -> Option<SystemProfileAction> {
hw.gfx_mut().clear(Color::BLACK);
hw.gfx_mut().clear(Color::INDIGO);
let in_shell = os.windows().focused_window().is_some();

View File

@ -636,82 +636,6 @@ 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 log_service = LogService::new(4096);
let mut fs = VirtualFS::new();
let mut fs_state = FsState::Unmounted;
let mut memcard = MemcardService::new();
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
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));
let glyph_slot_index = hardware.assets.glyph_asset_slot_index();
let mut slots = [None; 16];
slots[0] = Some(0);
glyph_slot_index.rebuild_from_slots(&slots);
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
&mut log_service,
&mut fs,
&mut fs_state,
&mut memcard,
&mut open_files,
&mut next_handle,
&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);

View File

@ -4,6 +4,50 @@ use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::Window;
#[derive(Debug, Clone, Copy)]
struct FbViewport {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub scale: f32,
}
impl FbViewport {
fn framebuffer_viewport(window: &Window) -> Option<FbViewport> {
let size = window.inner_size();
let win_w = size.width as f32;
let win_h = size.height as f32;
if win_w <= 0.0 || win_h <= 0.0 {
return None;
}
let fb_w = Hardware::W as f32;
let fb_h = Hardware::H as f32;
let scale_x = (win_w / fb_w).floor();
let scale_y = (win_h / fb_h).floor();
let scale = scale_x.min(scale_y).max(1.0);
let viewport_w = fb_w * scale;
let viewport_h = fb_h * scale;
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,
})
}
}
pub struct HostInputHandler {
pub signals: InputSignals,
}
@ -49,9 +93,10 @@ impl HostInputHandler {
}
WindowEvent::CursorMoved { position, .. } => {
let v = window_to_fb(position.x as f32, position.y as f32, window);
self.signals.x_pos = v.0;
self.signals.y_pos = v.1;
if let Some((x, y)) = window_to_fb(position.x as f32, position.y as f32, window) {
self.signals.x_pos = x;
self.signals.y_pos = y;
}
}
WindowEvent::MouseInput { state, button, .. } if *button == MouseButton::Left => {
@ -67,16 +112,24 @@ impl HostInputHandler {
_ => {}
}
fn window_to_fb(wx: f32, wy: f32, window: &Window) -> Option<(i32, i32)> {
let viewport = FbViewport::framebuffer_viewport(window)?;
let local_x = wx - viewport.x;
let local_y = wy - viewport.y;
if local_x < 0.0 || local_y < 0.0 || local_x >= viewport.w || local_y >= viewport.h {
return None;
}
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),
))
}
}
}
pub fn window_to_fb(wx: f32, wy: f32, window: &Window) -> (i32, i32) {
let size = window.inner_size();
let fb_w = Hardware::W as f32;
let fb_h = Hardware::H as f32;
let x = (wx * fb_w / size.width as f32).floor() as i32;
let y = (wy * fb_h / size.height as f32).floor() as i32;
(x.clamp(0, Hardware::W as i32 - 1), y.clamp(0, Hardware::H as i32 - 1))
}