1use std::{
2 cell::OnceCell,
3 future::Future,
4 sync::atomic::{AtomicBool, Ordering},
5 task::{Context, Poll, Waker},
6};
7
8use crate::gpu_stats::FrameStats;
9
10static FLOAT_COMPOSITION_UNSUPPORTED: AtomicBool = AtomicBool::new(false);
13
14pub(crate) fn composition_format() -> wgpu::TextureFormat {
15 static FORMAT: std::sync::OnceLock<wgpu::TextureFormat> = std::sync::OnceLock::new();
16 let preferred = *FORMAT.get_or_init(|| {
17 resolve_composition_format(
18 crate::debug_toggles::debug_toggle("CRANPOSE_COMPOSITION_8BIT").as_deref(),
19 cfg!(target_os = "android"),
20 )
21 });
22 if FLOAT_COMPOSITION_UNSUPPORTED.load(Ordering::Relaxed) {
23 wgpu::TextureFormat::Rgba8Unorm
24 } else {
25 preferred
26 }
27}
28
29pub(crate) fn settle_composition_format(
38 device: &wgpu::Device,
39 backend: wgpu::Backend,
40) -> wgpu::TextureFormat {
41 let format = composition_format();
42 if backend == wgpu::Backend::Gl
43 && format != wgpu::TextureFormat::Rgba8Unorm
44 && !renders_into(device, format)
45 {
46 log::warn!("[gpu-init] this device cannot draw into {format:?}; compositing in Rgba8Unorm");
47 FLOAT_COMPOSITION_UNSUPPORTED.store(true, Ordering::Relaxed);
48 }
49 composition_format()
50}
51
52pub(crate) fn renders_into(device: &wgpu::Device, format: wgpu::TextureFormat) -> bool {
54 let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
55 let _probe = create_2d_texture(
56 device,
57 format,
58 1,
59 1,
60 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
61 Some("Composition format probe"),
62 );
63 let mut error = std::pin::pin!(scope.pop());
64 match error.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
67 Poll::Ready(error) => error.is_none(),
68 Poll::Pending => true,
69 }
70}
71
72fn resolve_composition_format(requested: Option<&str>, android: bool) -> wgpu::TextureFormat {
73 let eight_bit = match requested.map(str::trim) {
74 Some("1" | "true" | "yes") => true,
75 Some("0" | "false" | "no") => false,
76 _ => android,
77 };
78 if eight_bit {
79 wgpu::TextureFormat::Rgba8Unorm
80 } else {
81 wgpu::TextureFormat::Rgba16Float
82 }
83}
84
85pub(crate) fn create_2d_texture(
86 device: &wgpu::Device,
87 format: wgpu::TextureFormat,
88 width: u32,
89 height: u32,
90 usage: wgpu::TextureUsages,
91 label: Option<&str>,
92) -> wgpu::Texture {
93 device.create_texture(&wgpu::TextureDescriptor {
94 label,
95 size: wgpu::Extent3d {
96 width,
97 height,
98 depth_or_array_layers: 1,
99 },
100 mip_level_count: 1,
101 sample_count: 1,
102 dimension: wgpu::TextureDimension::D2,
103 format,
104 usage,
105 view_formats: &[],
106 })
107}
108
109pub(crate) struct OffscreenTarget {
110 pub view: wgpu::TextureView,
111 pub width: u32,
112 pub height: u32,
113 bytes_per_pixel: u64,
114 cached_bind_group: OnceCell<wgpu::BindGroup>,
115}
116
117impl OffscreenTarget {
118 pub(crate) fn new(
119 device: &wgpu::Device,
120 format: wgpu::TextureFormat,
121 width: u32,
122 height: u32,
123 ) -> Self {
124 Self::new_labeled(device, format, width, height, "Offscreen Target")
125 }
126
127 pub(crate) fn new_labeled(
128 device: &wgpu::Device,
129 format: wgpu::TextureFormat,
130 width: u32,
131 height: u32,
132 label: &'static str,
133 ) -> Self {
134 let texture = create_2d_texture(
135 device,
136 format,
137 width,
138 height,
139 wgpu::TextureUsages::RENDER_ATTACHMENT
140 | wgpu::TextureUsages::TEXTURE_BINDING
141 | wgpu::TextureUsages::COPY_SRC
142 | wgpu::TextureUsages::COPY_DST,
143 Some(label),
144 );
145 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
146 Self {
147 view,
148 width,
149 height,
150 bytes_per_pixel: crate::frame_graph::texture_format_bytes_per_pixel(format),
151 cached_bind_group: OnceCell::new(),
152 }
153 }
154
155 pub(crate) fn texture(&self) -> &wgpu::Texture {
156 self.view.texture()
157 }
158
159 pub(crate) fn format(&self) -> wgpu::TextureFormat {
160 self.texture().format()
161 }
162
163 fn matches_size(&self, width: u32, height: u32) -> bool {
164 self.width == width && self.height == height
165 }
166
167 pub fn get_or_create_bind_group(
168 &self,
169 device: &wgpu::Device,
170 layout: &wgpu::BindGroupLayout,
171 sampler: &wgpu::Sampler,
172 ) -> &wgpu::BindGroup {
173 self.cached_bind_group.get_or_init(|| {
174 device.create_bind_group(&wgpu::BindGroupDescriptor {
175 label: Some("Offscreen Texture Bind Group (cached)"),
176 layout,
177 entries: &[
178 wgpu::BindGroupEntry {
179 binding: 0,
180 resource: wgpu::BindingResource::TextureView(&self.view),
181 },
182 wgpu::BindGroupEntry {
183 binding: 1,
184 resource: wgpu::BindingResource::Sampler(sampler),
185 },
186 ],
187 })
188 })
189 }
190
191 pub(crate) fn from_surface(texture: wgpu::Texture, view: wgpu::TextureView) -> Self {
194 let width = texture.width();
195 let height = texture.height();
196 let format = texture.format().remove_srgb_suffix();
197 Self {
198 view,
199 width,
200 height,
201 bytes_per_pixel: crate::frame_graph::texture_format_bytes_per_pixel(format),
202 cached_bind_group: OnceCell::new(),
203 }
204 }
205}
206
207pub fn composition_bytes_per_pixel() -> u64 {
209 crate::frame_graph::texture_format_bytes_per_pixel(composition_format())
210}
211
212pub(crate) struct OffscreenPool {
213 available: Vec<OffscreenTarget>,
214 format: wgpu::TextureFormat,
215 max_texture_dim: u32,
216}
217
218const MAX_POOLED_TARGETS: usize = 64;
219
220const MAX_POOLED_BYTES: u64 = 128 * 1024 * 1024;
221
222fn target_bytes(width: u32, height: u32, bytes_per_pixel: u64) -> u64 {
223 u64::from(width) * u64::from(height) * bytes_per_pixel
224}
225
226impl OffscreenPool {
227 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
228 Self {
229 available: Vec::new(),
230 format,
231 max_texture_dim: device.limits().max_texture_dimension_2d,
232 }
233 }
234
235 #[cfg(test)]
236 fn new_with_limit(format: wgpu::TextureFormat, max_texture_dim: u32) -> Self {
237 Self {
238 available: Vec::new(),
239 format,
240 max_texture_dim,
241 }
242 }
243
244 pub fn max_texture_dim(&self) -> u32 {
245 self.max_texture_dim
246 }
247
248 pub fn pool_size(&self) -> usize {
249 self.available.len()
250 }
251
252 pub fn estimated_bytes(&self) -> usize {
253 self.available
254 .iter()
255 .map(|t| {
256 (t.width as u64)
257 .saturating_mul(t.height as u64)
258 .saturating_mul(t.bytes_per_pixel) as usize
259 })
260 .sum()
261 }
262
263 pub fn acquire(
264 &mut self,
265 device: &wgpu::Device,
266 width: u32,
267 height: u32,
268 stats: Option<&FrameStats>,
269 ) -> OffscreenTarget {
270 let width = width.min(self.max_texture_dim).max(1);
271 let height = height.min(self.max_texture_dim).max(1);
272 if let Some(idx) = self
273 .available
274 .iter()
275 .position(|t| t.matches_size(width, height))
276 {
277 if let Some(s) = stats {
278 s.record_offscreen_acquire(width, height, self.format, false);
279 }
280 self.available.swap_remove(idx)
281 } else {
282 if let Some(s) = stats {
283 s.record_offscreen_acquire(width, height, self.format, true);
284 }
285 OffscreenTarget::new(device, self.format, width, height)
286 }
287 }
288
289 pub fn release(&mut self, target: OffscreenTarget) {
290 self.available.push(target);
291 while self.available.len() > MAX_POOLED_TARGETS
292 || self.pooled_bytes() > MAX_POOLED_BYTES && self.available.len() > 1
293 {
294 self.available.remove(0);
295 }
296 }
297
298 fn pooled_bytes(&self) -> u64 {
299 self.available
300 .iter()
301 .map(|t| target_bytes(t.width, t.height, self.bytes_per_pixel()))
302 .sum()
303 }
304
305 fn bytes_per_pixel(&self) -> u64 {
306 crate::frame_graph::texture_format_bytes_per_pixel(self.format)
307 }
308
309 pub fn texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
310 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
311 label: Some("Effect Texture Bind Group Layout"),
312 entries: &[
313 wgpu::BindGroupLayoutEntry {
314 binding: 0,
315 visibility: wgpu::ShaderStages::FRAGMENT,
316 ty: wgpu::BindingType::Texture {
317 sample_type: wgpu::TextureSampleType::Float { filterable: true },
318 view_dimension: wgpu::TextureViewDimension::D2,
319 multisampled: false,
320 },
321 count: None,
322 },
323 wgpu::BindGroupLayoutEntry {
324 binding: 1,
325 visibility: wgpu::ShaderStages::FRAGMENT,
326 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
327 count: None,
328 },
329 ],
330 })
331 }
332
333 pub fn uniform_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
334 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
335 label: Some("Effect Uniform Bind Group Layout"),
336 entries: &[wgpu::BindGroupLayoutEntry {
337 binding: 0,
338 visibility: wgpu::ShaderStages::FRAGMENT,
339 ty: wgpu::BindingType::Buffer {
340 ty: wgpu::BufferBindingType::Uniform,
341 has_dynamic_offset: true,
342 min_binding_size: None,
343 },
344 count: None,
345 }],
346 })
347 }
348}
349
350#[cfg(test)]
351#[path = "tests/offscreen_tests.rs"]
352mod tests;