use crate::error::{GpuError, Result};
pub struct RenderTarget {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub format: wgpu::TextureFormat,
pub width: u32,
pub height: u32,
pub sample_count: u32,
#[allow(dead_code)]
msaa_texture: Option<wgpu::Texture>,
pub msaa_view: Option<wgpu::TextureView>,
pub depth: Option<crate::depth::DepthTexture>,
}
impl RenderTarget {
pub fn new(
device: &wgpu::Device,
width: u32,
height: u32,
format: wgpu::TextureFormat,
) -> Self {
let (width, height) = if width == 0 || height == 0 {
tracing::warn!(
width,
height,
"zero-size render target requested, clamping to 1x1"
);
(width.max(1), height.max(1))
} else {
(width, height)
};
tracing::debug!(width, height, ?format, "creating render target");
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("render_target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Self {
texture,
view,
format,
width,
height,
sample_count: 1,
msaa_texture: None,
msaa_view: None,
depth: None,
}
}
pub fn matching_surface(
device: &wgpu::Device,
width: u32,
height: u32,
surface_format: wgpu::TextureFormat,
) -> Self {
Self::new(device, width, height, surface_format)
}
#[must_use]
#[inline]
pub fn render_view(&self) -> &wgpu::TextureView {
self.msaa_view.as_ref().unwrap_or(&self.view)
}
#[must_use]
#[inline]
pub fn resolve_target(&self) -> Option<&wgpu::TextureView> {
if self.sample_count > 1 {
Some(&self.view)
} else {
None
}
}
#[must_use]
#[inline]
pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
self.depth.as_ref().map(|d| &d.view)
}
pub fn read_pixels(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Result<Vec<u8>> {
tracing::debug!(self.width, self.height, ?self.format, "reading render target pixels");
let bytes_per_row = 4u32.checked_mul(self.width).ok_or_else(|| {
tracing::error!(width = self.width, "render target bytes_per_row overflow");
GpuError::Buffer("bytes_per_row overflow".into())
})?;
let padded_bytes_per_row = (bytes_per_row + 255) & !255;
let buffer_size = u64::from(padded_bytes_per_row.checked_mul(self.height).ok_or_else(
|| {
tracing::error!(
width = self.width,
height = self.height,
"render target buffer size overflow"
);
GpuError::Buffer("buffer size overflow".into())
},
)?);
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("render_target_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("readback_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &staging,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(self.height),
},
},
wgpu::Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let buffer_slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
let _ = device.poll(wgpu::PollType::Wait {
timeout: None,
submission_index: None,
});
rx.recv()
.map_err(|e| {
tracing::error!("render target readback channel error: {e}");
let _ = e;
GpuError::ReadbackChannel
})?
.map_err(|e| {
tracing::error!("render target readback map failed: {e}");
GpuError::ReadbackMap(e)
})?;
let data = buffer_slice.get_mapped_range();
let pixel_count = 4u64 * u64::from(self.width) * u64::from(self.height);
let mut pixels = Vec::with_capacity(pixel_count as usize);
for row in 0..self.height {
let start = (u64::from(row) * u64::from(padded_bytes_per_row)) as usize;
let end = start + (4 * self.width) as usize;
pixels.extend_from_slice(&data[start..end]);
}
drop(data);
staging.unmap();
Ok(pixels)
}
}
pub struct RenderTargetBuilder<'a> {
device: &'a wgpu::Device,
width: u32,
height: u32,
format: wgpu::TextureFormat,
sample_count: u32,
depth_format: Option<wgpu::TextureFormat>,
}
impl<'a> RenderTargetBuilder<'a> {
#[must_use]
pub fn new(device: &'a wgpu::Device, width: u32, height: u32) -> Self {
Self {
device,
width,
height,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
sample_count: 1,
depth_format: None,
}
}
#[must_use]
pub fn format(mut self, format: wgpu::TextureFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub fn msaa(mut self, sample_count: u32) -> Self {
self.sample_count = sample_count;
self
}
#[must_use]
pub fn depth(mut self, depth_format: wgpu::TextureFormat) -> Self {
self.depth_format = Some(depth_format);
self
}
pub fn build(self) -> RenderTarget {
let (width, height) = (self.width.max(1), self.height.max(1));
tracing::debug!(
width,
height,
?self.format,
self.sample_count,
depth = self.depth_format.is_some(),
"creating render target (builder)"
);
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("render_target_resolve"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let (msaa_texture, msaa_view) = if self.sample_count > 1 {
let msaa_tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("render_target_msaa"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.sample_count,
dimension: wgpu::TextureDimension::D2,
format: self.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let msaa_v = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
(Some(msaa_tex), Some(msaa_v))
} else {
(None, None)
};
let depth = self
.depth_format
.map(|fmt| crate::depth::DepthTexture::new(self.device, width, height, fmt));
RenderTarget {
texture,
view,
format: self.format,
width,
height,
sample_count: self.sample_count,
msaa_texture,
msaa_view,
depth,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_target_size() {
let _size = std::mem::size_of::<RenderTarget>();
}
#[test]
fn render_target_no_msaa() {
assert_eq!(1u32, 1); }
#[test]
fn builder_defaults() {
assert_eq!(
wgpu::TextureFormat::Rgba8UnormSrgb,
wgpu::TextureFormat::Rgba8UnormSrgb
);
}
fn try_gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
let ctx = pollster::block_on(crate::context::GpuContext::new()).ok()?;
Some((ctx.device, ctx.queue))
}
#[test]
fn gpu_render_target_new() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let target = RenderTarget::new(&device, 256, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
assert_eq!(target.width, 256);
assert_eq!(target.height, 256);
assert_eq!(target.sample_count, 1);
assert!(target.msaa_view.is_none());
assert!(target.depth.is_none());
}
#[test]
fn gpu_render_target_zero_size_clamp() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let target = RenderTarget::new(&device, 0, 0, wgpu::TextureFormat::Rgba8UnormSrgb);
assert_eq!(target.width, 1);
assert_eq!(target.height, 1);
}
#[test]
fn gpu_render_target_no_msaa_views() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let target = RenderTarget::new(&device, 64, 64, wgpu::TextureFormat::Rgba8UnormSrgb);
let _rv = target.render_view();
assert!(target.resolve_target().is_none());
assert!(target.depth_view().is_none());
}
#[test]
fn gpu_render_target_matching_surface() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let target =
RenderTarget::matching_surface(&device, 800, 600, wgpu::TextureFormat::Bgra8UnormSrgb);
assert_eq!(target.format, wgpu::TextureFormat::Bgra8UnormSrgb);
}
#[test]
fn gpu_render_target_read_pixels() {
let Some((device, queue)) = try_gpu() else {
return;
};
let target = RenderTarget::new(&device, 2, 2, wgpu::TextureFormat::Rgba8UnormSrgb);
let pixels = target.read_pixels(&device, &queue).unwrap();
assert_eq!(pixels.len(), 2 * 2 * 4); }
#[test]
fn gpu_builder_basic() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let target = RenderTargetBuilder::new(&device, 128, 128).build();
assert_eq!(target.width, 128);
assert_eq!(target.format, wgpu::TextureFormat::Rgba8UnormSrgb);
assert_eq!(target.sample_count, 1);
}
#[test]
fn gpu_builder_with_depth() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let target = RenderTargetBuilder::new(&device, 128, 128)
.depth(crate::depth::DepthTexture::DEFAULT_FORMAT)
.build();
assert!(target.depth.is_some());
assert!(target.depth_view().is_some());
}
#[test]
fn gpu_builder_with_msaa() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let target = RenderTargetBuilder::new(&device, 128, 128).msaa(4).build();
assert_eq!(target.sample_count, 4);
assert!(target.msaa_view.is_some());
assert!(target.resolve_target().is_some());
}
#[test]
fn gpu_builder_with_format() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let target = RenderTargetBuilder::new(&device, 64, 64)
.format(wgpu::TextureFormat::Rgba16Float)
.build();
assert_eq!(target.format, wgpu::TextureFormat::Rgba16Float);
}
}