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