implements PLN-0018

This commit is contained in:
bQUARKz 2026-04-17 13:24:25 +01:00
parent ed05f337ce
commit 5a0476e8b0
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 323 additions and 42 deletions

View File

@ -10,6 +10,7 @@ const EMPTY_SPRITE: Sprite = Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
@ -26,6 +27,10 @@ pub enum SceneStatus {
#[derive(Clone, Debug)]
pub struct SpriteController {
sprites: [Sprite; 512],
sprite_count: usize,
frame_counter: u64,
dropped_sprites: usize,
layer_buckets: [Vec<usize>; 4],
}
impl Default for SpriteController {
@ -36,7 +41,13 @@ impl Default for SpriteController {
impl SpriteController {
pub fn new() -> Self {
Self { sprites: [EMPTY_SPRITE; 512] }
Self {
sprites: [EMPTY_SPRITE; 512],
sprite_count: 0,
frame_counter: 0,
dropped_sprites: 0,
layer_buckets: std::array::from_fn(|_| Vec::with_capacity(128)),
}
}
pub fn sprites(&self) -> &[Sprite; 512] {
@ -46,6 +57,57 @@ impl SpriteController {
pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] {
&mut self.sprites
}
pub fn begin_frame(&mut self) {
self.frame_counter = self.frame_counter.wrapping_add(1);
self.sprite_count = 0;
self.dropped_sprites = 0;
for bucket in &mut self.layer_buckets {
bucket.clear();
}
}
pub fn emit_sprite(&mut self, mut sprite: Sprite) -> bool {
let Some(bucket) = self.layer_buckets.get_mut(sprite.layer as usize) else {
self.dropped_sprites += 1;
return false;
};
if self.sprite_count >= self.sprites.len() {
self.dropped_sprites += 1;
return false;
}
sprite.active = true;
let index = self.sprite_count;
self.sprites[index] = sprite;
self.sprite_count += 1;
bucket.push(index);
true
}
pub fn sprite_count(&self) -> usize {
self.sprite_count
}
pub fn frame_counter(&self) -> u64 {
self.frame_counter
}
pub fn dropped_sprites(&self) -> usize {
self.dropped_sprites
}
pub fn ordered_sprites(&self) -> Vec<Sprite> {
let mut ordered = Vec::with_capacity(self.sprite_count);
for bucket in &self.layer_buckets {
let mut indices = bucket.clone();
indices.sort_by_key(|&index| self.sprites[index].priority);
for index in indices {
ordered.push(self.sprites[index]);
}
}
ordered
}
}
pub struct FrameComposer {
@ -130,13 +192,24 @@ impl FrameComposer {
pub fn sprite_controller_mut(&mut self) -> &mut SpriteController {
&mut self.sprite_controller
}
pub fn begin_frame(&mut self) {
self.sprite_controller.begin_frame();
}
pub fn emit_sprite(&mut self, sprite: Sprite) -> bool {
self.sprite_controller.emit_sprite(sprite)
}
pub fn ordered_sprites(&self) -> Vec<Sprite> {
self.sprite_controller.ordered_sprites()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_banks::{MemoryBanks, SceneBankPoolInstaller};
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::TileSize;
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::tile::Tile;
@ -178,7 +251,8 @@ mod tests {
assert!(frame_composer.cache().is_none());
assert!(frame_composer.resolver().is_none());
assert_eq!(frame_composer.sprite_controller().sprites().len(), 512);
assert!(frame_composer.sprite_controller().sprites().iter().all(|sprite| !sprite.active));
assert_eq!(frame_composer.sprite_controller().sprite_count(), 0);
assert_eq!(frame_composer.sprite_controller().dropped_sprites(), 0);
}
#[test]
@ -194,4 +268,156 @@ mod tests {
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
assert_eq!(scene.layers[0].parallax_factor.y, 0.5);
}
#[test]
fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() {
let mut controller = SpriteController::new();
let emitted = controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 1, palette_id: 2 },
x: 4,
y: 5,
layer: 2,
bank_id: 3,
active: false,
flip_x: false,
flip_y: false,
priority: 1,
});
assert!(emitted);
controller.begin_frame();
assert_eq!(controller.frame_counter(), 1);
assert_eq!(controller.sprite_count(), 0);
assert_eq!(controller.dropped_sprites(), 0);
assert!(controller.ordered_sprites().is_empty());
}
#[test]
fn sprite_controller_orders_by_layer_then_priority_then_fifo() {
let mut controller = SpriteController::new();
controller.begin_frame();
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 10, palette_id: 0 },
x: 0,
y: 0,
layer: 1,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 2,
}));
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 11, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 3,
}));
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 12, palette_id: 0 },
x: 0,
y: 0,
layer: 1,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 1,
}));
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 13, palette_id: 0 },
x: 0,
y: 0,
layer: 1,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 2,
}));
let ordered = controller.ordered_sprites();
let ordered_ids: Vec<u16> = ordered.iter().map(|sprite| sprite.glyph.glyph_id).collect();
assert_eq!(ordered_ids, vec![11, 12, 10, 13]);
assert!(ordered.iter().all(|sprite| sprite.active));
}
#[test]
fn sprite_controller_drops_overflow_without_panicking() {
let mut controller = SpriteController::new();
controller.begin_frame();
for glyph_id in 0..512 {
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
}));
}
let overflowed = controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 999, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
});
assert!(!overflowed);
assert_eq!(controller.sprite_count(), 512);
assert_eq!(controller.dropped_sprites(), 1);
}
#[test]
fn frame_composer_emits_ordered_sprites_for_rendering() {
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
frame_composer.begin_frame();
assert!(frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 21, palette_id: 0 },
x: 0,
y: 0,
layer: 2,
bank_id: 1,
active: false,
flip_x: false,
flip_y: false,
priority: 1,
}));
assert!(frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 20, palette_id: 0 },
x: 0,
y: 0,
layer: 1,
bank_id: 1,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
}));
let ordered = frame_composer.ordered_sprites();
assert_eq!(ordered.len(), 2);
assert_eq!(ordered[0].glyph.glyph_id, 20);
assert_eq!(ordered[1].glyph.glyph_id, 21);
}
}

