use std::{
cell::OnceCell,
future::Future,
sync::atomic::{AtomicBool, Ordering},
task::{Context, Poll, Waker},
};
use crate::gpu_stats::FrameStats;
static FLOAT_COMPOSITION_UNSUPPORTED: AtomicBool = AtomicBool::new(false);
pub(crate) fn composition_format() -> wgpu::TextureFormat {
static FORMAT: std::sync::OnceLock<wgpu::TextureFormat> = std::sync::OnceLock::new();
let preferred = *FORMAT.get_or_init(|| {
resolve_composition_format(
crate::debug_toggles::debug_toggle("CRANPOSE_COMPOSITION_8BIT").as_deref(),
cfg!(target_os = "android"),
)
});
if FLOAT_COMPOSITION_UNSUPPORTED.load(Ordering::Relaxed) {
wgpu::TextureFormat::Rgba8Unorm
} else {
preferred
}
}
pub(crate) fn settle_composition_format(
device: &wgpu::Device,
backend: wgpu::Backend,
) -> wgpu::TextureFormat {
let format = composition_format();
if backend == wgpu::Backend::Gl
&& format != wgpu::TextureFormat::Rgba8Unorm
&& !renders_into(device, format)
{
log::warn!("[gpu-init] this device cannot draw into {format:?}; compositing in Rgba8Unorm");
FLOAT_COMPOSITION_UNSUPPORTED.store(true, Ordering::Relaxed);
}
composition_format()
}
pub(crate) fn renders_into(device: &wgpu::Device, format: wgpu::TextureFormat) -> bool {
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let _probe = create_2d_texture(
device,
format,
1,
1,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
Some("Composition format probe"),
);
let mut error = std::pin::pin!(scope.pop());
match error.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
Poll::Ready(error) => error.is_none(),
Poll::Pending => true,
}
}
fn resolve_composition_format(requested: Option<&str>, android: bool) -> wgpu::TextureFormat {
let eight_bit = match requested.map(str::trim) {
Some("1" | "true" | "yes") => true,
Some("0" | "false" | "no") => false,
_ => android,
};
if eight_bit {
wgpu::TextureFormat::Rgba8Unorm
} else {
wgpu::TextureFormat::Rgba16Float
}
}
pub(crate) fn create_2d_texture(
device: &wgpu::Device,
format: wgpu::TextureFormat,
width: u32,
height: u32,
usage: wgpu::TextureUsages,
label: Option<&str>,
) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label,
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage,
view_formats: &[],
})
}
pub(crate) struct OffscreenTarget {
pub view: wgpu::TextureView,
pub width: u32,
pub height: u32,
bytes_per_pixel: u64,
cached_bind_group: OnceCell<wgpu::BindGroup>,
}
impl OffscreenTarget {
pub(crate) fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
width: u32,
height: u32,
) -> Self {
Self::new_labeled(device, format, width, height, "Offscreen Target")
}
pub(crate) fn new_labeled(
device: &wgpu::Device,
format: wgpu::TextureFormat,
width: u32,
height: u32,
label: &'static str,
) -> Self {
let texture = create_2d_texture(
device,
format,
width,
height,
wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST,
Some(label),
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Self {
view,
width,
height,
bytes_per_pixel: crate::frame_graph::texture_format_bytes_per_pixel(format),
cached_bind_group: OnceCell::new(),
}
}
pub(crate) fn texture(&self) -> &wgpu::Texture {
self.view.texture()
}
pub(crate) fn format(&self) -> wgpu::TextureFormat {
self.texture().format()
}
fn matches_size(&self, width: u32, height: u32) -> bool {
self.width == width && self.height == height
}
pub fn get_or_create_bind_group(
&self,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
) -> &wgpu::BindGroup {
self.cached_bind_group.get_or_init(|| {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Offscreen Texture Bind Group (cached)"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
})
})
}
pub(crate) fn from_surface(texture: wgpu::Texture, view: wgpu::TextureView) -> Self {
let width = texture.width();
let height = texture.height();
let format = texture.format().remove_srgb_suffix();
Self {
view,
width,
height,
bytes_per_pixel: crate::frame_graph::texture_format_bytes_per_pixel(format),
cached_bind_group: OnceCell::new(),
}
}
}
pub fn composition_bytes_per_pixel() -> u64 {
crate::frame_graph::texture_format_bytes_per_pixel(composition_format())
}
pub(crate) struct OffscreenPool {
available: Vec<OffscreenTarget>,
format: wgpu::TextureFormat,
max_texture_dim: u32,
}
const MAX_POOLED_TARGETS: usize = 64;
const MAX_POOLED_BYTES: u64 = 128 * 1024 * 1024;
fn target_bytes(width: u32, height: u32, bytes_per_pixel: u64) -> u64 {
u64::from(width) * u64::from(height) * bytes_per_pixel
}
impl OffscreenPool {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
Self {
available: Vec::new(),
format,
max_texture_dim: device.limits().max_texture_dimension_2d,
}
}
#[cfg(test)]
fn new_with_limit(format: wgpu::TextureFormat, max_texture_dim: u32) -> Self {
Self {
available: Vec::new(),
format,
max_texture_dim,
}
}
pub fn max_texture_dim(&self) -> u32 {
self.max_texture_dim
}
pub fn pool_size(&self) -> usize {
self.available.len()
}
pub fn estimated_bytes(&self) -> usize {
self.available
.iter()
.map(|t| {
(t.width as u64)
.saturating_mul(t.height as u64)
.saturating_mul(t.bytes_per_pixel) as usize
})
.sum()
}
pub fn acquire(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
stats: Option<&FrameStats>,
) -> OffscreenTarget {
let width = width.min(self.max_texture_dim).max(1);
let height = height.min(self.max_texture_dim).max(1);
if let Some(idx) = self
.available
.iter()
.position(|t| t.matches_size(width, height))
{
if let Some(s) = stats {
s.record_offscreen_acquire(width, height, self.format, false);
}
self.available.swap_remove(idx)
} else {
if let Some(s) = stats {
s.record_offscreen_acquire(width, height, self.format, true);
}
OffscreenTarget::new(device, self.format, width, height)
}
}
pub fn release(&mut self, target: OffscreenTarget) {
self.available.push(target);
while self.available.len() > MAX_POOLED_TARGETS
|| self.pooled_bytes() > MAX_POOLED_BYTES && self.available.len() > 1
{
self.available.remove(0);
}
}
fn pooled_bytes(&self) -> u64 {
self.available
.iter()
.map(|t| target_bytes(t.width, t.height, self.bytes_per_pixel()))
.sum()
}
fn bytes_per_pixel(&self) -> u64 {
crate::frame_graph::texture_format_bytes_per_pixel(self.format)
}
pub fn texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Effect Texture Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
})
}
pub fn uniform_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Effect Uniform Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: None,
},
count: None,
}],
})
}
}
#[cfg(test)]
#[path = "tests/offscreen_tests.rs"]
mod tests;