1use frus_core::Scene;
5
6use crate::compositor::{preferred_sample_count, Painters};
7
8const CLEAR_COLOR: wgpu::Color = wgpu::Color {
10 r: 0.05,
11 g: 0.05,
12 b: 0.08,
13 a: 1.0,
14};
15
16pub fn backdrop() -> frus_core::Color {
20 frus_core::Color::rgb(
22 CLEAR_COLOR.r as f32,
23 CLEAR_COLOR.g as f32,
24 CLEAR_COLOR.b as f32,
25 )
26 .to_srgb()
27}
28
29static PIPELINE_CACHE_DIR: std::sync::Mutex<Option<std::path::PathBuf>> =
33 std::sync::Mutex::new(None);
34
35pub fn set_pipeline_cache_dir(dir: std::path::PathBuf) {
38 if let Ok(mut slot) = PIPELINE_CACHE_DIR.lock() {
39 *slot = Some(dir);
40 }
41}
42
43pub struct Renderer {
45 surface: wgpu::Surface<'static>,
46 device: wgpu::Device,
47 queue: wgpu::Queue,
48 config: wgpu::SurfaceConfiguration,
49 render_format: wgpu::TextureFormat,
52 painters: Painters,
53 startup: StartupTimes,
55 lost: std::sync::Arc<std::sync::atomic::AtomicBool>,
57 timings: RenderTimings,
59 gpu_timer: Option<crate::gpu_timer::GpuTimer>,
61 see_through: bool,
63}
64
65impl Renderer {
66 pub async fn new(
71 target: impl Into<wgpu::SurfaceTarget<'static>>,
72 width: u32,
73 height: u32,
74 ) -> anyhow::Result<Self> {
75 Self::make(target, width, height, false).await
76 }
77
78 pub async fn see_through(
90 target: impl Into<wgpu::SurfaceTarget<'static>>,
91 width: u32,
92 height: u32,
93 ) -> anyhow::Result<Self> {
94 Self::make(target, width, height, true).await
95 }
96
97 pub fn sees_through(&self) -> bool {
100 self.see_through
101 }
102
103 async fn make(
104 target: impl Into<wgpu::SurfaceTarget<'static>>,
105 width: u32,
106 height: u32,
107 see_through: bool,
108 ) -> anyhow::Result<Self> {
109 if see_through && !cfg!(windows) {
110 anyhow::bail!("a see-through window is composed by DirectComposition, on Windows");
111 }
112 let started = web_time::Instant::now();
113 let mut descriptor = wgpu::InstanceDescriptor {
114 backends: wgpu::Backends::PRIMARY,
115 ..wgpu::InstanceDescriptor::new_without_display_handle()
116 };
117 if see_through {
118 descriptor.backends = wgpu::Backends::DX12;
121 descriptor.backend_options.dx12.presentation_system =
122 wgpu::Dx12SwapchainKind::DxgiFromVisual;
123 }
124 #[cfg(target_os = "android")]
128 if std::env::var_os("WGPU_DEBUG").is_none() {
129 descriptor.flags.remove(wgpu::InstanceFlags::DEBUG);
130 }
131 #[cfg(any(target_os = "android", target_os = "linux"))]
136 if let Some(why) = vulkan_unusable(descriptor.flags).await {
137 log::info!("{why}: OpenGL ES instead");
138 descriptor.backends = wgpu::Backends::GL;
139 }
140 if let Some(backends) = wgpu::Backends::from_env() {
144 log::info!("GPU backends from WGPU_BACKEND: {backends:?}");
145 descriptor.backends = backends;
146 }
147 #[cfg(windows)]
151 {
152 let dxc = std::env::current_exe()
153 .ok()
154 .and_then(|exe| Some(exe.parent()?.join("dxcompiler.dll")))
155 .filter(|dll| dll.is_file());
156 match dxc {
157 Some(dll) => {
158 log::info!("DirectX 12 shaders: DXC ({})", dll.display());
159 descriptor.backend_options.dx12.shader_compiler =
160 wgpu::Dx12Compiler::DynamicDxc {
161 dxc_path: dll.display().to_string(),
162 };
163 }
164 None => log::info!(
165 "DirectX 12 shaders: Windows' own compiler (no dxcompiler.dll beside the \
166 program: a slower start)"
167 ),
168 }
169 }
170 let instance = wgpu::Instance::new(descriptor);
171
172 let surface = instance.create_surface(target)?;
173
174 let adapter = instance
175 .request_adapter(&wgpu::RequestAdapterOptions {
176 power_preference: if cfg!(target_arch = "wasm32") {
180 wgpu::PowerPreference::None
181 } else {
182 wgpu::PowerPreference::HighPerformance
183 },
184 compatible_surface: Some(&surface),
185 force_fallback_adapter: false,
186 ..Default::default()
187 })
188 .await?;
189
190 log::info!("GPU adapter: {:?}", adapter.get_info());
191
192 let downlevel = if adapter
198 .get_downlevel_capabilities()
199 .flags
200 .contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
201 {
202 wgpu::Limits::downlevel_defaults()
203 } else {
204 wgpu::Limits::downlevel_webgl2_defaults()
205 };
206 let required_limits = downlevel.using_resolution(adapter.limits());
207
208 let adapter_time = started.elapsed();
209 let device_started = web_time::Instant::now();
210 let (device, queue) = adapter
211 .request_device(&wgpu::DeviceDescriptor {
212 label: Some("frus.device"),
213 required_features: adapter.features()
216 & (wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::PIPELINE_CACHE),
217 required_limits,
218 memory_hints: wgpu::MemoryHints::Performance,
219 ..Default::default()
220 })
221 .await?;
222 let device_time = device_started.elapsed();
223 let lost = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
226 {
227 let lost = lost.clone();
228 device.set_device_lost_callback(move |reason, message| {
229 log::warn!("the GPU device was lost ({reason:?}): {message}");
230 lost.store(true, std::sync::atomic::Ordering::SeqCst);
231 });
232 }
233
234 let caps = surface.get_capabilities(&adapter);
235 let alpha_mode = if see_through {
236 if !caps
237 .alpha_modes
238 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
239 {
240 anyhow::bail!(
241 "no premultiplied composition on this surface: {:?}",
242 caps.alpha_modes
243 );
244 }
245 wgpu::CompositeAlphaMode::PreMultiplied
246 } else {
247 caps.alpha_modes[0]
248 };
249 let format = caps
250 .formats
251 .iter()
252 .copied()
253 .find(|f| f.is_srgb())
254 .unwrap_or(caps.formats[0]);
255 let render_format = format.add_srgb_suffix();
261 let view_formats = if render_format == format {
262 vec![]
263 } else {
264 vec![render_format]
265 };
266
267 let config = wgpu::SurfaceConfiguration {
268 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
269 format,
270 color_space: wgpu::SurfaceColorSpace::Auto,
271 width,
272 height,
273 present_mode: wgpu::PresentMode::Fifo,
281 alpha_mode,
282 view_formats,
283 desired_maximum_frame_latency: 2,
284 };
285 log::info!(
286 "present mode: {:?}, of {:?}",
287 config.present_mode,
288 caps.present_modes
289 );
290 surface.configure(&device, &config);
291
292 let sample_count = preferred_sample_count(&adapter, render_format);
294 log::info!("MSAA: {sample_count}×");
295
296 let pipelines_started = web_time::Instant::now();
297 let (pipeline_cache, cache_file, cached) = open_pipeline_cache(&adapter, &device);
298 let mut painters = Painters::new(
299 &device,
300 &queue,
301 render_format,
302 sample_count,
303 pipeline_cache.as_ref(),
304 );
305 let pipelines = pipelines_started.elapsed();
306 let warm_up_started = web_time::Instant::now();
308 painters.warm_up(&device, &queue, render_format);
309 if let (Some(cache), Some(file)) = (pipeline_cache.as_ref(), cache_file.as_ref()) {
310 keep_pipeline_cache(cache, file, cached.as_deref());
311 }
312 let startup = StartupTimes {
313 adapter: adapter_time,
314 device: device_time,
315 pipelines,
316 warm_up: warm_up_started.elapsed(),
317 };
318
319 let gpu_timer = crate::gpu_timer::GpuTimer::new(&device, &queue);
320 log::info!(
321 "GPU clock: {}",
322 if gpu_timer.is_some() {
323 "frames timed on the GPU"
324 } else {
325 "none, frames timed on the CPU only"
326 }
327 );
328 Ok(Self {
329 surface,
330 device,
331 queue,
332 config,
333 render_format,
334 gpu_timer,
335 painters,
336 startup,
337 lost,
338 timings: RenderTimings::default(),
339 see_through,
340 })
341 }
342
343 pub fn is_lost(&self) -> bool {
345 self.lost.load(std::sync::atomic::Ordering::SeqCst)
346 }
347
348 #[doc(hidden)]
351 pub fn lose_device_for_testing(&self) {
352 self.device.destroy();
353 }
354
355 pub fn startup_times(&self) -> StartupTimes {
357 self.startup
358 }
359
360 pub fn resize(&mut self, width: u32, height: u32) {
363 if width > 0 && height > 0 {
364 self.config.width = width;
365 self.config.height = height;
366 self.surface.configure(&self.device, &self.config);
367 }
368 }
369
370 pub fn reconfigure(&mut self) {
372 self.surface.configure(&self.device, &self.config);
373 }
374
375 pub fn last_timings(&self) -> RenderTimings {
380 self.timings
381 }
382
383 pub fn render(&mut self, scene: &Scene) -> RenderOutcome {
385 let _ = self.device.poll(wgpu::PollType::Poll);
387 if self.lost.load(std::sync::atomic::Ordering::SeqCst) {
388 return RenderOutcome::DeviceLost;
389 }
390 let clock = Clock::start();
391 let frame = match self.surface.get_current_texture() {
392 wgpu::CurrentSurfaceTexture::Success(frame)
393 | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
394 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
395 return RenderOutcome::Skipped;
396 }
397 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
398 return RenderOutcome::NeedsReconfigure;
399 }
400 wgpu::CurrentSurfaceTexture::Validation => {
401 log::warn!("frame skipped: surface validation error");
402 return RenderOutcome::Skipped;
403 }
404 };
405 let acquired = clock.lap();
406 let timed = self
407 .gpu_timer
408 .as_mut()
409 .and_then(|timer| timer.begin(&self.device, &self.queue));
410 let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
411 format: Some(self.render_format),
412 ..Default::default()
413 });
414
415 self.painters.render(
416 &self.device,
417 &self.queue,
418 self.render_format,
419 &view,
420 self.config.width,
421 self.config.height,
422 scene,
423 Some(if self.see_through {
425 wgpu::Color::TRANSPARENT
426 } else {
427 CLEAR_COLOR
428 }),
429 );
430
431 if let (Some(timer), Some(at)) = (self.gpu_timer.as_mut(), timed) {
432 timer.end(&self.device, &self.queue, at);
433 }
434 let drawn = clock.lap();
435 self.queue.present(frame);
436 let presented = clock.lap();
437 self.timings = RenderTimings {
438 acquire: acquired,
439 draw: drawn - acquired,
440 present: presented - drawn,
441 gpu: self.gpu_timer.as_ref().and_then(|timer| timer.last_ms()),
442 };
443 RenderOutcome::Presented
444 }
445}
446
447#[cfg(any(target_os = "android", target_os = "linux"))]
451async fn vulkan_unusable(flags: wgpu::InstanceFlags) -> Option<&'static str> {
452 if std::env::var_os("WGPU_BACKEND").is_some() {
453 return None;
454 }
455 let probe = wgpu::Instance::new(wgpu::InstanceDescriptor {
456 backends: wgpu::Backends::VULKAN,
457 flags,
458 ..wgpu::InstanceDescriptor::new_without_display_handle()
459 });
460 let adapters = probe.enumerate_adapters(wgpu::Backends::VULKAN).await;
461 let emulator = |adapter: &wgpu::Adapter| {
462 let info = adapter.get_info();
463 info.driver.eq_ignore_ascii_case("gfxstream") || info.name.contains("Goldfish")
464 };
465 if adapters.is_empty() {
466 Some("no Vulkan driver on this device")
467 } else if adapters.iter().all(emulator) {
468 Some("the emulator's Vulkan driver fails at the first frame")
469 } else {
470 None
471 }
472}
473
474#[derive(Clone, Copy, Debug, Default, PartialEq)]
477pub struct RenderTimings {
478 pub acquire: f32,
480 pub draw: f32,
482 pub present: f32,
484 pub gpu: Option<f32>,
487}
488
489struct Clock {
491 #[cfg(not(target_arch = "wasm32"))]
492 start: std::time::Instant,
493}
494
495impl Clock {
496 fn start() -> Self {
497 Self {
498 #[cfg(not(target_arch = "wasm32"))]
499 start: std::time::Instant::now(),
500 }
501 }
502
503 fn lap(&self) -> f32 {
504 #[cfg(not(target_arch = "wasm32"))]
505 return self.start.elapsed().as_secs_f32() * 1000.0;
506 #[cfg(target_arch = "wasm32")]
507 0.0
508 }
509}
510
511pub enum RenderOutcome {
513 Presented,
515 Skipped,
518 NeedsReconfigure,
520 DeviceLost,
523}
524
525fn open_pipeline_cache(
529 adapter: &wgpu::Adapter,
530 device: &wgpu::Device,
531) -> (
532 Option<wgpu::PipelineCache>,
533 Option<std::path::PathBuf>,
534 Option<Vec<u8>>,
535) {
536 if !device.features().contains(wgpu::Features::PIPELINE_CACHE) {
537 return (None, None, None);
538 }
539 let dir = PIPELINE_CACHE_DIR.lock().ok().and_then(|slot| slot.clone());
540 let Some(file) = dir
541 .zip(wgpu::util::pipeline_cache_key(&adapter.get_info()))
542 .map(|(dir, key)| dir.join(format!("frus-{key}")))
543 else {
544 return (None, None, None);
545 };
546 let cached = std::fs::read(&file).ok();
547 let cache = unsafe {
551 device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
552 label: Some("frus.pipelines"),
553 data: cached.as_deref(),
554 fallback: true,
555 })
556 };
557 log::info!(
558 "pipeline cache: {}",
559 match &cached {
560 Some(data) => format!("{} KB read from the previous launch", data.len() / 1024),
561 None => "none yet: the pipelines are compiled, and kept for the next launch".into(),
562 }
563 );
564 (Some(cache), Some(file), cached)
565}
566
567fn keep_pipeline_cache(cache: &wgpu::PipelineCache, file: &std::path::Path, read: Option<&[u8]>) {
569 let Some(data) = cache.get_data() else {
570 return;
571 };
572 if read == Some(data.as_slice()) {
573 return;
574 }
575 if let Some(dir) = file.parent() {
576 let _ = std::fs::create_dir_all(dir);
577 }
578 let partial = file.with_extension("partial");
580 if std::fs::write(&partial, &data).is_ok() {
581 let _ = std::fs::rename(&partial, file);
582 }
583}
584
585#[derive(Clone, Copy, Debug, Default, PartialEq)]
588pub struct StartupTimes {
589 pub adapter: std::time::Duration,
591 pub device: std::time::Duration,
593 pub pipelines: std::time::Duration,
595 pub warm_up: std::time::Duration,
597}