Skip to main content

tessera_ui/
entry_point.rs

1//! Application entry builder for registering packages and launching the
2//! renderer.
3//!
4//! ## Usage
5//!
6//! Configure app startup with packages and plugins before launching the
7//! renderer.
8
9use std::sync::Arc;
10
11use parking_lot::RwLock;
12
13use crate::{
14    entry_registry::{EntryRegistry, TesseraPackage},
15    plugin::Plugin,
16    render_module::RenderModule,
17    renderer::{Renderer, TesseraConfig},
18};
19
20#[cfg(target_os = "android")]
21use winit::platform::android::activity::AndroidApp;
22
23/// Builder for application entry configuration and startup.
24pub struct EntryPoint {
25    session: crate::RuntimeSession,
26    entry: Box<dyn Fn()>,
27    registry: EntryRegistry,
28    config: TesseraConfig,
29}
30
31impl EntryPoint {
32    /// Returns the runtime owner used by this application entry.
33    pub fn runtime_session(&self) -> &crate::RuntimeSession {
34        &self.session
35    }
36    /// Creates a new entry point builder from the root UI function.
37    pub fn new<Output>(entry: impl Fn() -> Output + 'static) -> Self {
38        let session = crate::RuntimeSession::new();
39        session.activate();
40        Self {
41            session,
42            entry: Box::new(move || {
43                entry();
44            }),
45            registry: EntryRegistry::new(),
46            config: TesseraConfig::default(),
47        }
48    }
49
50    /// Adds a render module to the entry registry.
51    pub fn module(mut self, module: impl RenderModule + 'static) -> Self {
52        let _session = self.session.enter();
53        self.registry.add_module(module);
54        self
55    }
56
57    /// Registers a plugin instance with this application session.
58    pub fn plugin(mut self, plugin: impl Plugin) -> Self {
59        let _session = self.session.enter();
60        self.registry.register_plugin(plugin);
61        self
62    }
63
64    /// Registers a boxed plugin instance with this application session.
65    pub fn plugin_boxed<P: Plugin>(mut self, plugin: Arc<RwLock<P>>) -> Self {
66        let _session = self.session.enter();
67        self.registry.register_plugin_boxed(plugin);
68        self
69    }
70
71    /// Registers a package into the entry registry.
72    pub fn package(mut self, package: impl TesseraPackage) -> Self {
73        let _session = self.session.enter();
74        self.registry.register_package(package);
75        self
76    }
77
78    /// Overrides the renderer configuration for this entry.
79    pub fn config(mut self, config: TesseraConfig) -> Self {
80        self.config = config;
81        self
82    }
83
84    /// Creates an offscreen renderer with this entry's packages and GPU settings.
85    #[cfg(all(feature = "headless", not(target_family = "wasm")))]
86    pub fn into_headless(
87        self,
88        mut config: crate::HeadlessConfig,
89    ) -> Result<crate::HeadlessRenderer, crate::headless::HeadlessError> {
90        let _session = self.session.enter();
91        config.sample_count = self.config.sample_count;
92        config.max_non_sampler_bindings = self.config.max_non_sampler_bindings;
93        crate::HeadlessRenderer::new(self.entry, self.registry.finish(), config)
94    }
95
96    /// Runs the entry point on desktop platforms.
97    #[cfg(not(any(target_os = "android", target_os = "ios", target_family = "wasm")))]
98    pub fn run_desktop(self) -> Result<(), winit::error::EventLoopError> {
99        self.run_native()
100    }
101
102    /// Runs the entry point on iOS from the application's main thread.
103    #[cfg(target_os = "ios")]
104    pub fn run_ios(self) -> Result<(), winit::error::EventLoopError> {
105        self.run_native()
106    }
107
108    #[cfg(all(not(target_os = "android"), not(target_family = "wasm")))]
109    fn run_native(self) -> Result<(), winit::error::EventLoopError> {
110        let _session = self.session.enter();
111        #[cfg(feature = "headless")]
112        if crate::headless::headless_mode_requested() {
113            // Headless mode owns stdout for its JSONL protocol, so tracing is
114            // configured there and the window/event-loop path is skipped.
115            if let Err(err) =
116                crate::headless::run_headless(self.entry, self.registry.finish(), self.config)
117            {
118                eprintln!("headless session failed: {err}");
119                std::process::exit(1);
120            }
121            return Ok(());
122        }
123
124        init_tracing();
125        #[cfg(debug_assertions)]
126        init_deadlock_detection();
127        Renderer::run_with_config(self.entry, self.registry.finish(), self.config)
128    }
129
130    /// Runs the entry point on web platforms.
131    #[cfg(target_family = "wasm")]
132    pub fn run_web(self) -> Result<(), winit::error::EventLoopError> {
133        let _session = self.session.enter();
134        init_web_panic_hook();
135        init_tracing();
136        Renderer::run_web_with_config(self.entry, self.registry.finish(), self.config)
137    }
138
139    /// Runs the entry point on Android.
140    #[cfg(target_os = "android")]
141    pub fn run_android(self, android_app: AndroidApp) -> Result<(), winit::error::EventLoopError> {
142        let _session = self.session.enter();
143        init_tracing();
144        #[cfg(debug_assertions)]
145        init_deadlock_detection();
146        Renderer::run_with_config(self.entry, self.registry.finish(), android_app, self.config)
147    }
148}
149
150#[cfg(target_family = "wasm")]
151fn init_web_panic_hook() {
152    console_error_panic_hook::set_once();
153}
154
155#[cfg(all(debug_assertions, not(target_family = "wasm")))]
156struct DeadlockMonitor {
157    stop: std::sync::mpsc::Sender<()>,
158    worker: Option<std::thread::JoinHandle<()>>,
159}
160
161#[cfg(all(debug_assertions, not(target_family = "wasm")))]
162impl Default for DeadlockMonitor {
163    fn default() -> Self {
164        let (stop, receive) = std::sync::mpsc::channel();
165        let worker = std::thread::spawn(move || {
166            while matches!(
167                receive.recv_timeout(std::time::Duration::from_secs(10)),
168                Err(std::sync::mpsc::RecvTimeoutError::Timeout)
169            ) {
170                let deadlocks = parking_lot::deadlock::check_deadlock();
171                if deadlocks.is_empty() {
172                    continue;
173                }
174                eprintln!("{} deadlocks detected", deadlocks.len());
175                for (index, threads) in deadlocks.iter().enumerate() {
176                    eprintln!("Deadlock #{index}");
177                    for thread in threads {
178                        eprintln!("Thread Id {:#?}", thread.thread_id());
179                        eprintln!("{:?}", thread.backtrace());
180                    }
181                }
182            }
183        });
184        Self {
185            stop,
186            worker: Some(worker),
187        }
188    }
189}
190
191#[cfg(all(debug_assertions, not(target_family = "wasm")))]
192impl Drop for DeadlockMonitor {
193    fn drop(&mut self) {
194        let _ = self.stop.send(());
195        if let Some(worker) = self.worker.take() {
196            let _ = worker.join();
197        }
198    }
199}
200
201#[cfg(all(debug_assertions, not(target_family = "wasm")))]
202fn init_deadlock_detection() {
203    let _ = crate::local_runtime_resource::<DeadlockMonitor>();
204}
205
206fn init_tracing() {
207    #[cfg(target_family = "wasm")]
208    {
209        let _ = tracing_subscriber::fmt()
210            .with_max_level(tracing::Level::INFO)
211            .with_writer(WebConsoleMakeWriter)
212            .with_ansi(false)
213            .without_time()
214            .try_init();
215    }
216
217    #[cfg(target_os = "android")]
218    {
219        let _ = tracing_subscriber::fmt()
220            .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
221            .with_max_level(tracing::Level::INFO)
222            .try_init();
223    }
224
225    #[cfg(all(not(target_os = "android"), not(target_family = "wasm")))]
226    {
227        let filter = match tracing_subscriber::EnvFilter::try_from_default_env() {
228            Ok(filter) => filter,
229            Err(_) => match tracing_subscriber::EnvFilter::try_new("error,tessera_ui=info") {
230                Ok(filter) => filter,
231                Err(_) => tracing_subscriber::EnvFilter::new("error"),
232            },
233        };
234
235        let _ = tracing_subscriber::fmt()
236            .pretty()
237            .with_env_filter(filter)
238            .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
239            .try_init();
240    }
241}
242
243#[cfg(target_family = "wasm")]
244struct WebConsoleMakeWriter;
245
246#[cfg(target_family = "wasm")]
247impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for WebConsoleMakeWriter {
248    type Writer = WebConsoleWriter;
249
250    fn make_writer(&'a self) -> Self::Writer {
251        WebConsoleWriter {
252            level: tracing::Level::INFO,
253            buffer: Vec::new(),
254        }
255    }
256
257    fn make_writer_for(&'a self, metadata: &tracing::Metadata<'_>) -> Self::Writer {
258        WebConsoleWriter {
259            level: *metadata.level(),
260            buffer: Vec::new(),
261        }
262    }
263}
264
265#[cfg(target_family = "wasm")]
266struct WebConsoleWriter {
267    level: tracing::Level,
268    buffer: Vec<u8>,
269}
270
271#[cfg(target_family = "wasm")]
272impl std::io::Write for WebConsoleWriter {
273    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
274        self.buffer.extend_from_slice(buffer);
275        Ok(buffer.len())
276    }
277
278    fn flush(&mut self) -> std::io::Result<()> {
279        if !self.buffer.is_empty() {
280            let message = String::from_utf8_lossy(&self.buffer);
281            let message = message.trim_end_matches(['\r', '\n']).into();
282            match self.level {
283                tracing::Level::ERROR => web_sys::console::error_1(&message),
284                tracing::Level::WARN => web_sys::console::warn_1(&message),
285                tracing::Level::INFO => web_sys::console::info_1(&message),
286                _ => web_sys::console::debug_1(&message),
287            }
288            self.buffer.clear();
289        }
290        Ok(())
291    }
292}
293
294#[cfg(target_family = "wasm")]
295impl Drop for WebConsoleWriter {
296    fn drop(&mut self) {
297        let _ = std::io::Write::flush(self);
298    }
299}