use std::any::{Any, TypeId};
use std::collections::HashMap;
use meridian_audio_core::{Emitter, Listener, Mixer};
use meridian_ecs_core::World;
use meridian_physics_core::{BroadPhase, ConstraintSolver, Integrator, NarrowPhase, RigidBody};
use meridian_platform_core::{Clock, CpuCapabilities, Time};
use meridian_task_core::{JobGraph, Scheduler};
#[derive(Default)]
pub struct EventSystem {
queues: HashMap<TypeId, Vec<Box<dyn Any>>>,
}
impl std::fmt::Debug for EventSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EventSystem")
.field("queued_event_types", &self.queues.len())
.finish()
}
}
impl EventSystem {
pub fn new() -> Self {
Self::default()
}
pub fn publish<E: 'static>(&mut self, event: E) {
self.queues
.entry(TypeId::of::<E>())
.or_default()
.push(Box::new(event));
}
pub fn drain<E: 'static>(&mut self) -> Vec<E> {
let Some(boxed) = self.queues.remove(&TypeId::of::<E>()) else {
return Vec::new();
};
boxed
.into_iter()
.map(|b| *b.downcast::<E>().expect("queue keyed by TypeId::of::<E>()"))
.collect()
}
}
#[derive(Debug)]
pub struct FrameScheduler {
scheduler: Scheduler,
}
impl Default for FrameScheduler {
fn default() -> Self {
Self::new(CpuCapabilities::detect().threads)
}
}
impl FrameScheduler {
pub fn new(worker_count: usize) -> Self {
Self {
scheduler: Scheduler::new(worker_count),
}
}
pub fn run(&self, graph: JobGraph) {
self.scheduler.run(graph);
}
}
pub struct SubsystemManager {
pub world: World,
pub bodies: Vec<RigidBody>,
pub broad_phase: BroadPhase,
pub narrow_phase: NarrowPhase,
pub solver: ConstraintSolver,
pub integrator: Integrator,
pub listener: Listener,
pub emitters: Vec<(Emitter, f32)>,
pub mixer: Mixer,
}
impl std::fmt::Debug for SubsystemManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubsystemManager")
.field("bodies", &self.bodies.len())
.field("emitters", &self.emitters.len())
.field("listener", &self.listener)
.finish_non_exhaustive()
}
}
impl SubsystemManager {
pub fn new(mixer: Mixer) -> Self {
Self {
world: World::new(),
bodies: Vec::new(),
broad_phase: BroadPhase::new(),
narrow_phase: NarrowPhase::new(),
solver: ConstraintSolver::default(),
integrator: Integrator::default(),
listener: Listener::default(),
emitters: Vec::new(),
mixer,
}
}
pub fn step_physics(&mut self, dt: f32) {
self.integrator.step(&mut self.bodies, dt);
let pairs = self.broad_phase.find_candidate_pairs(&self.bodies).to_vec();
for contact in self.narrow_phase.generate_contacts(&self.bodies, &pairs) {
self.solver.resolve(&mut self.bodies, &contact);
}
}
pub fn mix_audio(&self) -> Vec<(meridian_audio_core::Channel, f32)> {
self.mixer.mix(&self.listener, &self.emitters)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FrameCompleted {
pub frame_index: u64,
pub delta_seconds: f64,
}
#[derive(Debug)]
pub struct Runtime {
pub subsystems: SubsystemManager,
pub events: EventSystem,
pub frame_scheduler: FrameScheduler,
clock: Clock,
frame_index: u64,
}
impl Runtime {
pub fn new(subsystems: SubsystemManager) -> Self {
Self {
subsystems,
events: EventSystem::new(),
frame_scheduler: FrameScheduler::default(),
clock: Clock::new(),
frame_index: 0,
}
}
pub fn tick(&mut self) -> Time {
let time = self.clock.tick();
self.subsystems.step_physics(time.delta_seconds as f32);
self.events.publish(FrameCompleted {
frame_index: self.frame_index,
delta_seconds: time.delta_seconds,
});
self.frame_index += 1;
time
}
}
#[cfg(test)]
mod tests {
use super::*;
use meridian_audio_core::{AttenuationModel, Channel, SpeakerLayout};
use meridian_gac_core::{Motor3, Vec3};
use meridian_physics_core::ColliderShape;
#[test]
fn event_system_round_trips_by_type() {
let mut events = EventSystem::new();
events.publish(1i32);
events.publish(2i32);
events.publish("hello");
assert_eq!(events.drain::<i32>(), vec![1, 2]);
assert_eq!(events.drain::<&str>(), vec!["hello"]);
}
#[test]
fn event_system_drain_empties_the_queue() {
let mut events = EventSystem::new();
events.publish(42i32);
assert_eq!(events.drain::<i32>(), vec![42]);
assert_eq!(events.drain::<i32>(), Vec::<i32>::new());
}
#[test]
fn event_system_drain_of_unpublished_type_is_empty() {
let mut events = EventSystem::new();
assert_eq!(events.drain::<f32>(), Vec::<f32>::new());
}
#[test]
fn frame_scheduler_runs_a_job_graph() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let ran = Arc::new(AtomicUsize::new(0));
let mut graph = JobGraph::new();
let ran2 = ran.clone();
graph.add_job("job", &[], move || {
ran2.fetch_add(1, Ordering::SeqCst);
});
FrameScheduler::new(2).run(graph);
assert_eq!(ran.load(Ordering::SeqCst), 1);
}
fn falling_body() -> RigidBody {
RigidBody {
frame: Motor3::translation(Vec3::new(0.0, 10.0, 0.0)),
mass: 1.0,
shape: ColliderShape::Sphere { radius: 0.5 },
..Default::default()
}
}
#[test]
fn runtime_tick_advances_physics_under_gravity() {
let mut subsystems = SubsystemManager::new(Mixer::new(SpeakerLayout::mono()));
subsystems.bodies.push(falling_body());
let mut runtime = Runtime::new(subsystems);
for _ in 0..5 {
runtime.tick();
}
assert!(
runtime.subsystems.bodies[0].velocity.y < 0.0,
"gravity must have been applied across ticks"
);
}
#[test]
fn runtime_tick_publishes_frame_completed_with_increasing_index() {
let subsystems = SubsystemManager::new(Mixer::new(SpeakerLayout::mono()));
let mut runtime = Runtime::new(subsystems);
runtime.tick();
runtime.tick();
runtime.tick();
let completed = runtime.events.drain::<FrameCompleted>();
assert_eq!(completed.len(), 3);
assert_eq!(
completed.iter().map(|e| e.frame_index).collect::<Vec<_>>(),
vec![0, 1, 2]
);
}
#[test]
fn subsystem_manager_mixes_audio_from_current_emitter_positions() {
let mut subsystems = SubsystemManager::new(
Mixer::new(SpeakerLayout::stereo_headphones()).with_attenuation(AttenuationModel {
reference_distance: 1000.0,
rolloff: 1.0,
max_distance: 1000.0,
}),
);
subsystems.listener = Listener {
frame: Motor3::identity(),
};
subsystems.emitters.push((
Emitter {
frame: Motor3::translation(Vec3::new(0.0, 0.0, 5.0)),
},
1.0,
));
let gains = subsystems.mix_audio();
let gain_of = |channel: Channel| {
gains
.iter()
.find(|(c, _)| *c == channel)
.map(|(_, g)| *g)
.unwrap_or(0.0)
};
assert!(gain_of(Channel::Right) > 0.99);
assert!(gain_of(Channel::Left) < 1e-3);
}
#[test]
fn subsystem_manager_step_physics_resolves_a_resting_contact() {
let mut subsystems = SubsystemManager::new(Mixer::new(SpeakerLayout::mono()));
subsystems.bodies.push(RigidBody {
frame: Motor3::translation(Vec3::new(0.0, -50.0, 0.0)),
mass: 0.0, shape: ColliderShape::Sphere { radius: 50.0 },
..Default::default()
});
subsystems.bodies.push(falling_body());
for _ in 0..600 {
subsystems.step_physics(1.0 / 60.0);
}
let resting_height = subsystems.bodies[1].position().y;
assert!(
(resting_height - 0.5).abs() < 0.5,
"ball should settle near the floor surface, got y={resting_height}"
);
}
}