implements PLN-0018
This commit is contained in:
parent
ed05f337ce
commit
5a0476e8b0
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[0],
|
||||
&self.sprites,
|
||||
&*self.glyph_banks,
|
||||
);
|
||||
self.populate_layer_buckets();
|
||||
for bucket in &self.layer_buckets {
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
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::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[0],
|
||||
&self.sprites,
|
||||
&*self.glyph_banks,
|
||||
);
|
||||
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.layer_buckets[layer_index],
|
||||
&self.sprites,
|
||||
&*self.glyph_banks,
|
||||
);
|
||||
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.
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -166,6 +166,7 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
glyph: Glyph { glyph_id, palette_id },
|
||||
x,
|
||||
y,
|
||||
layer: 0,
|
||||
bank_id,
|
||||
active,
|
||||
flip_x,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user