View File

@ -71,8 +71,10 @@ pub struct Gfx {
/// Target color for the HUD fade effect.
pub hud_fade_color: Color,
/// Internal cache used to sort sprites into priority groups to optimize rendering.
priority_buckets: [Vec<usize>; 5],
/// Internal sprite count for the current frame state.
sprite_count: usize,
/// Internal cache used to sort sprites by layer while keeping stable priority order.
layer_buckets: [Vec<usize>; 4],
}
const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7];
@ -213,6 +215,9 @@ impl GfxBridge for Gfx {
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) {
self.render_scene_from_cache(cache, update)
}
fn load_frame_sprites(&mut self, sprites: &[Sprite]) {
self.load_frame_sprites(sprites)
}
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
self.draw_text(x, y, text, color)
}
@ -224,6 +229,7 @@ impl GfxBridge for Gfx {
&self.sprites[index]
}
fn sprite_mut(&mut self, index: usize) -> &mut Sprite {
self.sprite_count = self.sprite_count.max(index.saturating_add(1)).min(self.sprites.len());
&mut self.sprites[index]
}
@ -262,6 +268,7 @@ impl Gfx {
glyph: EMPTY_GLYPH,
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
@ -277,12 +284,12 @@ impl Gfx {
back: vec![0; len],
glyph_banks,
sprites: [EMPTY_SPRITE; 512],
sprite_count: 0,
scene_fade_level: 31,
scene_fade_color: Color::BLACK,
hud_fade_level: 31,
hud_fade_color: Color::BLACK,
priority_buckets: [
Vec::with_capacity(128),
layer_buckets: [
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
@ -530,23 +537,33 @@ impl Gfx {
std::mem::swap(&mut self.front, &mut self.back);
}
pub fn load_frame_sprites(&mut self, sprites: &[Sprite]) {
self.sprite_count = sprites.len().min(self.sprites.len());
for (index, sprite) in sprites.iter().copied().take(self.sprites.len()).enumerate() {
self.sprites[index] = Sprite { active: true, ..sprite };
}
for sprite in self.sprites.iter_mut().skip(self.sprite_count) {
sprite.active = false;
}
}
/// The main rendering pipeline.
///
/// This method composes the final frame by rasterizing layers and sprites in the
/// correct priority order into the back buffer.
/// Follows the hardware model where layers and sprites are composed every frame.
pub fn render_all(&mut self) {
self.populate_priority_buckets();
// 1. Priority 0 sprites: drawn at the very back, behind everything else.
self.populate_layer_buckets();
for bucket in &self.layer_buckets {
Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[0],
bucket,
&self.sprites,
&*self.glyph_banks,
);
}
// 2. Scene-only fallback path: sprites and fades still work even before a
// cache-backed world composition request is issued for the frame.
@ -563,18 +580,17 @@ impl Gfx {
/// plus sprite state and fade controls.
pub fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) {
self.back.fill(Color::BLACK.raw());
self.populate_priority_buckets();
self.populate_layer_buckets();
for layer_index in 0..cache.layers.len() {
Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[0],
&self.layer_buckets[layer_index],
&self.sprites,
&*self.glyph_banks,
);
for layer_index in 0..cache.layers.len() {
Self::draw_cache_layer_to_buffer(
&mut self.back,
self.w,
@ -583,31 +599,26 @@ impl Gfx {
&update.copy_requests[layer_index],
&*self.glyph_banks,
);
Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[layer_index + 1],
&self.sprites,
&*self.glyph_banks,
);
}
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
}
fn populate_priority_buckets(&mut self) {
for bucket in self.priority_buckets.iter_mut() {
fn populate_layer_buckets(&mut self) {
for bucket in self.layer_buckets.iter_mut() {
bucket.clear();
}
for (idx, sprite) in self.sprites.iter().enumerate() {
if sprite.active && sprite.priority < 5 {
self.priority_buckets[sprite.priority as usize].push(idx);
for (idx, sprite) in self.sprites.iter().take(self.sprite_count).enumerate() {
if sprite.active && (sprite.layer as usize) < self.layer_buckets.len() {
self.layer_buckets[sprite.layer as usize].push(idx);
}
}
for bucket in self.layer_buckets.iter_mut() {
bucket.sort_by_key(|&idx| self.sprites[idx].priority);
}
}
fn draw_cache_layer_to_buffer(
@ -1004,6 +1015,7 @@ mod tests {
glyph: Glyph { glyph_id: 0, palette_id: 4 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: true,
flip_x: false,
@ -1014,17 +1026,57 @@ mod tests {
glyph: Glyph { glyph_id: 0, palette_id: 4 },
x: 0,
y: 0,
layer: 2,
bank_id: 0,
active: true,
flip_x: false,
flip_y: false,
priority: 2,
};
gfx.sprite_count = 2;
gfx.render_scene_from_cache(&cache, &update);
assert_eq!(gfx.back[0], Color::BLUE.raw());
}
#[test]
fn load_frame_sprites_replaces_slot_first_submission_for_render_state() {
let banks = Arc::new(MemoryBanks::new());
let mut gfx = Gfx::new(16, 16, banks as Arc<dyn GlyphBankPoolAccess>);
gfx.load_frame_sprites(&[
Sprite {
glyph: Glyph { glyph_id: 1, palette_id: 2 },
x: 2,
y: 3,
layer: 1,
bank_id: 4,
active: false,
flip_x: true,
flip_y: false,
priority: 7,
},
Sprite {
glyph: Glyph { glyph_id: 5, palette_id: 6 },
x: 7,
y: 8,
layer: 3,
bank_id: 9,
active: false,
flip_x: false,
flip_y: true,
priority: 1,
},
]);
assert_eq!(gfx.sprite_count, 2);
assert!(gfx.sprites[0].active);
assert!(gfx.sprites[1].active);
assert!(!gfx.sprites[2].active);
assert_eq!(gfx.sprites[0].layer, 1);
assert_eq!(gfx.sprites[1].glyph.glyph_id, 5);
}
}
/// Blends in RGB565 per channel with saturation.

View File

@ -50,6 +50,7 @@ pub trait GfxBridge {
fn present(&mut self);
fn render_all(&mut self);
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate);
fn load_frame_sprites(&mut self, sprites: &[Sprite]);
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color);
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color);

View File

@ -5,6 +5,7 @@ pub struct Sprite {
pub glyph: Glyph,
pub x: i32,
pub y: i32,
pub layer: u8,
pub bank_id: u8,
pub active: bool,
pub flip_x: bool,

View File

@ -166,6 +166,7 @@ impl NativeInterface for VirtualMachineRuntime {
glyph: Glyph { glyph_id, palette_id },
x,
y,
layer: 0,
bank_id,
active,
flip_x,