Skip to main content

dreamwell_runtime/
lib.rs

1// Dreamwell Runtime v1.0.0 — cross-platform GPU-accelerated game client.
2//
3// Consumes dreamwell-engine for game logic, dreamwell-gpu for rendering,
4// dreamwell-fabric for frame orchestration.
5//
6// Two authority modes:
7//   Local  — standalone, test, offline, editor preview
8//   Remote — SpacetimeDB maincloud (feature-gated behind `multiplayer`)
9//
10// Same runtime contracts, two backends.
11
12pub mod app;
13pub mod game_state;
14pub mod input;
15pub mod renderer;
16pub mod scene;
17pub mod time;
18pub mod window;
19
20pub mod play;
21
22// ── v1.0.0 API modules ─────────────────────────────────────────────────
23pub mod authority;
24pub mod mirror;
25pub mod prelude;
26pub mod presentation;
27pub mod runtime_ui;
28pub mod sync;
29
30/// Runtime configuration.
31pub struct RuntimeConfig {
32    /// Window title.
33    pub title: String,
34    /// Initial window width.
35    pub width: u32,
36    /// Initial window height.
37    pub height: u32,
38    /// Enable vertical sync.
39    pub vsync: bool,
40    /// MSAA sample count (1 = no MSAA, 4 = recommended).
41    pub msaa_samples: u32,
42    /// Authority mode: Local (offline) or Remote (multiplayer).
43    pub authority_mode: AuthorityMode,
44    /// Root directory for content assets.
45    pub asset_root: std::path::PathBuf,
46    /// Waymark pack JSON file path to load on startup.
47    pub pack_file: Option<String>,
48    /// Tapestry lock file path for play mode.
49    pub lock_file: Option<String>,
50    /// SpacetimeDB host URL (enables remote authority when set).
51    pub db_host: Option<String>,
52    /// SpacetimeDB database name.
53    pub db_name: String,
54}
55
56/// Authority mode selection.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum AuthorityMode {
59    /// Local authority — standalone, test, offline, editor preview.
60    Local,
61    /// Remote authority — SpacetimeDB maincloud.
62    Remote,
63}
64
65impl Default for RuntimeConfig {
66    fn default() -> Self {
67        Self {
68            title: "Dreamwell".into(),
69            width: 1920,
70            height: 1080,
71            vsync: true,
72            msaa_samples: 1,
73            authority_mode: AuthorityMode::Local,
74            asset_root: "assets".into(),
75            pack_file: None,
76            lock_file: None,
77            db_host: None,
78            db_name: "dreamwell".into(),
79        }
80    }
81}
82
83/// Runtime phase model — explicit frame phases for profiling and test hooks.
84///
85/// Each phase has a defined purpose and mutation scope:
86/// - PollInput: collect window/input events (read-only scene)
87/// - PollAuthority: non-blocking check for authority events
88/// - StageAuthorityEvents: buffer events for atomic application
89/// - ApplyAuthorityEvents: apply staged events at tick boundary
90/// - UpdateMirror: update client mirror from applied events
91/// - UpdatePresentation: rebuild render-facing interpolated state
92/// - AssembleFrame: prepare observer context + render packet
93/// - Render: submit DreamFabric frame
94/// - DrainSync: drain sync packet and enqueue outgoing work
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum RuntimePhase {
97    PollInput,
98    PollAuthority,
99    StageAuthorityEvents,
100    ApplyAuthorityEvents,
101    UpdateMirror,
102    UpdatePresentation,
103    AssembleFrame,
104    Render,
105    DrainSync,
106}
107
108impl RuntimePhase {
109    /// All phases in execution order.
110    pub const ALL: &'static [RuntimePhase] = &[
111        Self::PollInput,
112        Self::PollAuthority,
113        Self::StageAuthorityEvents,
114        Self::ApplyAuthorityEvents,
115        Self::UpdateMirror,
116        Self::UpdatePresentation,
117        Self::AssembleFrame,
118        Self::Render,
119        Self::DrainSync,
120    ];
121
122    /// Phase name for profiler labels and logging.
123    pub fn label(self) -> &'static str {
124        match self {
125            Self::PollInput => "poll_input",
126            Self::PollAuthority => "poll_authority",
127            Self::StageAuthorityEvents => "stage_authority",
128            Self::ApplyAuthorityEvents => "apply_authority",
129            Self::UpdateMirror => "update_mirror",
130            Self::UpdatePresentation => "update_presentation",
131            Self::AssembleFrame => "assemble_frame",
132            Self::Render => "render",
133            Self::DrainSync => "drain_sync",
134        }
135    }
136}
137
138/// The Dreamwell runtime. Call `run()` to start the event loop.
139pub struct Runtime {
140    config: RuntimeConfig,
141}
142
143impl Runtime {
144    pub fn new(config: RuntimeConfig) -> Result<Self, Box<dyn std::error::Error>> {
145        Ok(Self { config })
146    }
147
148    /// Run the runtime. This blocks on the winit event loop.
149    #[cfg(not(target_arch = "wasm32"))]
150    pub fn run(self) -> Result<(), Box<dyn std::error::Error>> {
151        // On Windows, the event loop may run on a non-main thread (8MB stack thread
152        // spawned by main.rs). Use `with_any_thread(true)` to allow this.
153        #[cfg(target_os = "windows")]
154        let event_loop = {
155            use winit::platform::windows::EventLoopBuilderExtWindows;
156            winit::event_loop::EventLoop::builder().with_any_thread(true).build()?
157        };
158        #[cfg(not(target_os = "windows"))]
159        let event_loop = winit::event_loop::EventLoop::new()?;
160
161        let mut runtime_app = app::RuntimeApp::new(RuntimeConfig {
162            title: self.config.title.clone(),
163            width: self.config.width,
164            height: self.config.height,
165            authority_mode: self.config.authority_mode,
166            pack_file: self.config.pack_file.clone(),
167            lock_file: self.config.lock_file.clone(),
168            db_host: self.config.db_host.clone(),
169            db_name: self.config.db_name.clone(),
170            ..Default::default()
171        })
172        .map_err(|e| format!("{e:?}"))?;
173
174        // Mount pack file if provided via CLI args.
175        if let Some(ref pack_path) = self.config.pack_file {
176            match std::fs::read_to_string(pack_path) {
177                Ok(json) => {
178                    if let Err(e) = runtime_app.mount_pack(&json) {
179                        log::warn!("Failed to mount pack {}: {}", pack_path, e);
180                    }
181                }
182                Err(e) => log::warn!("Failed to read pack file {}: {}", pack_path, e),
183            }
184        }
185
186        event_loop.run_app(&mut runtime_app)?;
187        Ok(())
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn runtime_config_default() {
197        let c = RuntimeConfig::default();
198        assert_eq!(c.title, "Dreamwell");
199        assert_eq!(c.width, 1280);
200        assert_eq!(c.height, 720);
201        assert!(c.vsync);
202        assert_eq!(c.msaa_samples, 4);
203        assert_eq!(c.authority_mode, AuthorityMode::Local);
204        assert!(c.pack_file.is_none());
205        assert!(c.db_host.is_none());
206        assert_eq!(c.db_name, "dreamwell");
207    }
208
209    #[test]
210    fn runtime_phase_count() {
211        assert_eq!(RuntimePhase::ALL.len(), 9);
212    }
213
214    #[test]
215    fn runtime_phase_labels() {
216        assert_eq!(RuntimePhase::PollInput.label(), "poll_input");
217        assert_eq!(RuntimePhase::Render.label(), "render");
218        assert_eq!(RuntimePhase::DrainSync.label(), "drain_sync");
219    }
220
221    #[test]
222    fn authority_mode_default_local() {
223        assert_eq!(RuntimeConfig::default().authority_mode, AuthorityMode::Local);
224    }
225}