use frus_core::Scene;
use crate::compositor::{preferred_sample_count, Painters};
const CLEAR_COLOR: wgpu::Color = wgpu::Color {
r: 0.05,
g: 0.05,
b: 0.08,
a: 1.0,
};
pub fn backdrop() -> frus_core::Color {
frus_core::Color::rgb(
CLEAR_COLOR.r as f32,
CLEAR_COLOR.g as f32,
CLEAR_COLOR.b as f32,
)
.to_srgb()
}
static PIPELINE_CACHE_DIR: std::sync::Mutex<Option<std::path::PathBuf>> =
std::sync::Mutex::new(None);
pub fn set_pipeline_cache_dir(dir: std::path::PathBuf) {
if let Ok(mut slot) = PIPELINE_CACHE_DIR.lock() {
*slot = Some(dir);
}
}
pub struct Renderer {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
render_format: wgpu::TextureFormat,
painters: Painters,
startup: StartupTimes,
lost: std::sync::Arc<std::sync::atomic::AtomicBool>,
timings: RenderTimings,
gpu_timer: Option<crate::gpu_timer::GpuTimer>,
see_through: bool,
}
impl Renderer {
pub async fn new(
target: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
) -> anyhow::Result<Self> {
Self::make(target, width, height, false).await
}
pub async fn see_through(
target: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
) -> anyhow::Result<Self> {
Self::make(target, width, height, true).await
}
pub fn sees_through(&self) -> bool {
self.see_through
}
async fn make(
target: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
see_through: bool,
) -> anyhow::Result<Self> {
if see_through && !cfg!(windows) {
anyhow::bail!("a see-through window is composed by DirectComposition, on Windows");
}
let started = web_time::Instant::now();
let mut descriptor = wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..wgpu::InstanceDescriptor::new_without_display_handle()
};
if see_through {
descriptor.backends = wgpu::Backends::DX12;
descriptor.backend_options.dx12.presentation_system =
wgpu::Dx12SwapchainKind::DxgiFromVisual;
}
#[cfg(target_os = "android")]
if std::env::var_os("WGPU_DEBUG").is_none() {
descriptor.flags.remove(wgpu::InstanceFlags::DEBUG);
}
#[cfg(any(target_os = "android", target_os = "linux"))]
if let Some(why) = vulkan_unusable(descriptor.flags).await {
log::info!("{why}: OpenGL ES instead");
descriptor.backends = wgpu::Backends::GL;
}
if let Some(backends) = wgpu::Backends::from_env() {
log::info!("GPU backends from WGPU_BACKEND: {backends:?}");
descriptor.backends = backends;
}
#[cfg(windows)]
{
let dxc = std::env::current_exe()
.ok()
.and_then(|exe| Some(exe.parent()?.join("dxcompiler.dll")))
.filter(|dll| dll.is_file());
match dxc {
Some(dll) => {
log::info!("DirectX 12 shaders: DXC ({})", dll.display());
descriptor.backend_options.dx12.shader_compiler =
wgpu::Dx12Compiler::DynamicDxc {
dxc_path: dll.display().to_string(),
};
}
None => log::info!(
"DirectX 12 shaders: Windows' own compiler (no dxcompiler.dll beside the \
program: a slower start)"
),
}
}
let instance = wgpu::Instance::new(descriptor);
let surface = instance.create_surface(target)?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: if cfg!(target_arch = "wasm32") {
wgpu::PowerPreference::None
} else {
wgpu::PowerPreference::HighPerformance
},
compatible_surface: Some(&surface),
force_fallback_adapter: false,
..Default::default()
})
.await?;
log::info!("GPU adapter: {:?}", adapter.get_info());
let downlevel = if adapter
.get_downlevel_capabilities()
.flags
.contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
{
wgpu::Limits::downlevel_defaults()
} else {
wgpu::Limits::downlevel_webgl2_defaults()
};
let required_limits = downlevel.using_resolution(adapter.limits());
let adapter_time = started.elapsed();
let device_started = web_time::Instant::now();
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frus.device"),
required_features: adapter.features()
& (wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::PIPELINE_CACHE),
required_limits,
memory_hints: wgpu::MemoryHints::Performance,
..Default::default()
})
.await?;
let device_time = device_started.elapsed();
let lost = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
{
let lost = lost.clone();
device.set_device_lost_callback(move |reason, message| {
log::warn!("the GPU device was lost ({reason:?}): {message}");
lost.store(true, std::sync::atomic::Ordering::SeqCst);
});
}
let caps = surface.get_capabilities(&adapter);
let alpha_mode = if see_through {
if !caps
.alpha_modes
.contains(&wgpu::CompositeAlphaMode::PreMultiplied)
{
anyhow::bail!(
"no premultiplied composition on this surface: {:?}",
caps.alpha_modes
);
}
wgpu::CompositeAlphaMode::PreMultiplied
} else {
caps.alpha_modes[0]
};
let format = caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(caps.formats[0]);
let render_format = format.add_srgb_suffix();
let view_formats = if render_format == format {
vec![]
} else {
vec![render_format]
};
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
color_space: wgpu::SurfaceColorSpace::Auto,
width,
height,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode,
view_formats,
desired_maximum_frame_latency: 2,
};
log::info!(
"present mode: {:?}, of {:?}",
config.present_mode,
caps.present_modes
);
surface.configure(&device, &config);
let sample_count = preferred_sample_count(&adapter, render_format);
log::info!("MSAA: {sample_count}×");
let pipelines_started = web_time::Instant::now();
let (pipeline_cache, cache_file, cached) = open_pipeline_cache(&adapter, &device);
let mut painters = Painters::new(
&device,
&queue,
render_format,
sample_count,
pipeline_cache.as_ref(),
);
let pipelines = pipelines_started.elapsed();
let warm_up_started = web_time::Instant::now();
painters.warm_up(&device, &queue, render_format);
if let (Some(cache), Some(file)) = (pipeline_cache.as_ref(), cache_file.as_ref()) {
keep_pipeline_cache(cache, file, cached.as_deref());
}
let startup = StartupTimes {
adapter: adapter_time,
device: device_time,
pipelines,
warm_up: warm_up_started.elapsed(),
};
let gpu_timer = crate::gpu_timer::GpuTimer::new(&device, &queue);
log::info!(
"GPU clock: {}",
if gpu_timer.is_some() {
"frames timed on the GPU"
} else {
"none, frames timed on the CPU only"
}
);
Ok(Self {
surface,
device,
queue,
config,
render_format,
gpu_timer,
painters,
startup,
lost,
timings: RenderTimings::default(),
see_through,
})
}
pub fn is_lost(&self) -> bool {
self.lost.load(std::sync::atomic::Ordering::SeqCst)
}
#[doc(hidden)]
pub fn lose_device_for_testing(&self) {
self.device.destroy();
}
pub fn startup_times(&self) -> StartupTimes {
self.startup
}
pub fn resize(&mut self, width: u32, height: u32) {
if width > 0 && height > 0 {
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
}
}
pub fn reconfigure(&mut self) {
self.surface.configure(&self.device, &self.config);
}
pub fn last_timings(&self) -> RenderTimings {
self.timings
}
pub fn render(&mut self, scene: &Scene) -> RenderOutcome {
let _ = self.device.poll(wgpu::PollType::Poll);
if self.lost.load(std::sync::atomic::Ordering::SeqCst) {
return RenderOutcome::DeviceLost;
}
let clock = Clock::start();
let frame = match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
return RenderOutcome::Skipped;
}
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
return RenderOutcome::NeedsReconfigure;
}
wgpu::CurrentSurfaceTexture::Validation => {
log::warn!("frame skipped: surface validation error");
return RenderOutcome::Skipped;
}
};
let acquired = clock.lap();
let timed = self
.gpu_timer
.as_mut()
.and_then(|timer| timer.begin(&self.device, &self.queue));
let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
format: Some(self.render_format),
..Default::default()
});
self.painters.render(
&self.device,
&self.queue,
self.render_format,
&view,
self.config.width,
self.config.height,
scene,
Some(if self.see_through {
wgpu::Color::TRANSPARENT
} else {
CLEAR_COLOR
}),
);
if let (Some(timer), Some(at)) = (self.gpu_timer.as_mut(), timed) {
timer.end(&self.device, &self.queue, at);
}
let drawn = clock.lap();
self.queue.present(frame);
let presented = clock.lap();
self.timings = RenderTimings {
acquire: acquired,
draw: drawn - acquired,
present: presented - drawn,
gpu: self.gpu_timer.as_ref().and_then(|timer| timer.last_ms()),
};
RenderOutcome::Presented
}
}
#[cfg(any(target_os = "android", target_os = "linux"))]
async fn vulkan_unusable(flags: wgpu::InstanceFlags) -> Option<&'static str> {
if std::env::var_os("WGPU_BACKEND").is_some() {
return None;
}
let probe = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::VULKAN,
flags,
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapters = probe.enumerate_adapters(wgpu::Backends::VULKAN).await;
let emulator = |adapter: &wgpu::Adapter| {
let info = adapter.get_info();
info.driver.eq_ignore_ascii_case("gfxstream") || info.name.contains("Goldfish")
};
if adapters.is_empty() {
Some("no Vulkan driver on this device")
} else if adapters.iter().all(emulator) {
Some("the emulator's Vulkan driver fails at the first frame")
} else {
None
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct RenderTimings {
pub acquire: f32,
pub draw: f32,
pub present: f32,
pub gpu: Option<f32>,
}
struct Clock {
#[cfg(not(target_arch = "wasm32"))]
start: std::time::Instant,
}
impl Clock {
fn start() -> Self {
Self {
#[cfg(not(target_arch = "wasm32"))]
start: std::time::Instant::now(),
}
}
fn lap(&self) -> f32 {
#[cfg(not(target_arch = "wasm32"))]
return self.start.elapsed().as_secs_f32() * 1000.0;
#[cfg(target_arch = "wasm32")]
0.0
}
}
pub enum RenderOutcome {
Presented,
Skipped,
NeedsReconfigure,
DeviceLost,
}
fn open_pipeline_cache(
adapter: &wgpu::Adapter,
device: &wgpu::Device,
) -> (
Option<wgpu::PipelineCache>,
Option<std::path::PathBuf>,
Option<Vec<u8>>,
) {
if !device.features().contains(wgpu::Features::PIPELINE_CACHE) {
return (None, None, None);
}
let dir = PIPELINE_CACHE_DIR.lock().ok().and_then(|slot| slot.clone());
let Some(file) = dir
.zip(wgpu::util::pipeline_cache_key(&adapter.get_info()))
.map(|(dir, key)| dir.join(format!("frus-{key}")))
else {
return (None, None, None);
};
let cached = std::fs::read(&file).ok();
let cache = unsafe {
device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
label: Some("frus.pipelines"),
data: cached.as_deref(),
fallback: true,
})
};
log::info!(
"pipeline cache: {}",
match &cached {
Some(data) => format!("{} KB read from the previous launch", data.len() / 1024),
None => "none yet: the pipelines are compiled, and kept for the next launch".into(),
}
);
(Some(cache), Some(file), cached)
}
fn keep_pipeline_cache(cache: &wgpu::PipelineCache, file: &std::path::Path, read: Option<&[u8]>) {
let Some(data) = cache.get_data() else {
return;
};
if read == Some(data.as_slice()) {
return;
}
if let Some(dir) = file.parent() {
let _ = std::fs::create_dir_all(dir);
}
let partial = file.with_extension("partial");
if std::fs::write(&partial, &data).is_ok() {
let _ = std::fs::rename(&partial, file);
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct StartupTimes {
pub adapter: std::time::Duration,
pub device: std::time::Duration,
pub pipelines: std::time::Duration,
pub warm_up: std::time::Duration,
}