tessera_ui/
entry_point.rs1use 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
23pub struct EntryPoint {
25 session: crate::RuntimeSession,
26 entry: Box<dyn Fn()>,
27 registry: EntryRegistry,
28 config: TesseraConfig,
29}
30
31impl EntryPoint {
32 pub fn runtime_session(&self) -> &crate::RuntimeSession {
34 &self.session
35 }
36 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 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 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 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 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 pub fn config(mut self, config: TesseraConfig) -> Self {
80 self.config = config;
81 self
82 }
83
84 #[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 #[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 #[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 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 #[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 #[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}