Files
resolved/crates/resolved/src/gpu/targets.rs
T
2026-10-16 16:31:53 -04:00

357 lines
15 KiB
Rust

//! The per-frame images passes render into, and reading them back.
use super::GpuContext;
/// Channel radiance, one channel per component. Four channels fit in one target; f32
/// because solar radiance overflows f16. An f16 target scaled by the exposure was
/// measured at most 0.14 ms faster on the RTX 3060, off by up to 2 DN at 12 bits and
/// 16 at 16 bits, and too small for the Sun in a 10 ms exposure.
pub const RADIANCE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
/// Reversed-Z depth, near / z: cleared to 0 (infinitely far), nearer is greater.
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
/// Depth test for reversed Z.
pub const DEPTH_COMPARE: wgpu::CompareFunction = wgpu::CompareFunction::Greater;
/// The sizes of one frame's images, for [`FrameTargets::new`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TargetLayout {
/// Image width in pixels.
pub width: u32,
/// Image height in pixels.
pub height: u32,
/// Number of camera channels.
pub channels: u32,
/// Mesh samples per rendered pixel: 1, 2 or 4.
pub samples: u32,
/// Rendered pixels along each axis per image pixel.
pub scale: u32,
/// Rendered pixels past the image on each side, for the blur.
pub margin: u32,
/// With lens distortion, the size of the undistorted render that the focal plane is
/// read from; `None` without, where the render is the focal plane.
pub undistorted: Option<[u32; 2]>,
/// Whether stars are drawn, which needs a buffer of their electrons.
pub stars: bool,
/// Whether the detector blooms, which needs a buffer of the charge before readout.
pub charge: bool,
}
impl TargetLayout {
/// A `width` x `height` image of `channels` channels, rendered one pixel per pixel
/// with one mesh sample, no margin and no distortion.
pub fn new(width: u32, height: u32, channels: u32) -> Self {
Self {
width,
height,
channels,
samples: 1,
scale: 1,
margin: 0,
undistorted: None,
stars: false,
charge: false,
}
}
/// The focal plane's size in rendered pixels: the image at `scale`, with the margin.
pub fn focal_plane(&self) -> [u32; 2] {
[self.width, self.height].map(|n| n * self.scale + 2 * self.margin)
}
/// The size the meshes and bodies render at.
pub fn rendered(&self) -> [u32; 2] {
self.undistorted.unwrap_or_else(|| self.focal_plane())
}
}
/// The images one frame renders into, sized for one camera.
///
/// Target meshes are drawn first, into their own radiance and depth with
/// [`FrameTargets::samples`] samples per pixel. The bodies pass then draws Earth, the
/// Moon and the Sun and combines them with the meshes' samples into one value per
/// pixel, in `radiance` and `depth`. These are rendered [`FrameTargets::scale`] times
/// finer than the image along each axis, and with a blur they extend
/// [`FrameTargets::margin`] of those finer pixels past the image on every side, so that
/// light from just outside the frame can blur into it. With lens distortion they are
/// rendered undistorted, on the area [`TargetLayout::undistorted`] gives. The blur
/// passes then read the focal plane from them, through the distortion if any, blur it
/// and average it into the image's pixels, in `blurred`. The sensor pass reads
/// [`FrameTargets::sensed`].
#[derive(Debug)]
pub struct FrameTargets {
/// Image width in pixels.
pub width: u32,
/// Image height in pixels.
pub height: u32,
/// Rendered pixels along each axis per image pixel: 1, or more to render finer.
pub scale: u32,
/// Rendered pixels past the image on each side, for the blur; 0 without one.
pub margin: u32,
/// Samples per rendered pixel of the mesh targets: 1, or 2 or 4 to cover meshes at
/// several points.
pub samples: u32,
/// The meshes' channel radiance, [`RADIANCE_FORMAT`], `samples` per pixel. This and
/// the next three are the size [`TargetLayout::rendered`] gives.
pub mesh_radiance: wgpu::Texture,
/// The meshes' depth, [`DEPTH_FORMAT`], `samples` per pixel.
pub mesh_depth: wgpu::Texture,
/// Channel radiance of the whole scene, one value per pixel, [`RADIANCE_FORMAT`].
pub radiance: wgpu::Texture,
/// Depth of the nearest surface at each pixel, [`DEPTH_FORMAT`]: the body at the
/// pixel's center or the nearest mesh sample in front of it.
pub depth: wgpu::Texture,
/// With a blur, a `scale` above 1 or distortion: the focal plane blurred and
/// averaged along rows, leaving the rows rendered, then along columns into the
/// image, [`RADIANCE_FORMAT`].
pub blurred: Option<[wgpu::Texture; 2]>,
/// Number of camera channels.
pub channels: u32,
/// With stars: their electrons per pixel and channel, channels interleaved per pixel,
/// in fixed point, 1/64 of an electron per unit, which the sensor pass adds.
pub stars: Option<wgpu::Buffer>,
/// With blooming: the electrons each pixel and channel holds before readout, f32,
/// channels interleaved per pixel; then a u32 flag per column and channel, set where
/// it holds charge past the full well.
pub charge: Option<[wgpu::Buffer; 2]>,
/// The sensor pass's output: u16 DN, channels interleaved per pixel, rows from the
/// top, packed two to a u32 word. Mappable where [`FrameTargets::dn_mappable`].
pub dn: wgpu::Buffer,
}
impl FrameTargets {
/// Allocates the images `layout` describes. The blur's images are allocated with a
/// margin, a scale above 1 or distortion.
pub fn new(ctx: &GpuContext, layout: &TargetLayout) -> Self {
let TargetLayout {
width,
height,
channels,
samples,
scale,
margin,
..
} = *layout;
let [outer_width, outer_height] = layout.rendered();
let focal = layout.focal_plane();
let sized = |label, format, usage, sample_count, width, height| {
ctx.device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count,
dimension: wgpu::TextureDimension::D2,
format,
usage,
view_formats: &[],
})
};
let texture = |label, format, usage, samples| {
sized(label, format, usage, samples, outer_width, outer_height)
};
let usage = wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC;
let values = width as u64 * height as u64 * channels as u64;
// Multisampled textures cannot be copied out.
let mesh_usage = wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| if samples == 1 {
wgpu::TextureUsages::COPY_SRC
} else {
wgpu::TextureUsages::empty()
};
let storage = |label, width, height| {
sized(
label,
RADIANCE_FORMAT,
wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
1,
width,
height,
)
};
Self {
width,
height,
scale,
margin,
samples,
mesh_radiance: texture("mesh radiance", RADIANCE_FORMAT, mesh_usage, samples),
mesh_depth: texture("mesh depth", DEPTH_FORMAT, mesh_usage, samples),
// Writable from the CPU so tests can supply radiance directly.
radiance: texture(
"radiance",
RADIANCE_FORMAT,
usage | wgpu::TextureUsages::COPY_DST,
1,
),
depth: texture("depth", DEPTH_FORMAT, usage, 1),
blurred: (margin > 0 || scale > 1 || layout.undistorted.is_some()).then(|| {
[
storage("blurred rows", width, focal[1]),
storage("blurred", width, height),
]
}),
channels,
stars: layout.stars.then(|| {
ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("star electrons"),
size: values * 4,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
})
}),
charge: layout.charge.then(|| {
let lines = layout.width as u64 * channels as u64;
[("charge", values * 4), ("overfull lines", lines * 4)].map(|(label, size)| {
ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
})
}),
dn: ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("dn"),
size: values.div_ceil(2) * 4,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| if Self::dn_mappable(ctx) {
wgpu::BufferUsages::MAP_READ
} else {
wgpu::BufferUsages::empty()
},
mapped_at_creation: false,
}),
}
}
/// Whether the DN buffer can be mapped and read where the sensor pass writes it,
/// rather than copied to a staging buffer first: on devices with
/// `MAPPABLE_PRIMARY_BUFFERS` (both of mainframe's). `RendererOptions::dn_in_place`
/// chooses whether to.
///
/// wgpu documents this as a win only where CPU and GPU share memory and a possible
/// loss elsewhere. Measured at 1920 x 1080: lavapipe frames 1.9 ms faster;
/// on the RTX 3060 the sensor pass goes from 0.135 to 0.475 ms writing across PCIe,
/// but that replaces a 0.53 ms copy, and frames are 0.15 ms faster.
pub fn dn_mappable(ctx: &GpuContext) -> bool {
ctx.device
.features()
.contains(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS)
}
/// The radiance image the sensor pass reads, `width` x `height`: the blurred image,
/// or without one, `radiance`.
pub fn sensed(&self) -> &wgpu::Texture {
match &self.blurred {
Some([_, image]) => image,
None => &self.radiance,
}
}
/// Number of u16 values in the DN image.
pub fn dn_len(&self) -> usize {
self.width as usize * self.height as usize * self.channels as usize
}
/// Reads the DN image back. Slow, like [`FrameTargets::read_radiance`]; the per-frame
/// readback is the renderer's.
pub fn read_dn(&self, ctx: &GpuContext) -> Vec<u16> {
let staging = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("dn readback"),
size: self.dn.size(),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = ctx.device.create_command_encoder(&Default::default());
encoder.copy_buffer_to_buffer(&self.dn, 0, &staging, 0, self.dn.size());
ctx.queue.submit([encoder.finish()]);
staging
.slice(..)
.map_async(wgpu::MapMode::Read, |r| r.expect("readback map"));
ctx.device
.poll(wgpu::PollType::wait_indefinitely())
.expect("readback poll");
let view = staging
.slice(..)
.get_mapped_range()
.expect("readback range");
bytemuck::cast_slice::<u8, u16>(&view)[..self.dn_len()].to_vec()
}
/// Reads the radiance image the sensor sees ([`FrameTargets::sensed`]) back, row by
/// row from the top. Slow; for tests and debugging, not the per-frame path.
pub fn read_radiance(&self, ctx: &GpuContext) -> Vec<[f32; 4]> {
read_texture(ctx, self.sensed(), wgpu::TextureAspect::All, 16)
}
/// Reads the depth target back, as rendered (at `scale`, with the margin), row by
/// row from the top. Slow, like [`FrameTargets::read_radiance`].
pub fn read_depth(&self, ctx: &GpuContext) -> Vec<f32> {
read_texture(ctx, &self.depth, wgpu::TextureAspect::DepthOnly, 4)
}
}
/// Copies a whole texture into a buffer, waits, and unpacks it into texels of
/// `texel_bytes` each.
fn read_texture<T: bytemuck::Pod>(
ctx: &GpuContext,
texture: &wgpu::Texture,
aspect: wgpu::TextureAspect,
texel_bytes: u32,
) -> Vec<T> {
let (width, height) = (texture.width(), texture.height());
let row = width * texel_bytes;
// Buffer rows must start on 256-byte boundaries.
let padded = row.next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
let buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("readback"),
size: padded as u64 * height as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = ctx.device.create_command_encoder(&Default::default());
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(height),
},
},
texture.size(),
);
ctx.queue.submit([encoder.finish()]);
buffer
.slice(..)
.map_async(wgpu::MapMode::Read, |r| r.expect("readback map"));
ctx.device
.poll(wgpu::PollType::wait_indefinitely())
.expect("readback poll");
let view = buffer.slice(..).get_mapped_range().expect("readback range");
view.chunks(padded as usize)
.flat_map(|r| {
bytemuck::cast_slice::<u8, T>(&r[..row as usize])
.iter()
.copied()
})
.collect()
}