1use oxiui_core::UiError;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
31pub enum SurfaceColorFormat {
32 #[default]
34 Rgba8Unorm,
35 Rgba8UnormSrgb,
38 Bgra8Unorm,
40 Bgra8UnormSrgb,
42 Rgb10a2Unorm,
45 Rgba16Float,
47}
48
49impl SurfaceColorFormat {
50 pub fn wgpu_format(self) -> wgpu::TextureFormat {
52 match self {
53 Self::Rgba8Unorm => wgpu::TextureFormat::Rgba8Unorm,
54 Self::Rgba8UnormSrgb => wgpu::TextureFormat::Rgba8UnormSrgb,
55 Self::Bgra8Unorm => wgpu::TextureFormat::Bgra8Unorm,
56 Self::Bgra8UnormSrgb => wgpu::TextureFormat::Bgra8UnormSrgb,
57 Self::Rgb10a2Unorm => wgpu::TextureFormat::Rgb10a2Unorm,
58 Self::Rgba16Float => wgpu::TextureFormat::Rgba16Float,
59 }
60 }
61
62 pub fn is_hdr(self) -> bool {
65 matches!(self, Self::Rgba16Float)
66 }
67
68 pub fn bits_per_channel(self) -> u32 {
70 match self {
71 Self::Rgba8Unorm | Self::Rgba8UnormSrgb | Self::Bgra8Unorm | Self::Bgra8UnormSrgb => 8,
72 Self::Rgb10a2Unorm => 10,
73 Self::Rgba16Float => 16,
74 }
75 }
76
77 pub fn expects_linear_light(self) -> bool {
84 matches!(self, Self::Rgba16Float)
85 }
86}
87
88pub fn select_surface_format(
97 supported_formats: &[wgpu::TextureFormat],
98 prefer_hdr: bool,
99) -> SurfaceColorFormat {
100 let mapped: Vec<SurfaceColorFormat> = supported_formats
102 .iter()
103 .filter_map(|&f| match f {
104 wgpu::TextureFormat::Rgba8Unorm => Some(SurfaceColorFormat::Rgba8Unorm),
105 wgpu::TextureFormat::Rgba8UnormSrgb => Some(SurfaceColorFormat::Rgba8UnormSrgb),
106 wgpu::TextureFormat::Bgra8Unorm => Some(SurfaceColorFormat::Bgra8Unorm),
107 wgpu::TextureFormat::Bgra8UnormSrgb => Some(SurfaceColorFormat::Bgra8UnormSrgb),
108 wgpu::TextureFormat::Rgb10a2Unorm => Some(SurfaceColorFormat::Rgb10a2Unorm),
109 wgpu::TextureFormat::Rgba16Float => Some(SurfaceColorFormat::Rgba16Float),
110 _ => None,
111 })
112 .collect();
113
114 if mapped.is_empty() {
115 return SurfaceColorFormat::default();
116 }
117
118 if prefer_hdr {
119 for candidate in &[
121 SurfaceColorFormat::Rgba16Float,
122 SurfaceColorFormat::Rgb10a2Unorm,
123 ] {
124 if mapped.contains(candidate) {
125 return *candidate;
126 }
127 }
128 }
129
130 for candidate in &[
132 SurfaceColorFormat::Bgra8UnormSrgb,
133 SurfaceColorFormat::Rgba8UnormSrgb,
134 SurfaceColorFormat::Bgra8Unorm,
135 SurfaceColorFormat::Rgba8Unorm,
136 ] {
137 if mapped.contains(candidate) {
138 return *candidate;
139 }
140 }
141
142 mapped[0]
143}
144
145pub struct HdrGpuContext {
158 pub device: wgpu::Device,
160 pub queue: wgpu::Queue,
162 pub color_texture: wgpu::Texture,
164 pub color_view: wgpu::TextureView,
166 pub width: u32,
168 pub height: u32,
170}
171
172pub const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
174
175impl HdrGpuContext {
176 pub fn headless(width: u32, height: u32) -> Result<Self, UiError> {
185 if width == 0 || height == 0 {
186 return Err(UiError::Unsupported(
187 "HdrGpuContext dimensions must be non-zero".to_string(),
188 ));
189 }
190
191 let instance = wgpu::Instance::default();
192 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
193 power_preference: wgpu::PowerPreference::default(),
194 force_fallback_adapter: false,
195 compatible_surface: None,
196 }))
197 .map_err(|e| UiError::Unsupported(format!("no GPU adapter: {e}")))?;
198
199 let fmt_features = adapter.get_texture_format_features(HDR_FORMAT);
201 if !fmt_features
202 .allowed_usages
203 .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
204 {
205 return Err(UiError::Unsupported(
206 "adapter does not support Rgba16Float as a render attachment".to_string(),
207 ));
208 }
209
210 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
211 label: Some("oxiui-render-wgpu hdr device"),
212 required_features: wgpu::Features::empty(),
213 required_limits: wgpu::Limits::downlevel_defaults(),
214 memory_hints: wgpu::MemoryHints::Performance,
215 experimental_features: wgpu::ExperimentalFeatures::disabled(),
216 trace: wgpu::Trace::Off,
217 }))
218 .map_err(|e| UiError::Backend(format!("HDR GPU device request failed: {e}")))?;
219
220 let color_texture = device.create_texture(&wgpu::TextureDescriptor {
221 label: Some("oxiui-render-wgpu hdr target"),
222 size: wgpu::Extent3d {
223 width,
224 height,
225 depth_or_array_layers: 1,
226 },
227 mip_level_count: 1,
228 sample_count: 1,
229 dimension: wgpu::TextureDimension::D2,
230 format: HDR_FORMAT,
231 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
232 view_formats: &[],
233 });
234 let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
235
236 Ok(Self {
237 device,
238 queue,
239 color_texture,
240 color_view,
241 width,
242 height,
243 })
244 }
245
246 pub fn readback_f16(&self) -> Result<Vec<u8>, UiError> {
256 let bytes_per_pixel = 8u32;
258 let unpadded_bytes_per_row = self.width * bytes_per_pixel;
259 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
260 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
261 let buffer_size = (padded_bytes_per_row * self.height) as wgpu::BufferAddress;
262
263 let readback = self.device.create_buffer(&wgpu::BufferDescriptor {
264 label: Some("oxiui-render-wgpu hdr readback"),
265 size: buffer_size,
266 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
267 mapped_at_creation: false,
268 });
269
270 let mut encoder = self
271 .device
272 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
273 label: Some("oxiui-render-wgpu hdr readback encoder"),
274 });
275
276 encoder.copy_texture_to_buffer(
277 wgpu::TexelCopyTextureInfo {
278 texture: &self.color_texture,
279 mip_level: 0,
280 origin: wgpu::Origin3d::ZERO,
281 aspect: wgpu::TextureAspect::All,
282 },
283 wgpu::TexelCopyBufferInfo {
284 buffer: &readback,
285 layout: wgpu::TexelCopyBufferLayout {
286 offset: 0,
287 bytes_per_row: Some(padded_bytes_per_row),
288 rows_per_image: Some(self.height),
289 },
290 },
291 wgpu::Extent3d {
292 width: self.width,
293 height: self.height,
294 depth_or_array_layers: 1,
295 },
296 );
297
298 self.queue.submit(Some(encoder.finish()));
299
300 let slice = readback.slice(..);
301 slice.map_async(wgpu::MapMode::Read, |_| {});
302 self.device
303 .poll(wgpu::PollType::wait_indefinitely())
304 .map_err(|e| UiError::Render(format!("HdrGpuContext GPU poll failed: {e:?}")))?;
305
306 let data = slice.get_mapped_range();
307 let mut out = Vec::with_capacity((unpadded_bytes_per_row * self.height) as usize);
308 for row in 0..self.height {
309 let start = (row * padded_bytes_per_row) as usize;
310 let end = start + unpadded_bytes_per_row as usize;
311 out.extend_from_slice(&data[start..end]);
312 }
313 drop(data);
314 readback.unmap();
315 Ok(out)
316 }
317}
318
319#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn surface_color_format_is_hdr() {
327 assert!(SurfaceColorFormat::Rgba16Float.is_hdr());
328 assert!(!SurfaceColorFormat::Rgba8Unorm.is_hdr());
329 assert!(!SurfaceColorFormat::Bgra8UnormSrgb.is_hdr());
330 }
331
332 #[test]
333 fn surface_color_format_bits_per_channel() {
334 assert_eq!(SurfaceColorFormat::Rgba8Unorm.bits_per_channel(), 8);
335 assert_eq!(SurfaceColorFormat::Rgb10a2Unorm.bits_per_channel(), 10);
336 assert_eq!(SurfaceColorFormat::Rgba16Float.bits_per_channel(), 16);
337 }
338
339 #[test]
340 fn surface_color_format_expects_linear() {
341 assert!(SurfaceColorFormat::Rgba16Float.expects_linear_light());
342 assert!(!SurfaceColorFormat::Rgba8Unorm.expects_linear_light());
343 assert!(!SurfaceColorFormat::Rgba8UnormSrgb.expects_linear_light());
344 }
345
346 #[test]
347 fn select_surface_format_prefers_hdr_when_available() {
348 let fmts = &[
349 wgpu::TextureFormat::Bgra8UnormSrgb,
350 wgpu::TextureFormat::Rgba16Float,
351 ];
352 let chosen = select_surface_format(fmts, true);
353 assert_eq!(chosen, SurfaceColorFormat::Rgba16Float);
354 }
355
356 #[test]
357 fn select_surface_format_falls_back_to_srgb_when_no_hdr() {
358 let fmts = &[
359 wgpu::TextureFormat::Rgba8Unorm,
360 wgpu::TextureFormat::Bgra8UnormSrgb,
361 ];
362 let chosen = select_surface_format(fmts, true); assert_eq!(chosen, SurfaceColorFormat::Bgra8UnormSrgb);
365 }
366
367 #[test]
368 fn select_surface_format_prefers_srgb_without_hdr_preference() {
369 let fmts = &[
370 wgpu::TextureFormat::Rgba8Unorm,
371 wgpu::TextureFormat::Bgra8UnormSrgb,
372 wgpu::TextureFormat::Rgba16Float,
373 ];
374 let chosen = select_surface_format(fmts, false);
375 assert_eq!(chosen, SurfaceColorFormat::Bgra8UnormSrgb);
377 }
378
379 #[test]
380 fn select_surface_format_empty_list_returns_default() {
381 let chosen = select_surface_format(&[], false);
382 assert_eq!(chosen, SurfaceColorFormat::default());
383 }
384
385 #[test]
386 fn hdr_gpu_context_creates_or_skips() {
387 match HdrGpuContext::headless(32, 32) {
389 Ok(ctx) => {
390 assert_eq!(ctx.width, 32);
391 assert_eq!(ctx.height, 32);
392 }
393 Err(e @ oxiui_core::UiError::Unsupported(_)) => {
394 println!("skip: HDR not supported: {e}");
395 }
396 Err(e) => {
397 panic!("unexpected error creating HdrGpuContext: {e}");
398 }
399 }
400 }
401
402 #[test]
403 fn hdr_format_is_rgba16float() {
404 assert_eq!(HDR_FORMAT, wgpu::TextureFormat::Rgba16Float);
405 }
406}