Skip to main content

cranpose_render_wgpu/
offscreen.rs

1use std::cell::OnceCell;
2
3use crate::{gpu_stats::FrameStats, idle_pool::IdlePool};
4
5/// Bytes one pixel of a composition target holds: every renderer composites
6/// in an 8-bit format.
7pub const COMPOSITION_BYTES_PER_PIXEL: u64 = 4;
8
9/// The format a renderer composites in: the presentable image's own 8-bit
10/// format, so a frame draws straight into the image it presents, or
11/// `Rgba8Unorm` beside any other display format.
12pub(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    /// Wraps a swapchain image as the frame's root target so the scene
126    /// renders into it directly, with no composition copy behind it.
127    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
149/// The targets the pool keeps once the frame after the one that handed them
150/// back has passed. A frame's targets serve the next frame's requests of
151/// their sizes however many there are; older ones serve a later return to
152/// a size. In the gauntlet at tier 12 on a Mate 20 X, the first frame kept
153/// 96 layer surfaces and the cache gave back 90 of them within ten frames:
154/// the pool held 64 of them, 4 MiB, and the next 90 frames took none back.
155/// Most later reuses took one of the 16 most recently pooled targets. The
156/// GL memory Android reports for the process stays at its peak, so a target
157/// no frame takes back costs memory for the rest of the run.
158const 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    /// Ends a frame, keeping every target this frame handed back and
234    /// [`MAX_IDLE_TARGETS`] of the older ones, and dropping the targets no
235    /// frame has reused for [`crate::idle_pool::IDLE_FRAMES`].
236    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                // A runtime shader's vertex stage reads its quad placement.
274                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;