Skip to main content

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}