use std::cell::RefCell;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use crate::event::Events;
use crate::world::World;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FrameEvent {
pub kind: u32,
pub entity: u32,
pub position: [f32; 3],
pub intensity: f32,
pub data: [f32; 4],
}
pub const FRAME_EVENT_STRIDE: usize = 10;
pub trait RenderEvent: 'static + Send + Sync {
const KIND: u32;
fn entity(&self) -> u32;
fn position(&self) -> [f32; 3];
fn intensity(&self) -> f32 {
1.0
}
fn data(&self) -> [f32; 4] {
[0.0; 4]
}
}
type EventExtractFn = Box<dyn Fn(&World) -> Vec<FrameEvent> + Send + Sync>;
pub struct RenderEventRegistry {
extractors: Vec<EventExtractFn>,
pending: RefCell<Vec<FrameEvent>>,
}
impl RenderEventRegistry {
pub fn new() -> Self {
Self {
extractors: Vec::new(),
pending: RefCell::new(Vec::new()),
}
}
pub fn register<T: RenderEvent>(&mut self) {
let offset = AtomicUsize::new(0);
let last_epoch = AtomicU64::new(0);
self.extractors.push(Box::new(move |world: &World| {
let Some(events) = world.try_resource::<Events<T>>() else {
return Vec::new();
};
let epoch = world.event_swap_epoch();
if last_epoch.load(Ordering::Relaxed) != epoch {
offset.store(0, Ordering::Relaxed);
last_epoch.store(epoch, Ordering::Relaxed);
}
let skip = offset.load(Ordering::Relaxed);
let result: Vec<FrameEvent> = events
.read_current()
.skip(skip)
.map(|e| FrameEvent {
kind: T::KIND,
entity: e.entity(),
position: e.position(),
intensity: e.intensity(),
data: e.data(),
})
.collect();
offset.store(events.current_len(), Ordering::Relaxed);
result
}));
}
pub fn accumulate(&self, world: &World) {
let mut pending = self.pending.borrow_mut();
for extractor in &self.extractors {
pending.extend(extractor(world));
}
}
pub fn drain(&self) -> Vec<FrameEvent> {
std::mem::take(&mut *self.pending.borrow_mut())
}
pub fn len(&self) -> usize {
self.extractors.len()
}
pub fn is_empty(&self) -> bool {
self.extractors.is_empty()
}
}
impl Default for RenderEventRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world::World;
#[derive(Debug, PartialEq)]
struct ImpactEvent {
entity_index: u32,
pos: [f32; 3],
force: f32,
}
impl RenderEvent for ImpactEvent {
const KIND: u32 = 1;
fn entity(&self) -> u32 {
self.entity_index
}
fn position(&self) -> [f32; 3] {
self.pos
}
fn intensity(&self) -> f32 {
self.force
}
}
#[derive(Debug, PartialEq)]
struct ExplosionEvent {
pos: [f32; 3],
radius: f32,
}
impl RenderEvent for ExplosionEvent {
const KIND: u32 = 2;
fn entity(&self) -> u32 {
0
}
fn position(&self) -> [f32; 3] {
self.pos
}
fn intensity(&self) -> f32 {
self.radius
}
}
#[test]
fn empty_registry() {
let registry = RenderEventRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn register_increments_len() {
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
assert_eq!(registry.len(), 1);
registry.register::<ExplosionEvent>();
assert_eq!(registry.len(), 2);
}
fn flush_and_drain(registry: &RenderEventRegistry, world: &World) -> Vec<FrameEvent> {
registry.accumulate(world);
registry.drain()
}
#[test]
fn drain_empty_when_no_events() {
let mut world = World::new();
world.add_event::<ImpactEvent>();
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
let events = flush_and_drain(®istry, &world);
assert!(events.is_empty());
}
#[test]
fn drain_empty_when_events_resource_missing() {
let world = World::new();
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
let events = flush_and_drain(®istry, &world);
assert!(events.is_empty());
}
#[test]
fn accumulate_captures_current_buffer() {
let mut world = World::new();
world.add_event::<ImpactEvent>();
world
.resource_mut::<Events<ImpactEvent>>()
.send(ImpactEvent {
entity_index: 42,
pos: [1.0, 2.0, 3.0],
force: 0.75,
});
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
let events = flush_and_drain(®istry, &world);
assert_eq!(events.len(), 1);
assert_eq!(events[0].kind, 1);
assert_eq!(events[0].entity, 42);
assert_eq!(events[0].position, [1.0, 2.0, 3.0]);
assert!((events[0].intensity - 0.75).abs() < f32::EPSILON);
assert_eq!(events[0].data, [0.0; 4]);
}
#[test]
fn accumulate_multiple_events_same_type() {
let mut world = World::new();
world.add_event::<ImpactEvent>();
let events_res = world.resource_mut::<Events<ImpactEvent>>();
events_res.send(ImpactEvent {
entity_index: 1,
pos: [0.0; 3],
force: 1.0,
});
events_res.send(ImpactEvent {
entity_index: 2,
pos: [5.0; 3],
force: 0.5,
});
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
let events = flush_and_drain(®istry, &world);
assert_eq!(events.len(), 2);
assert_eq!(events[0].entity, 1);
assert_eq!(events[1].entity, 2);
}
#[test]
fn accumulate_multiple_event_types() {
let mut world = World::new();
world.add_event::<ImpactEvent>();
world.add_event::<ExplosionEvent>();
world
.resource_mut::<Events<ImpactEvent>>()
.send(ImpactEvent {
entity_index: 10,
pos: [1.0, 0.0, 0.0],
force: 0.9,
});
world
.resource_mut::<Events<ExplosionEvent>>()
.send(ExplosionEvent {
pos: [5.0, 5.0, 5.0],
radius: 3.0,
});
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
registry.register::<ExplosionEvent>();
let events = flush_and_drain(®istry, &world);
assert_eq!(events.len(), 2);
let impact = events.iter().find(|e| e.kind == 1).unwrap();
assert_eq!(impact.entity, 10);
let explosion = events.iter().find(|e| e.kind == 2).unwrap();
assert_eq!(explosion.entity, 0);
assert!((explosion.intensity - 3.0).abs() < f32::EPSILON);
}
#[test]
fn multi_tick_accumulation_no_event_loss() {
let mut world = World::new();
world.add_event::<ImpactEvent>();
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
world
.resource_mut::<Events<ImpactEvent>>()
.send(ImpactEvent {
entity_index: 1,
pos: [0.0; 3],
force: 1.0,
});
registry.accumulate(&world);
world.update_events();
world
.resource_mut::<Events<ImpactEvent>>()
.send(ImpactEvent {
entity_index: 2,
pos: [5.0; 3],
force: 0.5,
});
registry.accumulate(&world);
world.update_events();
let events = registry.drain();
assert_eq!(events.len(), 2);
assert_eq!(events[0].entity, 1);
assert_eq!(events[1].entity, 2);
}
#[test]
fn drain_clears_pending() {
let mut world = World::new();
world.add_event::<ImpactEvent>();
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
world
.resource_mut::<Events<ImpactEvent>>()
.send(ImpactEvent {
entity_index: 1,
pos: [0.0; 3],
force: 1.0,
});
registry.accumulate(&world);
assert_eq!(registry.drain().len(), 1);
assert!(registry.drain().is_empty());
}
#[test]
fn no_double_delivery_across_frames() {
let mut world = World::new();
world.add_event::<ImpactEvent>();
let mut registry = RenderEventRegistry::new();
registry.register::<ImpactEvent>();
world
.resource_mut::<Events<ImpactEvent>>()
.send(ImpactEvent {
entity_index: 1,
pos: [0.0; 3],
force: 1.0,
});
registry.accumulate(&world);
let frame1 = registry.drain();
assert_eq!(frame1.len(), 1);
world.update_events();
registry.accumulate(&world);
let frame2 = registry.drain();
assert!(frame2.is_empty()); }
#[test]
fn default_intensity_is_one() {
#[derive(Debug)]
struct MinimalEvent;
impl RenderEvent for MinimalEvent {
const KIND: u32 = 99;
fn entity(&self) -> u32 {
0
}
fn position(&self) -> [f32; 3] {
[0.0; 3]
}
}
let mut world = World::new();
world.add_event::<MinimalEvent>();
world
.resource_mut::<Events<MinimalEvent>>()
.send(MinimalEvent);
let mut registry = RenderEventRegistry::new();
registry.register::<MinimalEvent>();
let events = flush_and_drain(®istry, &world);
assert_eq!(events.len(), 1);
assert!((events[0].intensity - 1.0).abs() < f32::EPSILON);
}
#[test]
fn custom_data_payload() {
#[derive(Debug)]
struct HitFlash {
color: [f32; 4],
}
impl RenderEvent for HitFlash {
const KIND: u32 = 50;
fn entity(&self) -> u32 {
0
}
fn position(&self) -> [f32; 3] {
[0.0; 3]
}
fn data(&self) -> [f32; 4] {
self.color
}
}
let mut world = World::new();
world.add_event::<HitFlash>();
world.resource_mut::<Events<HitFlash>>().send(HitFlash {
color: [1.0, 0.0, 0.0, 0.8],
});
let mut registry = RenderEventRegistry::new();
registry.register::<HitFlash>();
let events = flush_and_drain(®istry, &world);
assert_eq!(events.len(), 1);
assert_eq!(events[0].kind, 50);
assert_eq!(events[0].data, [1.0, 0.0, 0.0, 0.8]);
}
#[test]
fn drain_with_empty_registry_returns_empty() {
let registry = RenderEventRegistry::new();
assert!(registry.drain().is_empty());
}
}