357 lines
15 KiB
Rust
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()
|
|
}
|