1use crate::gpu_stats::FrameStats;
8use std::cell::OnceCell;
9
10pub(crate) struct OffscreenTarget {
12 texture: wgpu::Texture,
14 pub view: wgpu::TextureView,
15 pub width: u32,
16 pub height: u32,
17 cached_bind_group: OnceCell<wgpu::BindGroup>,
20}
21
22impl OffscreenTarget {
23 pub(crate) fn new(
24 device: &wgpu::Device,
25 format: wgpu::TextureFormat,
26 width: u32,
27 height: u32,
28 ) -> Self {
29 Self::new_labeled(device, format, width, height, "Offscreen Target")
30 }
31
32 pub(crate) fn new_labeled(
33 device: &wgpu::Device,
34 format: wgpu::TextureFormat,
35 width: u32,
36 height: u32,
37 label: &'static str,
38 ) -> Self {
39 let texture = device.create_texture(&wgpu::TextureDescriptor {
40 label: Some(label),
41 size: wgpu::Extent3d {
42 width,
43 height,
44 depth_or_array_layers: 1,
45 },
46 mip_level_count: 1,
47 sample_count: 1,
48 dimension: wgpu::TextureDimension::D2,
49 format,
50 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
51 | wgpu::TextureUsages::TEXTURE_BINDING
52 | wgpu::TextureUsages::COPY_SRC
53 | wgpu::TextureUsages::COPY_DST,
54 view_formats: &[],
55 });
56 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
57 Self {
58 texture,
59 view,
60 width,
61 height,
62 cached_bind_group: OnceCell::new(),
63 }
64 }
65
66 fn matches_size(&self, width: u32, height: u32) -> bool {
71 self.width == width && self.height == height
72 }
73
74 pub fn get_or_create_bind_group(
80 &self,
81 device: &wgpu::Device,
82 layout: &wgpu::BindGroupLayout,
83 sampler: &wgpu::Sampler,
84 ) -> &wgpu::BindGroup {
85 self.cached_bind_group.get_or_init(|| {
86 device.create_bind_group(&wgpu::BindGroupDescriptor {
87 label: Some("Offscreen Texture Bind Group (cached)"),
88 layout,
89 entries: &[
90 wgpu::BindGroupEntry {
91 binding: 0,
92 resource: wgpu::BindingResource::TextureView(&self.view),
93 },
94 wgpu::BindGroupEntry {
95 binding: 1,
96 resource: wgpu::BindingResource::Sampler(sampler),
97 },
98 ],
99 })
100 })
101 }
102
103 pub(crate) fn texture(&self) -> &wgpu::Texture {
104 &self.texture
105 }
106
107 pub(crate) fn from_readable_texture(
108 texture: &wgpu::Texture,
109 view: &wgpu::TextureView,
110 ) -> Option<Self> {
111 if !texture_supports_backdrop_reads(texture.usage()) {
112 return None;
113 }
114 Some(Self {
115 texture: texture.clone(),
116 view: view.clone(),
117 width: texture.width(),
118 height: texture.height(),
119 cached_bind_group: OnceCell::new(),
120 })
121 }
122}
123
124pub(crate) fn texture_supports_backdrop_reads(usage: wgpu::TextureUsages) -> bool {
125 usage.contains(wgpu::TextureUsages::COPY_SRC)
126 && usage.contains(wgpu::TextureUsages::TEXTURE_BINDING)
127}
128
129pub(crate) fn capture_root_target_reads() -> bool {
130 cranpose_core::env_flag!("CRANPOSE_CAPTURE_ROOT_TARGET_READS")
131}
132
133pub fn display_surface_usages(supported: wgpu::TextureUsages) -> wgpu::TextureUsages {
134 let mut usages = wgpu::TextureUsages::RENDER_ATTACHMENT;
135 for read in [
136 wgpu::TextureUsages::COPY_SRC,
137 wgpu::TextureUsages::TEXTURE_BINDING,
138 ] {
139 if supported.contains(read) {
140 usages |= read;
141 }
142 }
143 usages
144}
145
146pub(crate) struct OffscreenPool {
152 available: Vec<OffscreenTarget>,
153 format: wgpu::TextureFormat,
154 max_texture_dim: u32,
155}
156
157const MAX_POOLED_TARGETS: usize = 64;
160
161const MAX_POOLED_BYTES: u64 = 64 * 1024 * 1024;
170
171fn target_bytes(width: u32, height: u32) -> u64 {
172 u64::from(width) * u64::from(height) * 4
173}
174
175impl OffscreenPool {
176 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
177 Self {
178 available: Vec::new(),
179 format,
180 max_texture_dim: device.limits().max_texture_dimension_2d,
181 }
182 }
183
184 #[cfg(test)]
185 fn new_with_limit(format: wgpu::TextureFormat, max_texture_dim: u32) -> Self {
186 Self {
187 available: Vec::new(),
188 format,
189 max_texture_dim,
190 }
191 }
192
193 pub fn max_texture_dim(&self) -> u32 {
195 self.max_texture_dim
196 }
197
198 pub fn pool_size(&self) -> usize {
200 self.available.len()
201 }
202
203 pub fn estimated_bytes(&self) -> usize {
205 self.available
206 .iter()
207 .map(|t| (t.width as usize) * (t.height as usize) * 4)
208 .sum()
209 }
210
211 pub fn acquire(
216 &mut self,
217 device: &wgpu::Device,
218 width: u32,
219 height: u32,
220 stats: Option<&FrameStats>,
221 ) -> OffscreenTarget {
222 let width = width.min(self.max_texture_dim).max(1);
223 let height = height.min(self.max_texture_dim).max(1);
224 if let Some(idx) = self
225 .available
226 .iter()
227 .position(|t| t.matches_size(width, height))
228 {
229 if let Some(s) = stats {
230 s.record_offscreen_acquire(width, height, false);
231 }
232 self.available.swap_remove(idx)
233 } else {
234 if let Some(s) = stats {
235 s.record_offscreen_acquire(width, height, true);
236 }
237 OffscreenTarget::new(device, self.format, width, height)
238 }
239 }
240
241 pub fn release(&mut self, target: OffscreenTarget) {
247 self.available.push(target);
248 while self.available.len() > MAX_POOLED_TARGETS
249 || self.pooled_bytes() > MAX_POOLED_BYTES && self.available.len() > 1
250 {
251 self.available.remove(0);
252 }
253 }
254
255 fn pooled_bytes(&self) -> u64 {
256 self.available
257 .iter()
258 .map(|t| target_bytes(t.width, t.height))
259 .sum()
260 }
261
262 pub fn texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
267 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
268 label: Some("Effect Texture Bind Group Layout"),
269 entries: &[
270 wgpu::BindGroupLayoutEntry {
271 binding: 0,
272 visibility: wgpu::ShaderStages::FRAGMENT,
273 ty: wgpu::BindingType::Texture {
274 sample_type: wgpu::TextureSampleType::Float { filterable: true },
275 view_dimension: wgpu::TextureViewDimension::D2,
276 multisampled: false,
277 },
278 count: None,
279 },
280 wgpu::BindGroupLayoutEntry {
281 binding: 1,
282 visibility: wgpu::ShaderStages::FRAGMENT,
283 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
284 count: None,
285 },
286 ],
287 })
288 }
289
290 pub fn uniform_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
294 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
295 label: Some("Effect Uniform Bind Group Layout"),
296 entries: &[wgpu::BindGroupLayoutEntry {
297 binding: 0,
298 visibility: wgpu::ShaderStages::FRAGMENT,
299 ty: wgpu::BindingType::Buffer {
300 ty: wgpu::BufferBindingType::Uniform,
301 has_dynamic_offset: false,
302 min_binding_size: None,
303 },
304 count: None,
305 }],
306 })
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
318 fn a_frame_worth_of_small_surfaces_stays_pooled() {
319 let bytes: u64 = (0..20).map(|_| target_bytes(132, 132)).sum();
320 assert!(
321 bytes < MAX_POOLED_BYTES,
322 "twenty control surfaces must fit the pool budget"
323 );
324 const _: () = assert!(20 < MAX_POOLED_TARGETS);
327 }
328
329 #[test]
330 fn the_budget_bounds_full_screen_surfaces() {
331 let full_screen = target_bytes(1080, 2244);
332 let held = MAX_POOLED_BYTES / full_screen;
333 assert!(
334 (2..=8).contains(&held),
335 "the budget should hold a few full-screen surfaces, not dozens: {held}"
336 );
337 }
338
339 #[test]
340 fn a_surface_asks_for_the_reads_the_adapter_offers() {
341 let all = wgpu::TextureUsages::RENDER_ATTACHMENT
342 | wgpu::TextureUsages::COPY_SRC
343 | wgpu::TextureUsages::TEXTURE_BINDING
344 | wgpu::TextureUsages::COPY_DST;
345 let asked = display_surface_usages(all);
346 assert!(asked.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
347 assert!(asked.contains(wgpu::TextureUsages::COPY_SRC));
348 assert!(asked.contains(wgpu::TextureUsages::TEXTURE_BINDING));
349 assert!(!asked.contains(wgpu::TextureUsages::COPY_DST));
350 assert!(texture_supports_backdrop_reads(asked));
351 }
352
353 #[test]
354 fn a_surface_that_offers_no_read_keeps_the_attachment_alone() {
355 let asked = display_surface_usages(wgpu::TextureUsages::RENDER_ATTACHMENT);
356 assert_eq!(asked, wgpu::TextureUsages::RENDER_ATTACHMENT);
357 assert!(!texture_supports_backdrop_reads(asked));
358 }
359
360 #[test]
361 fn one_read_alone_is_not_enough_for_a_backdrop() {
362 let copy_only = display_surface_usages(
363 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
364 );
365 assert!(copy_only.contains(wgpu::TextureUsages::COPY_SRC));
366 assert!(!texture_supports_backdrop_reads(copy_only));
367 let sample_only = display_surface_usages(
368 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
369 );
370 assert!(!texture_supports_backdrop_reads(sample_only));
371 }
372
373 #[test]
374 fn pool_starts_empty() {
375 let pool = OffscreenPool::new_with_limit(wgpu::TextureFormat::Bgra8Unorm, 8192);
376 assert!(pool.available.is_empty());
377 assert_eq!(pool.pool_size(), 0);
378 }
379
380 #[test]
381 fn max_texture_dimension_stored() {
382 let pool = OffscreenPool::new_with_limit(wgpu::TextureFormat::Bgra8Unorm, 2048);
383 assert_eq!(pool.max_texture_dim, 2048);
384
385 let pool = OffscreenPool::new_with_limit(wgpu::TextureFormat::Bgra8Unorm, 4096);
386 assert_eq!(pool.max_texture_dim, 4096);
387 }
388}