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)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn android_composites_in_eight_bits_and_the_rest_in_float() {
356 assert_eq!(
357 resolve_composition_format(None, true),
358 wgpu::TextureFormat::Rgba8Unorm
359 );
360 assert_eq!(
361 resolve_composition_format(None, false),
362 wgpu::TextureFormat::Rgba16Float
363 );
364 }
365
366 #[test]
367 fn the_override_wins_in_both_directions_on_any_platform() {
368 assert_eq!(
369 resolve_composition_format(Some("0"), true),
370 wgpu::TextureFormat::Rgba16Float
371 );
372 assert_eq!(
373 resolve_composition_format(Some(" yes "), false),
374 wgpu::TextureFormat::Rgba8Unorm
375 );
376 }
377
378 #[test]
379 fn an_unparsable_override_falls_back_to_the_platform_default() {
380 assert_eq!(
381 resolve_composition_format(Some("half"), true),
382 wgpu::TextureFormat::Rgba8Unorm
383 );
384 assert_eq!(
385 resolve_composition_format(Some(""), false),
386 wgpu::TextureFormat::Rgba16Float
387 );
388 }
389
390 #[test]
391 fn a_device_is_asked_whether_it_can_draw_into_a_format() {
392 let (_lock, device, _queue) = crate::frame_graph::upload_test_device();
393 assert!(renders_into(&device, wgpu::TextureFormat::Rgba8Unorm));
394 assert!(
395 !renders_into(&device, wgpu::TextureFormat::Rgb9e5Ufloat),
396 "no device draws into a shared-exponent format"
397 );
398 }
399
400 #[test]
401 fn a_device_that_draws_into_the_float_format_keeps_it() {
402 let (_lock, device, _queue) = crate::frame_graph::upload_test_device();
403 let backend = device.adapter_info().backend;
404 let preferred = composition_format();
405 assert_eq!(settle_composition_format(&device, backend), preferred);
406 }
407
408 #[test]
409 fn a_frame_worth_of_small_surfaces_stays_pooled() {
410 let bytes: u64 = (0..20)
411 .map(|_| target_bytes(132, 132, composition_bytes_per_pixel()))
412 .sum();
413 assert!(
414 bytes < MAX_POOLED_BYTES,
415 "twenty control surfaces must fit the pool budget"
416 );
417 const _: () = assert!(20 < MAX_POOLED_TARGETS);
418 }
419
420 #[test]
421 fn the_byte_budget_bounds_full_screen_float_surfaces() {
422 let pool = OffscreenPool::new_with_limit(wgpu::TextureFormat::Rgba16Float, 4096);
423 let full_screen = target_bytes(1080, 2244, pool.bytes_per_pixel());
424 let held = MAX_POOLED_BYTES / full_screen;
425 assert!(
426 (2..=8).contains(&held),
427 "the budget should hold a few full-screen surfaces, not dozens: {held}"
428 );
429 }
430
431 #[test]
432 fn pool_starts_empty() {
433 let pool = OffscreenPool::new_with_limit(wgpu::TextureFormat::Bgra8Unorm, 8192);
434 assert!(pool.available.is_empty());
435 assert_eq!(pool.pool_size(), 0);
436 }
437
438 #[test]
439 fn max_texture_dimension_stored() {
440 let pool = OffscreenPool::new_with_limit(wgpu::TextureFormat::Bgra8Unorm, 2048);
441 assert_eq!(pool.max_texture_dim, 2048);
442
443 let pool = OffscreenPool::new_with_limit(wgpu::TextureFormat::Bgra8Unorm, 4096);
444 assert_eq!(pool.max_texture_dim, 4096);
445 }
446}