use oxiui_core::UiError;
use crate::gpu::device::TARGET_FORMAT;
pub struct RenderTarget {
pub color_texture: wgpu::Texture,
pub color_view: wgpu::TextureView,
pub msaa_view: Option<wgpu::TextureView>,
pub width: u32,
pub height: u32,
pub sample_count: u32,
dirty: bool,
}
impl RenderTarget {
pub fn new(
device: &wgpu::Device,
width: u32,
height: u32,
sample_count: u32,
) -> Result<Self, UiError> {
if width == 0 || height == 0 {
return Err(UiError::Unsupported(
"RenderTarget dimensions must be non-zero".to_string(),
));
}
let sc = sample_count.max(1);
let color_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("oxiui-render-wgpu render-target backing"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TARGET_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
let msaa_view = if sc > 1 {
let msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("oxiui-render-wgpu render-target msaa"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: sc,
dimension: wgpu::TextureDimension::D2,
format: TARGET_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
Some(msaa_texture.create_view(&wgpu::TextureViewDescriptor::default()))
} else {
None
};
Ok(Self {
color_texture,
color_view,
msaa_view,
width,
height,
sample_count: sc,
dirty: true,
})
}
pub fn new_simple(device: &wgpu::Device, width: u32, height: u32) -> Result<Self, UiError> {
Self::new(device, width, height, 1)
}
pub fn color_attachment(&self) -> (&wgpu::TextureView, Option<&wgpu::TextureView>) {
match &self.msaa_view {
Some(msaa) => (msaa, Some(&self.color_view)),
None => (&self.color_view, None),
}
}
pub fn texture_view(&self) -> &wgpu::TextureView {
&self.color_view
}
pub fn mark_dirty(&mut self) {
self.dirty = true;
}
pub fn mark_clean(&mut self) {
self.dirty = false;
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn readback_rgba(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> Result<Vec<u8>, UiError> {
let unpadded_bytes_per_row = self.width * 4;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
let buffer_size = (padded_bytes_per_row * self.height) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("oxiui-render-wgpu 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("oxiui-render-wgpu render-target readback encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &self.color_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
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(Some(encoder.finish()));
let slice = readback.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
device
.poll(wgpu::PollType::wait_indefinitely())
.map_err(|e| UiError::Render(format!("RenderTarget GPU poll failed: {e:?}")))?;
let data = slice.get_mapped_range();
let mut out = Vec::with_capacity((unpadded_bytes_per_row * self.height) as usize);
for row in 0..self.height {
let start = (row * padded_bytes_per_row) as usize;
let end = start + unpadded_bytes_per_row as usize;
out.extend_from_slice(&data[start..end]);
}
drop(data);
readback.unmap();
Ok(out)
}
pub fn resize(
&mut self,
device: &wgpu::Device,
new_width: u32,
new_height: u32,
) -> Result<(), UiError> {
*self = Self::new(device, new_width, new_height, self.sample_count)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxiui_core::UiError;
fn try_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::default();
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("test device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
}))
.ok()
}
#[test]
fn render_target_zero_dimensions_fail() {
let Some((device, _queue)) = try_device() else {
return;
};
assert!(matches!(
RenderTarget::new(&device, 0, 64, 1),
Err(UiError::Unsupported(_))
));
assert!(matches!(
RenderTarget::new(&device, 64, 0, 1),
Err(UiError::Unsupported(_))
));
}
#[test]
fn render_target_creates_and_is_dirty() {
let Some((device, _queue)) = try_device() else {
return;
};
let rt = RenderTarget::new_simple(&device, 64, 32).expect("create render target");
assert_eq!(rt.width, 64);
assert_eq!(rt.height, 32);
assert_eq!(rt.sample_count, 1);
assert!(rt.is_dirty(), "fresh target must be dirty");
}
#[test]
fn render_target_dirty_flag_management() {
let Some((device, _queue)) = try_device() else {
return;
};
let mut rt = RenderTarget::new_simple(&device, 32, 32).expect("create");
assert!(rt.is_dirty());
rt.mark_clean();
assert!(!rt.is_dirty());
rt.mark_dirty();
assert!(rt.is_dirty());
}
#[test]
fn render_target_resize_resets_dirty() {
let Some((device, _queue)) = try_device() else {
return;
};
let mut rt = RenderTarget::new_simple(&device, 32, 32).expect("create");
rt.mark_clean();
assert!(!rt.is_dirty());
rt.resize(&device, 64, 64).expect("resize");
assert_eq!(rt.width, 64);
assert_eq!(rt.height, 64);
assert!(rt.is_dirty(), "resized target must be dirty");
}
#[test]
fn render_target_readback_all_transparent() {
let Some((device, queue)) = try_device() else {
return;
};
let rt = RenderTarget::new_simple(&device, 16, 16).expect("create");
let buf = rt.readback_rgba(&device, &queue).expect("readback");
assert_eq!(
buf.len(),
(16 * 16 * 4) as usize,
"readback buffer must be tightly packed"
);
}
}