dried stuff

Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
2025-12-13 22:50:13 +00:00
parent 5cb258fe6b
commit b0f62dae38
4 changed files with 203 additions and 91 deletions

1
Cargo.lock generated
View File

@@ -4511,6 +4511,7 @@ dependencies = [
"chrono", "chrono",
"crdts", "crdts",
"criterion", "criterion",
"crossbeam-channel",
"futures-lite", "futures-lite",
"glam 0.29.3", "glam 0.29.3",
"iroh", "iroh",

View File

@@ -30,7 +30,7 @@ pub use crate::input::event_buffer::InputEventBuffer;
/// Application handler state machine /// Application handler state machine
enum AppHandler { enum AppHandler {
Initializing { app: App }, Initializing { app: Option<App> },
Running { Running {
window: Arc<WinitWindow>, window: Arc<WinitWindow>,
bevy_window_entity: Entity, bevy_window_entity: Entity,
@@ -38,17 +38,62 @@ enum AppHandler {
}, },
} }
impl AppHandler { // Helper functions to reduce duplication
fn initialize(&mut self, event_loop: &ActiveEventLoop) { fn send_window_created(app: &mut App, window: Entity) {
// Only initialize if we're in the Initializing state app.world_mut()
if !matches!(self, AppHandler::Initializing { .. }) { .resource_mut::<Messages<WindowCreated>>()
return; .write(WindowCreated { window });
} }
// Take ownership of the app (replace with placeholder temporarily) fn send_window_resized(
let temp_state = std::mem::replace(self, AppHandler::Initializing { app: App::new() }); app: &mut App,
let AppHandler::Initializing { app } = temp_state else { unreachable!() }; window: Entity,
let mut bevy_app = app; physical_size: winit::dpi::PhysicalSize<u32>,
scale_factor: f64,
) {
app.world_mut()
.resource_mut::<Messages<WindowResized>>()
.write(WindowResized {
window,
width: physical_size.width as f32 / scale_factor as f32,
height: physical_size.height as f32 / scale_factor as f32,
});
}
fn send_scale_factor_changed(app: &mut App, window: Entity, scale_factor: f64) {
app.world_mut()
.resource_mut::<Messages<WindowScaleFactorChanged>>()
.write(WindowScaleFactorChanged {
window,
scale_factor,
});
}
fn send_window_closing(app: &mut App, window: Entity) {
app.world_mut()
.resource_mut::<Messages<WindowClosing>>()
.write(WindowClosing { window });
}
impl AppHandler {
/// Initialize the window and transition to Running state.
///
/// This is called on the first `resumed()` event from winit.
/// Subsequent `resumed()` calls are ignored to prevent double initialization.
///
/// # Errors
/// Returns an error if window creation or RawHandleWrapper creation fails.
fn initialize(&mut self, event_loop: &ActiveEventLoop) -> Result<(), String> {
// Only initialize if we're in the Initializing state with an app
let AppHandler::Initializing { app: app_opt } = self else {
warn!("initialize() called on non-Initializing state, ignoring");
return Ok(());
};
let Some(mut bevy_app) = app_opt.take() else {
warn!("initialize() called twice, ignoring");
return Ok(());
};
// Insert InputEventBuffer resource // Insert InputEventBuffer resource
bevy_app.insert_resource(InputEventBuffer::default()); bevy_app.insert_resource(InputEventBuffer::default());
@@ -72,31 +117,32 @@ impl AppHandler {
.with_inner_size(winit::dpi::LogicalSize::new(1280, 720)); .with_inner_size(winit::dpi::LogicalSize::new(1280, 720));
let winit_window = event_loop.create_window(window_attributes) let winit_window = event_loop.create_window(window_attributes)
.expect("Failed to create window"); .map_err(|e| format!("Failed to create window: {}", e))?;
let winit_window = Arc::new(winit_window); let winit_window = Arc::new(winit_window);
info!("Created window before app.finish()"); info!("Created window before app.finish()");
let physical_size = winit_window.inner_size(); let physical_size = winit_window.inner_size();
let scale_factor = winit_window.scale_factor(); let scale_factor = winit_window.scale_factor();
// Create window entity with all required components // Create window entity with all required components (use logical size)
let mut window = bevy::window::Window { let mut window = bevy::window::Window {
title: "Marathon".to_string(), title: "Marathon".to_string(),
resolution: WindowResolution::new( resolution: WindowResolution::new(
physical_size.width, physical_size.width / scale_factor as u32,
physical_size.height, physical_size.height / scale_factor as u32,
), ),
mode: WindowMode::Windowed, mode: WindowMode::Windowed,
position: WindowPosition::Automatic, position: WindowPosition::Automatic,
focused: true, focused: true,
..Default::default() ..Default::default()
}; };
window.resolution.set_scale_factor_override(Some(scale_factor as f32)); // Set scale factor explicitly
window.resolution.set_scale_factor(scale_factor as f32);
// Create WindowWrapper and RawHandleWrapper for renderer // Create WindowWrapper and RawHandleWrapper for renderer
let window_wrapper = WindowWrapper::new(winit_window.clone()); let window_wrapper = WindowWrapper::new(winit_window.clone());
let raw_handle_wrapper = RawHandleWrapper::new(&window_wrapper) let raw_handle_wrapper = RawHandleWrapper::new(&window_wrapper)
.expect("Failed to create RawHandleWrapper"); .map_err(|e| format!("Failed to create RawHandleWrapper: {}", e))?;
let window_entity = bevy_app.world_mut().spawn(( let window_entity = bevy_app.world_mut().spawn((
window, window,
@@ -105,19 +151,10 @@ impl AppHandler {
)).id(); )).id();
info!("Created window entity {}", window_entity); info!("Created window entity {}", window_entity);
// Send WindowCreated event // Send initialization events
bevy_app.world_mut() send_window_created(&mut bevy_app, window_entity);
.resource_mut::<Messages<WindowCreated>>() send_window_resized(&mut bevy_app, window_entity, physical_size, scale_factor);
.write(WindowCreated { window: window_entity }); send_scale_factor_changed(&mut bevy_app, window_entity, scale_factor);
// Send WindowResized event
bevy_app.world_mut()
.resource_mut::<Messages<WindowResized>>()
.write(WindowResized {
window: window_entity,
width: physical_size.width as f32 / scale_factor as f32,
height: physical_size.height as f32 / scale_factor as f32,
});
// Now finish the app - the renderer will initialize with the window // Now finish the app - the renderer will initialize with the window
bevy_app.finish(); bevy_app.finish();
@@ -130,16 +167,54 @@ impl AppHandler {
bevy_window_entity: window_entity, bevy_window_entity: window_entity,
bevy_app, bevy_app,
}; };
Ok(())
}
/// Clean shutdown of the app and window
fn shutdown(&mut self, event_loop: &ActiveEventLoop) {
if let AppHandler::Running {
bevy_window_entity,
ref mut bevy_app,
..
} = self
{
info!("Shutting down gracefully");
// Send WindowClosing event
send_window_closing(bevy_app, *bevy_window_entity);
// Run one final update to process close event
bevy_app.update();
// Cleanup
bevy_app.finish();
bevy_app.cleanup();
}
event_loop.exit();
} }
} }
impl ApplicationHandler for AppHandler { impl ApplicationHandler for AppHandler {
fn resumed(&mut self, event_loop: &ActiveEventLoop) { fn resumed(&mut self, event_loop: &ActiveEventLoop) {
// Initialize on first resumed() call // Initialize on first resumed() call
self.initialize(event_loop); if let Err(e) = self.initialize(event_loop) {
error!("Failed to initialize: {}", e);
event_loop.exit();
return;
}
info!("App resumed"); info!("App resumed");
} }
// TODO(@siennathesane): Implement suspended() callback for mobile platforms.
// On iOS/Android, the app can be backgrounded (suspended). We should:
// - Stop requesting redraws to save battery
// - Potentially release GPU resources
// - Log the suspended state for debugging
// Note: RedrawRequested events won't fire while suspended anyway,
// so the unbounded loop naturally pauses.
fn window_event( fn window_event(
&mut self, &mut self,
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
@@ -161,41 +236,35 @@ impl ApplicationHandler for AppHandler {
match event { match event {
WinitWindowEvent::CloseRequested => { WinitWindowEvent::CloseRequested => {
info!("Window close requested"); self.shutdown(event_loop);
event_loop.exit();
} }
WinitWindowEvent::Resized(physical_size) => { WinitWindowEvent::Resized(physical_size) => {
// Notify Bevy of window resize // Notify Bevy of window resize
let scale_factor = window.scale_factor(); let scale_factor = window.scale_factor();
bevy_app.world_mut() send_window_resized(bevy_app, *bevy_window_entity, physical_size, scale_factor);
.resource_mut::<Messages<WindowResized>>()
.write(WindowResized {
window: *bevy_window_entity,
width: physical_size.width as f32 / scale_factor as f32,
height: physical_size.height as f32 / scale_factor as f32,
});
} }
WinitWindowEvent::RedrawRequested => { WinitWindowEvent::RedrawRequested => {
// Collect input events from platform bridge // Collect input events from platform bridge
let input_events = desktop::drain_as_input_events(); let input_events = desktop::drain_as_input_events();
// Write events to InputEventBuffer resource // Reuse buffer capacity instead of replacing (optimization)
bevy_app.world_mut().resource_mut::<InputEventBuffer>().events = input_events; {
let mut buffer = bevy_app.world_mut().resource_mut::<InputEventBuffer>();
buffer.events.clear();
buffer.events.extend(input_events);
}
// Run one Bevy ECS update (unbounded) // Run one Bevy ECS update (unbounded)
bevy_app.update(); bevy_app.update();
// Check if app should exit // Check if app should exit
if let Some(exit) = bevy_app.should_exit() { if bevy_app.should_exit().is_some() {
info!("App exit requested: {:?}", exit); self.shutdown(event_loop);
event_loop.exit(); return;
} }
// Clear input buffer for next frame
bevy_app.world_mut().resource_mut::<InputEventBuffer>().clear();
// Request next frame immediately (unbounded loop) // Request next frame immediately (unbounded loop)
window.request_redraw(); window.request_redraw();
} }
@@ -205,29 +274,70 @@ impl ApplicationHandler for AppHandler {
} }
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
// Request redraw to keep loop running // Ensure we keep rendering even if no window events arrive.
// This is needed for unbounded mode with ControlFlow::Poll to maintain
// maximum frame rate by continuously requesting redraws.
if let AppHandler::Running { ref window, .. } = self { if let AppHandler::Running { ref window, .. } = self {
window.request_redraw(); window.request_redraw();
} }
} }
} }
/// Run the application executor /// Run the application executor with a custom event loop.
///
/// This executor owns the winit event loop directly (instead of using Bevy's WinitPlugin)
/// and drives both the window and Bevy's ECS in unbounded mode for maximum performance.
///
/// # Unbounded Execution
///
/// The executor runs as fast as possible using `ControlFlow::Poll`, which means:
/// - The window event loop runs continuously without waiting for events
/// - Bevy's ECS `app.update()` is called on every frame
/// - Both rendering and game logic run unbounded (no frame rate cap)
///
/// This provides maximum performance but will drain battery quickly.
/// See the TODO comment about adaptive frame rate limiting for production use.
///
/// # Window Lifecycle
///
/// 1. Event loop starts and calls `resumed()` (winit requirement)
/// 2. Window and window entity are created BEFORE `app.finish()`
/// 3. Bevy's renderer initializes during `app.finish()` with the window available
/// 4. App transitions to Running state and begins the render loop
///
/// # Errors
///
/// Returns an error if:
/// - The event loop cannot be created
/// - Window creation fails during initialization
/// - The event loop encounters a fatal error
///
/// # Example
///
/// ```no_run
/// use bevy::prelude::*;
///
/// let app = App::new();
/// // ... configure app with plugins and systems ...
///
/// executor::run(app).expect("Failed to run executor");
/// ```
pub fn run(app: App) -> Result<(), Box<dyn std::error::Error>> { pub fn run(app: App) -> Result<(), Box<dyn std::error::Error>> {
let event_loop = EventLoop::new()?; let event_loop = EventLoop::new()?;
// TODO: Add battery power detection and adaptive frame/tick rate limiting // TODO(@siennathesane): Add battery power detection and adaptive frame/tick rate limiting
// When on battery: reduce to 60fps cap, lower ECS tick rate // When on battery: reduce to 60fps cap, lower ECS tick rate
// When plugged in: run unbounded for maximum performance // When plugged in: run unbounded for maximum performance
// See GitHub issue #TBD for implementation details
// Run as fast as possible (unbounded) // Run as fast as possible (unbounded)
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
info!("Starting executor (unbounded mode)"); info!("Starting executor (unbounded mode)");
// Create handler in Initializing state // Create handler in Initializing state with the app
// It will transition to Running state on first resumed() callback // It will transition to Running state on first resumed() callback
let mut handler = AppHandler::Initializing { app }; let mut handler = AppHandler::Initializing { app: Some(app) };
event_loop.run_app(&mut handler)?; event_loop.run_app(&mut handler)?;

View File

@@ -27,6 +27,7 @@ blake3 = "1.5"
rand = "0.8" rand = "0.8"
tokio.workspace = true tokio.workspace = true
blocking = "1.6" blocking = "1.6"
crossbeam-channel = "0.5"
iroh = { workspace = true, features = ["discovery-local-network"] } iroh = { workspace = true, features = ["discovery-local-network"] }
iroh-gossip.workspace = true iroh-gossip.workspace = true

View File

@@ -4,8 +4,9 @@
//! to engine-agnostic InputEvents. //! to engine-agnostic InputEvents.
use crate::engine::{InputEvent, KeyCode, Modifiers, MouseButton, TouchPhase}; use crate::engine::{InputEvent, KeyCode, Modifiers, MouseButton, TouchPhase};
use crossbeam_channel::{Receiver, Sender, unbounded};
use glam::Vec2; use glam::Vec2;
use std::sync::Mutex; use std::sync::{Mutex, OnceLock};
use winit::event::{ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent}; use winit::event::{ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent};
use winit::keyboard::PhysicalKey; use winit::keyboard::PhysicalKey;
@@ -31,11 +32,15 @@ pub enum RawWinitEvent {
}, },
} }
/// Thread-safe buffer for winit events /// Lock-free channel for winit events
/// ///
/// The winit event loop pushes events here. /// The winit event loop sends events here.
/// The engine drains them each frame. /// The engine receives them each frame.
static BUFFER: Mutex<Vec<RawWinitEvent>> = Mutex::new(Vec::new()); static EVENT_CHANNEL: OnceLock<(Sender<RawWinitEvent>, Receiver<RawWinitEvent>)> = OnceLock::new();
fn get_event_channel() -> &'static (Sender<RawWinitEvent>, Receiver<RawWinitEvent>) {
EVENT_CHANNEL.get_or_init(|| unbounded())
}
/// Current input state for tracking drags and modifiers /// Current input state for tracking drags and modifiers
static INPUT_STATE: Mutex<InputState> = Mutex::new(InputState { static INPUT_STATE: Mutex<InputState> = Mutex::new(InputState {
@@ -83,13 +88,13 @@ pub fn push_window_event(event: &WindowEvent) {
MouseButton::Middle => input_state.middle_pressed = *state == ElementState::Pressed, MouseButton::Middle => input_state.middle_pressed = *state == ElementState::Pressed,
} }
if let Ok(mut buf) = BUFFER.lock() { // Send to lock-free channel (never blocks or fails)
buf.push(RawWinitEvent::MouseButton { let (sender, _) = get_event_channel();
button: mouse_button, let _ = sender.send(RawWinitEvent::MouseButton {
state: *state, button: mouse_button,
position, state: *state,
}); position,
} });
} }
} }
@@ -101,9 +106,8 @@ pub fn push_window_event(event: &WindowEvent) {
// Generate drag events for any pressed buttons // Generate drag events for any pressed buttons
if input_state.left_pressed || input_state.right_pressed || input_state.middle_pressed { if input_state.left_pressed || input_state.right_pressed || input_state.middle_pressed {
if let Ok(mut buf) = BUFFER.lock() { let (sender, _) = get_event_channel();
buf.push(RawWinitEvent::CursorMoved { position: pos }); let _ = sender.send(RawWinitEvent::CursorMoved { position: pos });
}
} }
} }
} }
@@ -112,13 +116,12 @@ pub fn push_window_event(event: &WindowEvent) {
// Only handle physical keys // Only handle physical keys
if let PhysicalKey::Code(key_code) = key_event.physical_key { if let PhysicalKey::Code(key_code) = key_event.physical_key {
if let Ok(input_state) = INPUT_STATE.lock() { if let Ok(input_state) = INPUT_STATE.lock() {
if let Ok(mut buf) = BUFFER.lock() { let (sender, _) = get_event_channel();
buf.push(RawWinitEvent::Keyboard { let _ = sender.send(RawWinitEvent::Keyboard {
key: key_code, key: key_code,
state: key_event.state, state: key_event.state,
modifiers: input_state.modifiers, modifiers: input_state.modifiers,
}); });
}
} }
} }
} }
@@ -141,12 +144,11 @@ pub fn push_window_event(event: &WindowEvent) {
}; };
if let Ok(input_state) = INPUT_STATE.lock() { if let Ok(input_state) = INPUT_STATE.lock() {
if let Ok(mut buf) = BUFFER.lock() { let (sender, _) = get_event_channel();
buf.push(RawWinitEvent::MouseWheel { let _ = sender.send(RawWinitEvent::MouseWheel {
delta: scroll_delta, delta: scroll_delta,
position: input_state.last_position, position: input_state.last_position,
}); });
}
} }
} }
@@ -157,17 +159,15 @@ pub fn push_window_event(event: &WindowEvent) {
/// Drain all buffered winit events and convert to InputEvents /// Drain all buffered winit events and convert to InputEvents
/// ///
/// Call this from your engine's input processing to consume events. /// Call this from your engine's input processing to consume events.
/// This uses a lock-free channel so it never blocks and can't silently drop events.
pub fn drain_as_input_events() -> Vec<InputEvent> { pub fn drain_as_input_events() -> Vec<InputEvent> {
BUFFER let (_, receiver) = get_event_channel();
.lock()
.ok() // Drain all events from the channel
.map(|mut b| { receiver
std::mem::take(&mut *b) .try_iter()
.into_iter() .filter_map(raw_to_input_event)
.filter_map(raw_to_input_event) .collect()
.collect()
})
.unwrap_or_default()
} }
/// Convert a raw winit event to an engine InputEvent /// Convert a raw winit event to an engine InputEvent