1use std::cell::OnceCell;
2
3use crate::{gpu_stats::FrameStats, idle_pool::IdlePool};
4
5pub const COMPOSITION_BYTES_PER_PIXEL: u64 = 4;
8
9pub(crate) fn composition_format(display: wgpu::TextureFormat) -> wgpu::TextureFormat {
13 match display.remove_srgb_suffix() {
14 format @ (wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm) => format,
15 _ => wgpu::TextureFormat::Rgba8Unorm,
16 }
17}
18
19pub(crate) fn create_2d_texture(
20 device: &wgpu::Device,
21 format: wgpu::TextureFormat,
22 width: u32,
23 height: u32,
24 usage: wgpu::TextureUsages,
25 label: Option<&str>,
26) -> wgpu::Texture {
27 device.create_texture(&wgpu::TextureDescriptor {
28 label,
29 size: wgpu::Extent3d {
30 width,
31 height,
32 depth_or_array_layers: 1,
33 },
34 mip_level_count: 1,
35 sample_count: 1,
36 dimension: wgpu::TextureDimension::D2,
37 format,
38 usage,
39 view_formats: &[],
40 })
41}
42
43pub(crate) struct OffscreenTarget {
44 pub view: wgpu::TextureView,
45 pub width: u32,
46 pub height: u32,
47 bytes_per_pixel: u64,
48 cached_bind_group: OnceCell<wgpu::BindGroup>,
49}
50
51impl OffscreenTarget {
52 pub(crate) fn new(
53 device: &wgpu::Device,
54 format: wgpu::TextureFormat,
55 width: u32,
56 height: u32,
57 ) -> Self {
58 Self::new_labeled(device, format, width, height, "Offscreen Target")
59 }
60
61 pub(crate) fn new_labeled(
62 device: &wgpu::Device,
63 format: wgpu::TextureFormat,
64 width: u32,
65 height: u32,
66 label: &'static str,
67 ) -> Self {
68 let texture = create_2d_texture(
69 device,
70 format,
71 width,
72 height,
73 wgpu::TextureUsages::RENDER_ATTACHMENT
74 | wgpu::TextureUsages::TEXTURE_BINDING
75 | wgpu::TextureUsages::COPY_SRC
76 | wgpu::TextureUsages::COPY_DST,
77 Some(label),
78 );
79 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
80 Self {
81 view,
82 width,
83 height,
84 bytes_per_pixel: crate::frame_graph::texture_format_bytes_per_pixel(format),
85 cached_bind_group: OnceCell::new(),
86 }
87 }
88
89 pub(crate) fn texture(&self) -> &wgpu::Texture {
90 self.view.texture()
91 }
92
93 pub(crate) fn format(&self) -> wgpu::TextureFormat {
94 self.texture().format()
95 }
96
97 fn matches_size(&self, width: u32, height: u32) -> bool {
98 self.width == width && self.height == height
99 }
100
101 pub fn get_or_create_bind_group(
102 &self,
103 device: &wgpu::Device,
104 layout: &wgpu::BindGroupLayout,
105 sampler: &wgpu::Sampler,
106 ) -> &wgpu::BindGroup {
107 self.cached_bind_group.get_or_init(|| {
108 device.create_bind_group(&wgpu::BindGroupDescriptor {
109 label: Some("Offscreen Texture Bind Group (cached)"),
110 layout,
111 entries: &[
112 wgpu::BindGroupEntry {
113 binding: 0,
114 resource: wgpu::BindingResource::TextureView(&self.view),
115 },
116 wgpu::BindGroupEntry {
117 binding: 1,
118 resource: wgpu::BindingResource::Sampler(sampler),
119 },
120 ],
121 })
122 })
123 }
124
125 pub(crate) fn from_surface(texture: wgpu::Texture, view: wgpu::TextureView) -> Self {
128 let width = texture.width();
129 let height = texture.height();
130 let format = texture.format().remove_srgb_suffix();
131 Self {
132 view,
133 width,
134 height,
135 bytes_per_pixel: crate::frame_graph::texture_format_bytes_per_pixel(format),
136 cached_bind_group: OnceCell::new(),
137 }
138 }
139}
140
141pub(crate) struct OffscreenPool {
142 available: IdlePool<OffscreenTarget>,
143 format: wgpu::TextureFormat,
144 max_texture_dim: u32,
145}
146
147const MAX_POOLED_TARGETS: usize = 64;
148
149const MAX_IDLE_TARGETS: usize = 16;
159
160const MAX_POOLED_BYTES: u64 = 128 * 1024 * 1024;
161
162fn target_bytes(width: u32, height: u32, bytes_per_pixel: u64) -> u64 {
163 u64::from(width) * u64::from(height) * bytes_per_pixel
164}
165
166impl OffscreenPool {
167 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
168 Self {
169 available: IdlePool::default(),
170 format,
171 max_texture_dim: device.limits().max_texture_dimension_2d,
172 }
173 }
174
175 #[cfg(test)]
176 fn new_with_limit(format: wgpu::TextureFormat, max_texture_dim: u32) -> Self {
177 Self {
178 available: IdlePool::default(),
179 format,
180 max_texture_dim,
181 }
182 }
183
184 pub fn max_texture_dim(&self) -> u32 {
185 self.max_texture_dim
186 }
187
188 pub fn pool_size(&self) -> usize {
189 self.available.len()
190 }
191
192 pub fn estimated_bytes(&self) -> usize {
193 self.available
194 .iter()
195 .map(|t| {
196 (t.width as u64)
197 .saturating_mul(t.height as u64)
198 .saturating_mul(t.bytes_per_pixel) as usize
199 })
200 .sum()
201 }
202
203 pub fn acquire(
204 &mut self,
205 device: &wgpu::Device,
206 width: u32,
207 height: u32,
208 stats: Option<&FrameStats>,
209 ) -> OffscreenTarget {
210 let width = width.min(self.max_texture_dim).max(1);
211 let height = height.min(self.max_texture_dim).max(1);
212 if let Some(target) = self.available.take(|t| t.matches_size(width, height)) {
213 if let Some(s) = stats {
214 s.record_offscreen_acquire(width, height, self.format, false);
215 }
216 target
217 } else {
218 if let Some(s) = stats {
219 s.record_offscreen_acquire(width, height, self.format, true);
220 }
221 OffscreenTarget::new(device, self.format, width, height)
222 }
223 }
224
225 pub fn release(&mut self, target: OffscreenTarget) {
226 let bytes_per_pixel = self.bytes_per_pixel();
227 self.available
228 .put(target, MAX_POOLED_TARGETS, MAX_POOLED_BYTES, |t| {
229 target_bytes(t.width, t.height, bytes_per_pixel)
230 });
231 }
232
233 pub fn end_frame(&mut self) {
237 self.available.end_frame_keeping(MAX_IDLE_TARGETS);
238 }
239
240 fn bytes_per_pixel(&self) -> u64 {
241 crate::frame_graph::texture_format_bytes_per_pixel(self.format)
242 }
243
244 pub fn texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
245 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
246 label: Some("Effect Texture Bind Group Layout"),
247 entries: &[
248 wgpu::BindGroupLayoutEntry {
249 binding: 0,
250 visibility: wgpu::ShaderStages::FRAGMENT,
251 ty: wgpu::BindingType::Texture {
252 sample_type: wgpu::TextureSampleType::Float { filterable: true },
253 view_dimension: wgpu::TextureViewDimension::D2,
254 multisampled: false,
255 },
256 count: None,
257 },
258 wgpu::BindGroupLayoutEntry {
259 binding: 1,
260 visibility: wgpu::ShaderStages::FRAGMENT,
261 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
262 count: None,
263 },
264 ],
265 })
266 }
267
268 pub fn uniform_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
269 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
270 label: Some("Effect Uniform Bind Group Layout"),
271 entries: &[wgpu::BindGroupLayoutEntry {
272 binding: 0,
273 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
275 ty: wgpu::BindingType::Buffer {
276 ty: wgpu::BufferBindingType::Uniform,
277 has_dynamic_offset: true,
278 min_binding_size: None,
279 },
280 count: None,
281 }],
282 })
283 }
284}
285
286#[cfg(test)]
287#[path = "tests/offscreen_tests.rs"]
288mod tests;