Centralize world and viewport game loops
This commit is contained in:
16
crates/metacrate-game-loop/Cargo.toml
Normal file
16
crates/metacrate-game-loop/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "metacrate-game-loop"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Stable OpenSim/Second Life update, rendering, and viewport orchestration"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
metacrate-grid-world = { version = "0.0.1", path = "../metacrate-grid-world" }
|
||||
metacrate-rendering-wgpu = { version = "0.0.1", path = "../metacrate-rendering-wgpu", features = ["wgpu"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
761
crates/metacrate-game-loop/src/lib.rs
Normal file
761
crates/metacrate-game-loop/src/lib.rs
Normal file
@@ -0,0 +1,761 @@
|
||||
//! Stable OpenSim/Second Life update, rendering, and viewport orchestration.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::{
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
mpsc::{self, Receiver, RecvTimeoutError, SyncSender, TryRecvError},
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub use metacrate_grid_world::{
|
||||
DirtyWorldItem, LibreMetaverseWorldEvents, RegionDescriptor, WorldEvent, WorldEventMetrics,
|
||||
WorldEventReceiver, WorldSnapshot, WorldState,
|
||||
};
|
||||
|
||||
/// Slight scheduling margin keeps measured delivery at or above 10 FPS.
|
||||
pub const DEFAULT_VIEWPORT_FRAME_INTERVAL: Duration = Duration::from_millis(90);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct LoopConfig {
|
||||
pub update_interval: Duration,
|
||||
pub render_interval: Duration,
|
||||
pub max_update_catch_up: u32,
|
||||
pub max_events_per_iteration: usize,
|
||||
pub event_queue_capacity: usize,
|
||||
}
|
||||
|
||||
impl Default for LoopConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
update_interval: Duration::from_nanos(16_666_667),
|
||||
render_interval: Duration::from_millis(100),
|
||||
max_update_catch_up: 4,
|
||||
max_events_per_iteration: 1_024,
|
||||
event_queue_capacity: 4_096,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LoopConfig {
|
||||
#[must_use]
|
||||
pub const fn valid(self) -> bool {
|
||||
!self.update_interval.is_zero()
|
||||
&& !self.render_interval.is_zero()
|
||||
&& self.max_update_catch_up != 0
|
||||
&& self.max_events_per_iteration != 0
|
||||
&& self.event_queue_capacity != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct LoopStep {
|
||||
pub updates: u32,
|
||||
pub render: bool,
|
||||
pub skipped_updates: u64,
|
||||
pub skipped_renders: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LoopSchedule {
|
||||
config: LoopConfig,
|
||||
next_update: Instant,
|
||||
next_render: Instant,
|
||||
}
|
||||
|
||||
impl LoopSchedule {
|
||||
#[must_use]
|
||||
pub fn new(config: LoopConfig, started: Instant) -> Option<Self> {
|
||||
config.valid().then_some(Self {
|
||||
config,
|
||||
next_update: started + config.update_interval,
|
||||
next_render: started + config.render_interval,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn next_deadline(self) -> Instant {
|
||||
self.next_update.min(self.next_render)
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, now: Instant) -> LoopStep {
|
||||
let update_due = intervals_due(now, self.next_update, self.config.update_interval);
|
||||
let updates = u32::try_from(update_due.min(u64::from(self.config.max_update_catch_up)))
|
||||
.unwrap_or(self.config.max_update_catch_up);
|
||||
let skipped_updates = update_due.saturating_sub(u64::from(updates));
|
||||
if update_due != 0 {
|
||||
self.next_update = next_after(now, self.next_update, self.config.update_interval);
|
||||
}
|
||||
let render_due = intervals_due(now, self.next_render, self.config.render_interval);
|
||||
if render_due != 0 {
|
||||
self.next_render = next_after(now, self.next_render, self.config.render_interval);
|
||||
}
|
||||
LoopStep {
|
||||
updates,
|
||||
render: render_due != 0,
|
||||
skipped_updates,
|
||||
skipped_renders: render_due.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn intervals_due(now: Instant, deadline: Instant, interval: Duration) -> u64 {
|
||||
if now < deadline {
|
||||
return 0;
|
||||
}
|
||||
let intervals = now.duration_since(deadline).as_nanos() / interval.as_nanos() + 1;
|
||||
intervals.try_into().unwrap_or(u64::MAX)
|
||||
}
|
||||
|
||||
fn next_after(now: Instant, deadline: Instant, interval: Duration) -> Instant {
|
||||
let due = intervals_due(now, deadline, interval);
|
||||
let Ok(due) = u32::try_from(due) else {
|
||||
return now + interval;
|
||||
};
|
||||
deadline
|
||||
.checked_add(interval.saturating_mul(due))
|
||||
.unwrap_or(now + interval)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LatestValue<T> {
|
||||
sequence: u64,
|
||||
value: Option<Arc<T>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Latest<T> {
|
||||
inner: Mutex<LatestValue<T>>,
|
||||
}
|
||||
|
||||
impl<T> Default for Latest<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: Mutex::new(LatestValue {
|
||||
sequence: 0,
|
||||
value: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Published<T> {
|
||||
pub sequence: u64,
|
||||
pub value: Arc<T>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ViewportFrame {
|
||||
pub sequence: u64,
|
||||
pub scene_sequence: u64,
|
||||
pub observed_unix_millis: u64,
|
||||
pub camera: metacrate_rendering_wgpu::Camera,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub rgba: Arc<[u8]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StableViewport {
|
||||
latest: Latest<ViewportFrame>,
|
||||
}
|
||||
|
||||
impl StableViewport {
|
||||
pub fn publish(&self, frame: ViewportFrame) -> Result<u64, ViewportFrame> {
|
||||
let expected = usize::try_from(frame.width)
|
||||
.ok()
|
||||
.and_then(|width| {
|
||||
usize::try_from(frame.height)
|
||||
.ok()
|
||||
.and_then(|height| width.checked_mul(height))
|
||||
})
|
||||
.and_then(|pixels| pixels.checked_mul(4));
|
||||
if frame.sequence == 0 || expected != Some(frame.rgba.len()) {
|
||||
return Err(frame);
|
||||
}
|
||||
Ok(self.latest.publish(frame))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn latest(&self) -> Option<Published<ViewportFrame>> {
|
||||
self.latest.load()
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns the renderer and publishes immutable completed frames. Readers never
|
||||
/// observe the render target while Bevy is writing the next frame.
|
||||
pub struct ViewportRuntime {
|
||||
renderer: metacrate_rendering_wgpu::Renderer,
|
||||
viewport: StableViewport,
|
||||
next_sequence: AtomicU64,
|
||||
loaded_scene: Mutex<Option<u64>>,
|
||||
}
|
||||
|
||||
pub struct ViewportRender {
|
||||
pub frame: Published<ViewportFrame>,
|
||||
pub timings: metacrate_rendering_wgpu::RenderTimings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ViewportScene {
|
||||
pub sequence: u64,
|
||||
pub camera: metacrate_rendering_wgpu::Camera,
|
||||
pub renderables: Arc<[metacrate_rendering_wgpu::Renderable]>,
|
||||
pub textures: Arc<[metacrate_rendering_wgpu::Texture]>,
|
||||
pub background_srgb: [u8; 4],
|
||||
pub limits: metacrate_rendering_wgpu::RenderLimits,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ViewportStats {
|
||||
pub completed_frames: u64,
|
||||
pub missed_frame_deadlines: u64,
|
||||
pub render_errors: u64,
|
||||
pub last_render: Duration,
|
||||
}
|
||||
|
||||
/// Fixed-rate viewport producer. Scene submission is nonblocking and always
|
||||
/// replaces stale pending input with the newest complete scene.
|
||||
pub struct ViewportLoop {
|
||||
runtime: Arc<ViewportRuntime>,
|
||||
input: Arc<Latest<ViewportScene>>,
|
||||
stopped: Arc<AtomicBool>,
|
||||
thread: Mutex<Option<thread::JoinHandle<()>>>,
|
||||
completed_frames: Arc<AtomicU64>,
|
||||
missed_frame_deadlines: Arc<AtomicU64>,
|
||||
render_errors: Arc<AtomicU64>,
|
||||
last_render_nanos: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl ViewportLoop {
|
||||
pub fn start(frame_interval: Duration) -> Result<Self, metacrate_rendering_wgpu::RenderError> {
|
||||
if frame_interval.is_zero() {
|
||||
return Err(metacrate_rendering_wgpu::RenderError::InvalidScene);
|
||||
}
|
||||
let runtime = Arc::new(ViewportRuntime::new_blocking()?);
|
||||
let input: Arc<Latest<ViewportScene>> = Arc::new(Latest::default());
|
||||
let stopped = Arc::new(AtomicBool::new(false));
|
||||
let completed_frames = Arc::new(AtomicU64::new(0));
|
||||
let missed_frame_deadlines = Arc::new(AtomicU64::new(0));
|
||||
let render_errors = Arc::new(AtomicU64::new(0));
|
||||
let last_render_nanos = Arc::new(AtomicU64::new(0));
|
||||
let worker_runtime = Arc::clone(&runtime);
|
||||
let worker_input = Arc::clone(&input);
|
||||
let worker_stopped = Arc::clone(&stopped);
|
||||
let worker_completed = Arc::clone(&completed_frames);
|
||||
let worker_missed = Arc::clone(&missed_frame_deadlines);
|
||||
let worker_errors = Arc::clone(&render_errors);
|
||||
let worker_last_render = Arc::clone(&last_render_nanos);
|
||||
let thread = thread::Builder::new()
|
||||
.name("metacrate-viewport-loop".into())
|
||||
.spawn(move || {
|
||||
let mut next_frame = Instant::now();
|
||||
while !worker_stopped.load(Ordering::Acquire) {
|
||||
let now = Instant::now();
|
||||
if now < next_frame {
|
||||
thread::park_timeout(next_frame - now);
|
||||
continue;
|
||||
}
|
||||
let due = intervals_due(now, next_frame, frame_interval);
|
||||
worker_missed.fetch_add(due.saturating_sub(1), Ordering::Relaxed);
|
||||
next_frame = next_after(now, next_frame, frame_interval);
|
||||
let Some(scene) = worker_input.load() else {
|
||||
continue;
|
||||
};
|
||||
let started = Instant::now();
|
||||
let result = worker_runtime.render_scene(
|
||||
scene.value.sequence,
|
||||
scene.value.camera,
|
||||
|| Arc::clone(&scene.value.renderables),
|
||||
Arc::clone(&scene.value.textures),
|
||||
scene.value.background_srgb,
|
||||
scene.value.limits,
|
||||
);
|
||||
worker_last_render.store(
|
||||
started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
if result.is_ok() {
|
||||
worker_completed.fetch_add(1, Ordering::Relaxed);
|
||||
} else {
|
||||
worker_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|error| metacrate_rendering_wgpu::RenderError::Device(error.to_string()))?;
|
||||
Ok(Self {
|
||||
runtime,
|
||||
input,
|
||||
stopped,
|
||||
thread: Mutex::new(Some(thread)),
|
||||
completed_frames,
|
||||
missed_frame_deadlines,
|
||||
render_errors,
|
||||
last_render_nanos,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn submit(&self, scene: ViewportScene) -> u64 {
|
||||
let sequence = self.input.publish(scene);
|
||||
if let Some(thread) = lock(&self.thread).as_ref() {
|
||||
thread.thread().unpark();
|
||||
}
|
||||
sequence
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn latest(&self) -> Option<Published<ViewportFrame>> {
|
||||
self.runtime.latest()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stats(&self) -> ViewportStats {
|
||||
ViewportStats {
|
||||
completed_frames: self.completed_frames.load(Ordering::Relaxed),
|
||||
missed_frame_deadlines: self.missed_frame_deadlines.load(Ordering::Relaxed),
|
||||
render_errors: self.render_errors.load(Ordering::Relaxed),
|
||||
last_render: Duration::from_nanos(self.last_render_nanos.load(Ordering::Relaxed)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
self.stopped.store(true, Ordering::Release);
|
||||
if let Some(thread) = lock(&self.thread).take() {
|
||||
thread.thread().unpark();
|
||||
let _ = thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ViewportLoop {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewportRuntime {
|
||||
pub fn new_blocking() -> Result<Self, metacrate_rendering_wgpu::RenderError> {
|
||||
Ok(Self {
|
||||
renderer: metacrate_rendering_wgpu::Renderer::new_blocking()?,
|
||||
viewport: StableViewport::default(),
|
||||
next_sequence: AtomicU64::new(0),
|
||||
loaded_scene: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_scene<F>(
|
||||
&self,
|
||||
scene_sequence: u64,
|
||||
camera: metacrate_rendering_wgpu::Camera,
|
||||
renderables: F,
|
||||
textures: Arc<[metacrate_rendering_wgpu::Texture]>,
|
||||
background_srgb: [u8; 4],
|
||||
limits: metacrate_rendering_wgpu::RenderLimits,
|
||||
) -> Result<ViewportRender, metacrate_rendering_wgpu::RenderError>
|
||||
where
|
||||
F: FnOnce() -> Arc<[metacrate_rendering_wgpu::Renderable]>,
|
||||
{
|
||||
let mut loaded_scene = lock(&self.loaded_scene);
|
||||
let mut timings = if *loaded_scene == Some(scene_sequence) {
|
||||
metacrate_rendering_wgpu::RenderTimings::default()
|
||||
} else {
|
||||
let renderables = renderables();
|
||||
let timings = self
|
||||
.renderer
|
||||
.load_scene_shared(camera, renderables, textures, limits)?;
|
||||
*loaded_scene = Some(scene_sequence);
|
||||
timings
|
||||
};
|
||||
let rendered = self.renderer.render_view(camera, background_srgb, limits)?;
|
||||
timings.clear_previous_frame += rendered.timings.clear_previous_frame;
|
||||
timings.scene_setup += rendered.timings.scene_setup;
|
||||
timings.capture += rendered.timings.capture;
|
||||
let sequence = self.next_sequence.fetch_add(1, Ordering::AcqRel) + 1;
|
||||
self.viewport
|
||||
.publish(ViewportFrame {
|
||||
sequence,
|
||||
scene_sequence,
|
||||
observed_unix_millis: unix_millis_now(),
|
||||
camera,
|
||||
width: limits.width,
|
||||
height: limits.height,
|
||||
rgba: rendered.rgba.into(),
|
||||
})
|
||||
.map_err(|_| metacrate_rendering_wgpu::RenderError::InvalidScene)?;
|
||||
Ok(ViewportRender {
|
||||
frame: self
|
||||
.viewport
|
||||
.latest()
|
||||
.ok_or(metacrate_rendering_wgpu::RenderError::Readback)?,
|
||||
timings,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn latest(&self) -> Option<Published<ViewportFrame>> {
|
||||
self.viewport.latest()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Latest<T> {
|
||||
pub fn publish(&self, value: T) -> u64 {
|
||||
self.publish_arc(Arc::new(value))
|
||||
}
|
||||
|
||||
pub fn publish_arc(&self, value: Arc<T>) -> u64 {
|
||||
let mut latest = lock(&self.inner);
|
||||
latest.sequence = latest.sequence.saturating_add(1);
|
||||
latest.value = Some(value);
|
||||
latest.sequence
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn load(&self) -> Option<Published<T>> {
|
||||
let latest = lock(&self.inner);
|
||||
Some(Published {
|
||||
sequence: latest.sequence,
|
||||
value: Arc::clone(latest.value.as_ref()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum TimingPhase {
|
||||
Events,
|
||||
Update,
|
||||
Render,
|
||||
Publish,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TimingSignal {
|
||||
pub phase: TimingPhase,
|
||||
pub duration: Duration,
|
||||
pub sequence: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TimingEmitter {
|
||||
sender: SyncSender<TimingSignal>,
|
||||
dropped: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TimingReceiver {
|
||||
receiver: Receiver<TimingSignal>,
|
||||
dropped: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn timing_channel(capacity: usize) -> Option<(TimingEmitter, TimingReceiver)> {
|
||||
if capacity == 0 {
|
||||
return None;
|
||||
}
|
||||
let (sender, receiver) = mpsc::sync_channel(capacity);
|
||||
let dropped = Arc::new(AtomicU64::new(0));
|
||||
Some((
|
||||
TimingEmitter {
|
||||
sender,
|
||||
dropped: Arc::clone(&dropped),
|
||||
},
|
||||
TimingReceiver { receiver, dropped },
|
||||
))
|
||||
}
|
||||
|
||||
impl TimingEmitter {
|
||||
pub fn emit(&self, signal: TimingSignal) {
|
||||
if self.sender.try_send(signal).is_err() {
|
||||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TimingReceiver {
|
||||
pub fn recv_timeout(&self, timeout: Duration) -> Result<TimingSignal, RecvTimeoutError> {
|
||||
self.receiver.recv_timeout(timeout)
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Result<TimingSignal, TryRecvError> {
|
||||
self.receiver.try_recv()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dropped(&self) -> u64 {
|
||||
self.dropped.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LoopDriver: Send + 'static {
|
||||
fn world_changed(&mut self, world: &WorldState, dirty: &[DirtyWorldItem]);
|
||||
fn update(&mut self, world: &WorldState, step: Duration);
|
||||
fn render(&mut self, world: &WorldState);
|
||||
}
|
||||
|
||||
impl LoopDriver for () {
|
||||
fn world_changed(&mut self, _world: &WorldState, _dirty: &[DirtyWorldItem]) {}
|
||||
fn update(&mut self, _world: &WorldState, _step: Duration) {}
|
||||
fn render(&mut self, _world: &WorldState) {}
|
||||
}
|
||||
|
||||
pub struct LoopHandle {
|
||||
stopped: Arc<AtomicBool>,
|
||||
thread: Option<thread::JoinHandle<()>>,
|
||||
world: Arc<Latest<WorldSnapshot>>,
|
||||
world_events: WorldEventMetrics,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WorldReader {
|
||||
world: Arc<Latest<WorldSnapshot>>,
|
||||
world_events: WorldEventMetrics,
|
||||
}
|
||||
|
||||
impl WorldReader {
|
||||
#[must_use]
|
||||
pub fn latest(&self) -> Option<Published<WorldSnapshot>> {
|
||||
self.world.load()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dropped_events(&self) -> u64 {
|
||||
self.world_events.dropped()
|
||||
}
|
||||
}
|
||||
|
||||
impl LoopHandle {
|
||||
#[must_use]
|
||||
pub fn latest_world(&self) -> Option<Published<WorldSnapshot>> {
|
||||
self.world.load()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn world_reader(&self) -> WorldReader {
|
||||
WorldReader {
|
||||
world: Arc::clone(&self.world),
|
||||
world_events: self.world_events.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dropped_world_events(&self) -> u64 {
|
||||
self.world_events.dropped()
|
||||
}
|
||||
|
||||
pub fn shutdown(mut self) -> thread::Result<()> {
|
||||
self.stop();
|
||||
self.thread.take().map_or(Ok(()), thread::JoinHandle::join)
|
||||
}
|
||||
|
||||
fn stop(&self) {
|
||||
self.stopped.store(true, Ordering::Release);
|
||||
if let Some(thread) = &self.thread {
|
||||
thread.thread().unpark();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LoopHandle {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn start<D: LoopDriver>(
|
||||
config: LoopConfig,
|
||||
events: WorldEventReceiver,
|
||||
mut driver: D,
|
||||
timings: Option<TimingEmitter>,
|
||||
) -> Option<LoopHandle> {
|
||||
if !config.valid() {
|
||||
return None;
|
||||
}
|
||||
let stopped = Arc::new(AtomicBool::new(false));
|
||||
let world_events = events.metrics();
|
||||
let world = Arc::new(Latest::default());
|
||||
let published_world = Arc::clone(&world);
|
||||
let thread_stopped = Arc::clone(&stopped);
|
||||
let thread = thread::Builder::new()
|
||||
.name("metacrate-game-loop".into())
|
||||
.spawn(move || {
|
||||
let Some(mut schedule) = LoopSchedule::new(config, Instant::now()) else {
|
||||
return;
|
||||
};
|
||||
let mut sequence = 0_u64;
|
||||
let mut world = WorldState::default();
|
||||
while !thread_stopped.load(Ordering::Acquire) {
|
||||
let started = Instant::now();
|
||||
let mut event_count = 0;
|
||||
while event_count < config.max_events_per_iteration {
|
||||
match events.try_recv() {
|
||||
Ok(event) => {
|
||||
world.apply(event);
|
||||
event_count += 1;
|
||||
}
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => return,
|
||||
}
|
||||
}
|
||||
if event_count != 0 {
|
||||
let dirty = world.take_dirty();
|
||||
driver.world_changed(&world, &dirty);
|
||||
published_world.publish(world.snapshot());
|
||||
emit(
|
||||
timings.as_ref(),
|
||||
TimingPhase::Events,
|
||||
started.elapsed(),
|
||||
sequence,
|
||||
);
|
||||
}
|
||||
let step = schedule.advance(Instant::now());
|
||||
for _ in 0..step.updates {
|
||||
let started = Instant::now();
|
||||
driver.update(&world, config.update_interval);
|
||||
sequence = sequence.saturating_add(1);
|
||||
emit(
|
||||
timings.as_ref(),
|
||||
TimingPhase::Update,
|
||||
started.elapsed(),
|
||||
sequence,
|
||||
);
|
||||
}
|
||||
if step.render {
|
||||
let started = Instant::now();
|
||||
driver.render(&world);
|
||||
emit(
|
||||
timings.as_ref(),
|
||||
TimingPhase::Render,
|
||||
started.elapsed(),
|
||||
sequence,
|
||||
);
|
||||
}
|
||||
let wait = schedule
|
||||
.next_deadline()
|
||||
.saturating_duration_since(Instant::now());
|
||||
if wait.is_zero() {
|
||||
thread::yield_now();
|
||||
} else {
|
||||
thread::park_timeout(wait);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some(LoopHandle {
|
||||
stopped,
|
||||
thread: Some(thread),
|
||||
world,
|
||||
world_events,
|
||||
})
|
||||
}
|
||||
|
||||
fn emit(emitter: Option<&TimingEmitter>, phase: TimingPhase, duration: Duration, sequence: u64) {
|
||||
if let Some(emitter) = emitter {
|
||||
emitter.emit(TimingSignal {
|
||||
phase,
|
||||
duration,
|
||||
sequence,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lock<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
value
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn unix_millis_now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |duration| {
|
||||
duration.as_millis().try_into().unwrap_or(u64::MAX)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct Driver(Arc<AtomicU64>);
|
||||
|
||||
impl LoopDriver for Driver {
|
||||
fn world_changed(&mut self, _world: &WorldState, dirty: &[DirtyWorldItem]) {
|
||||
self.0.fetch_add(
|
||||
u64::try_from(dirty.len()).unwrap_or(u64::MAX),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
fn update(&mut self, _world: &WorldState, _step: Duration) {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
fn render(&mut self, _world: &WorldState) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schedule_bounds_catch_up_latest_publishes_and_runner_emits_timings() {
|
||||
let config = LoopConfig {
|
||||
update_interval: Duration::from_millis(10),
|
||||
render_interval: Duration::from_millis(25),
|
||||
max_update_catch_up: 2,
|
||||
..LoopConfig::default()
|
||||
};
|
||||
let started = Instant::now();
|
||||
let mut schedule = LoopSchedule::new(config, started).unwrap();
|
||||
let step = schedule.advance(started + Duration::from_millis(56));
|
||||
assert_eq!(step.updates, 2);
|
||||
assert_eq!(step.skipped_updates, 3);
|
||||
assert!(step.render);
|
||||
assert_eq!(step.skipped_renders, 1);
|
||||
|
||||
let latest = Latest::default();
|
||||
assert_eq!(latest.publish("old"), 1);
|
||||
assert_eq!(latest.publish("new"), 2);
|
||||
let published = latest.load().unwrap();
|
||||
assert_eq!(published.sequence, 2);
|
||||
assert_eq!(*published.value, "new");
|
||||
|
||||
let viewport = StableViewport::default();
|
||||
let camera = metacrate_rendering_wgpu::Camera::look_at(
|
||||
[0.0; 3],
|
||||
[1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0],
|
||||
60.0,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
viewport
|
||||
.publish(ViewportFrame {
|
||||
sequence: 1,
|
||||
scene_sequence: 1,
|
||||
observed_unix_millis: 1,
|
||||
camera,
|
||||
width: 2,
|
||||
height: 1,
|
||||
rgba: vec![0; 8].into(),
|
||||
})
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(viewport.latest().unwrap().value.sequence, 1);
|
||||
|
||||
let count = Arc::new(AtomicU64::new(0));
|
||||
let (timings, receiver) = timing_channel(32).unwrap();
|
||||
let (events, event_receiver) = metacrate_grid_world::world_event_channel(32).unwrap();
|
||||
events.send(WorldEvent::RegionChanged(None));
|
||||
let handle = start(
|
||||
config,
|
||||
event_receiver,
|
||||
Driver(Arc::clone(&count)),
|
||||
Some(timings),
|
||||
)
|
||||
.unwrap();
|
||||
thread::sleep(Duration::from_millis(40));
|
||||
handle.shutdown().unwrap();
|
||||
assert!(count.load(Ordering::Relaxed) >= 2);
|
||||
assert!(receiver.try_recv().is_ok());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user