1use oxiui_core::UiError;
22
23use crate::gpu::device::TARGET_FORMAT;
24
25pub struct RenderTarget {
32 pub color_texture: wgpu::Texture,
34 pub color_view: wgpu::TextureView,
36 pub msaa_view: Option<wgpu::TextureView>,
38 pub width: u32,
40 pub height: u32,
42 pub sample_count: u32,
44 dirty: bool,
46}
47
48impl RenderTarget {
49 pub fn new(
59 device: &wgpu::Device,
60 width: u32,
61 height: u32,
62 sample_count: u32,
63 ) -> Result<Self, UiError> {
64 if width == 0 || height == 0 {
65 return Err(UiError::Unsupported(
66 "RenderTarget dimensions must be non-zero".to_string(),
67 ));
68 }
69 let sc = sample_count.max(1);
70
71 let color_texture = device.create_texture(&wgpu::TextureDescriptor {
74 label: Some("oxiui-render-wgpu render-target backing"),
75 size: wgpu::Extent3d {
76 width,
77 height,
78 depth_or_array_layers: 1,
79 },
80 mip_level_count: 1,
81 sample_count: 1,
82 dimension: wgpu::TextureDimension::D2,
83 format: TARGET_FORMAT,
84 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
85 | wgpu::TextureUsages::COPY_SRC
86 | wgpu::TextureUsages::TEXTURE_BINDING,
87 view_formats: &[],
88 });
89 let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
90
91 let msaa_view = if sc > 1 {
94 let msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
95 label: Some("oxiui-render-wgpu render-target msaa"),
96 size: wgpu::Extent3d {
97 width,
98 height,
99 depth_or_array_layers: 1,
100 },
101 mip_level_count: 1,
102 sample_count: sc,
103 dimension: wgpu::TextureDimension::D2,
104 format: TARGET_FORMAT,
105 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
106 view_formats: &[],
107 });
108 Some(msaa_texture.create_view(&wgpu::TextureViewDescriptor::default()))
109 } else {
110 None
111 };
112
113 Ok(Self {
114 color_texture,
115 color_view,
116 msaa_view,
117 width,
118 height,
119 sample_count: sc,
120 dirty: true,
121 })
122 }
123
124 pub fn new_simple(device: &wgpu::Device, width: u32, height: u32) -> Result<Self, UiError> {
128 Self::new(device, width, height, 1)
129 }
130
131 pub fn color_attachment(&self) -> (&wgpu::TextureView, Option<&wgpu::TextureView>) {
138 match &self.msaa_view {
139 Some(msaa) => (msaa, Some(&self.color_view)),
140 None => (&self.color_view, None),
141 }
142 }
143
144 pub fn texture_view(&self) -> &wgpu::TextureView {
149 &self.color_view
150 }
151
152 pub fn mark_dirty(&mut self) {
154 self.dirty = true;
155 }
156
157 pub fn mark_clean(&mut self) {
159 self.dirty = false;
160 }
161
162 pub fn is_dirty(&self) -> bool {
164 self.dirty
165 }
166
167 pub fn readback_rgba(
176 &self,
177 device: &wgpu::Device,
178 queue: &wgpu::Queue,
179 ) -> Result<Vec<u8>, UiError> {
180 let unpadded_bytes_per_row = self.width * 4;
181 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
182 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
183 let buffer_size = (padded_bytes_per_row * self.height) as wgpu::BufferAddress;
184
185 let readback = device.create_buffer(&wgpu::BufferDescriptor {
186 label: Some("oxiui-render-wgpu render-target readback"),
187 size: buffer_size,
188 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
189 mapped_at_creation: false,
190 });
191
192 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
193 label: Some("oxiui-render-wgpu render-target readback encoder"),
194 });
195
196 encoder.copy_texture_to_buffer(
197 wgpu::TexelCopyTextureInfo {
198 texture: &self.color_texture,
199 mip_level: 0,
200 origin: wgpu::Origin3d::ZERO,
201 aspect: wgpu::TextureAspect::All,
202 },
203 wgpu::TexelCopyBufferInfo {
204 buffer: &readback,
205 layout: wgpu::TexelCopyBufferLayout {
206 offset: 0,
207 bytes_per_row: Some(padded_bytes_per_row),
208 rows_per_image: Some(self.height),
209 },
210 },
211 wgpu::Extent3d {
212 width: self.width,
213 height: self.height,
214 depth_or_array_layers: 1,
215 },
216 );
217
218 queue.submit(Some(encoder.finish()));
219
220 let slice = readback.slice(..);
221 slice.map_async(wgpu::MapMode::Read, |_| {});
222 device
223 .poll(wgpu::PollType::wait_indefinitely())
224 .map_err(|e| UiError::Render(format!("RenderTarget GPU poll failed: {e:?}")))?;
225
226 let data = slice.get_mapped_range();
227 let mut out = Vec::with_capacity((unpadded_bytes_per_row * self.height) as usize);
228 for row in 0..self.height {
229 let start = (row * padded_bytes_per_row) as usize;
230 let end = start + unpadded_bytes_per_row as usize;
231 out.extend_from_slice(&data[start..end]);
232 }
233 drop(data);
234 readback.unmap();
235 Ok(out)
236 }
237
238 pub fn resize(
248 &mut self,
249 device: &wgpu::Device,
250 new_width: u32,
251 new_height: u32,
252 ) -> Result<(), UiError> {
253 *self = Self::new(device, new_width, new_height, self.sample_count)?;
254 Ok(())
255 }
256}
257
258#[cfg(test)]
261mod tests {
262 use super::*;
263 use oxiui_core::UiError;
264
265 fn try_device() -> Option<(wgpu::Device, wgpu::Queue)> {
266 let instance = wgpu::Instance::default();
267 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
268 power_preference: wgpu::PowerPreference::default(),
269 force_fallback_adapter: false,
270 compatible_surface: None,
271 }))
272 .ok()?;
273 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
274 label: Some("test device"),
275 required_features: wgpu::Features::empty(),
276 required_limits: wgpu::Limits::downlevel_defaults(),
277 memory_hints: wgpu::MemoryHints::Performance,
278 experimental_features: wgpu::ExperimentalFeatures::disabled(),
279 trace: wgpu::Trace::Off,
280 }))
281 .ok()
282 }
283
284 #[test]
285 fn render_target_zero_dimensions_fail() {
286 let Some((device, _queue)) = try_device() else {
287 return;
288 };
289 assert!(matches!(
290 RenderTarget::new(&device, 0, 64, 1),
291 Err(UiError::Unsupported(_))
292 ));
293 assert!(matches!(
294 RenderTarget::new(&device, 64, 0, 1),
295 Err(UiError::Unsupported(_))
296 ));
297 }
298
299 #[test]
300 fn render_target_creates_and_is_dirty() {
301 let Some((device, _queue)) = try_device() else {
302 return;
303 };
304 let rt = RenderTarget::new_simple(&device, 64, 32).expect("create render target");
305 assert_eq!(rt.width, 64);
306 assert_eq!(rt.height, 32);
307 assert_eq!(rt.sample_count, 1);
308 assert!(rt.is_dirty(), "fresh target must be dirty");
309 }
310
311 #[test]
312 fn render_target_dirty_flag_management() {
313 let Some((device, _queue)) = try_device() else {
314 return;
315 };
316 let mut rt = RenderTarget::new_simple(&device, 32, 32).expect("create");
317 assert!(rt.is_dirty());
318 rt.mark_clean();
319 assert!(!rt.is_dirty());
320 rt.mark_dirty();
321 assert!(rt.is_dirty());
322 }
323
324 #[test]
325 fn render_target_resize_resets_dirty() {
326 let Some((device, _queue)) = try_device() else {
327 return;
328 };
329 let mut rt = RenderTarget::new_simple(&device, 32, 32).expect("create");
330 rt.mark_clean();
331 assert!(!rt.is_dirty());
332 rt.resize(&device, 64, 64).expect("resize");
333 assert_eq!(rt.width, 64);
334 assert_eq!(rt.height, 64);
335 assert!(rt.is_dirty(), "resized target must be dirty");
337 }
338
339 #[test]
340 fn render_target_readback_all_transparent() {
341 let Some((device, queue)) = try_device() else {
346 return;
347 };
348 let rt = RenderTarget::new_simple(&device, 16, 16).expect("create");
349 let buf = rt.readback_rgba(&device, &queue).expect("readback");
350 assert_eq!(
351 buf.len(),
352 (16 * 16 * 4) as usize,
353 "readback buffer must be tightly packed"
354 );
355 }
356}