gpu_handle_types/texture_bytes.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Canonical on-device byte estimator for `wgpu::Texture` resources — the
4// single source of truth for:
5// * budget reservations (e.g. an import cache's), and
6// * a frame pool's out-of-memory `required_bytes` reporting and
7// transient / metrics accounting.
8//
9// It handles every shape axis (mip chain, array layers / depth slices,
10// sample count) and both multi-planar (NV12 / P010) and block-compressed
11// formats. The estimate is honest enough to drive coarse budget pressure
12// and Tier-2 reclaim accounting; flat-bpp fallbacks would over-estimate
13// NV12 by ~2.6× and P010 by ~1.3×, leaving the budget thinking it freed
14// twice what it really did.
15
16/// Per-pixel byte cost summed across every plane of `format` at
17/// `(width, height)`.
18///
19/// Multi-planar YCbCr formats follow the 4:2:0 convention (chroma plane
20/// is half-width × half-height, rounded up for odd extents — wgpu
21/// requires even dims for NV12 / P010, but odd ones are still handled). Every other
22/// format is `block_count × block_copy_size`, which collapses to
23/// `bpp × w × h` for the uncompressed single-cell formats and stays
24/// correct for block-compressed layouts. Formats without a single linear
25/// cell size (`Depth24Plus`, combined depth-stencil) get a 4-bpp upper
26/// bound — an over-estimate, but those rarely ride the budget and the
27/// bound stays conservative.
28fn plane_bytes_2d(format: wgpu::TextureFormat, w: u64, h: u64) -> u64 {
29 match format {
30 // NV12: plane 0 = R8 (1 bpp); plane 1 = Rg8 at half-res (2 bpp).
31 wgpu::TextureFormat::NV12 => w * h + w.div_ceil(2) * h.div_ceil(2) * 2,
32 // P010: plane 0 = R16 (2 bpp); plane 1 = Rg16 at half-res (4 bpp).
33 wgpu::TextureFormat::P010 => w * h * 2 + w.div_ceil(2) * h.div_ceil(2) * 4,
34 other => {
35 let (bw, bh) = other.block_dimensions();
36 let bs = other.block_copy_size(None).unwrap_or(4) as u64;
37 w.div_ceil(bw as u64) * h.div_ceil(bh as u64) * bs
38 }
39 }
40}
41
42/// Estimate the on-device byte footprint of a 2D / 3D texture.
43///
44/// Charges every mip level and every array layer / depth slice. Mip
45/// dimensions follow the wgpu convention (`max(1, dim >> level)`). Sample
46/// count multiplies the base level only (mip levels above 0 of an MSAA
47/// texture are not allocated by any current driver — sample expansion on
48/// level > 0 is a validation error in wgpu and in the native APIs beneath
49/// it).
50pub fn estimate_texture_bytes(
51 format: wgpu::TextureFormat,
52 width: u32,
53 height: u32,
54 depth_or_array_layers: u32,
55 sample_count: u32,
56 mip_level_count: u32,
57) -> u64 {
58 let layers = depth_or_array_layers.max(1) as u64;
59 let samples = sample_count.max(1) as u64;
60 let levels = mip_level_count.max(1);
61
62 let mut total: u64 = 0;
63 for level in 0..levels {
64 let w = (width >> level).max(1) as u64;
65 let h = (height >> level).max(1) as u64;
66 let plane = plane_bytes_2d(format, w, h);
67 let level_bytes = plane.saturating_mul(layers);
68 // Sample count multiplies level 0 only — see fn doc.
69 let level_bytes = if level == 0 { level_bytes.saturating_mul(samples) } else { level_bytes };
70 total = total.saturating_add(level_bytes);
71 }
72 total
73}
74
75/// Convenience wrapper around [`estimate_texture_bytes`] for callers that
76/// already hold a `wgpu::TextureDescriptor`.
77pub fn estimate_descriptor_bytes(d: &wgpu::TextureDescriptor<'_>) -> u64 {
78 estimate_texture_bytes(
79 d.format,
80 d.size.width,
81 d.size.height,
82 d.size.depth_or_array_layers,
83 d.sample_count,
84 d.mip_level_count,
85 )
86}