use oxiui_core::UiError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum SurfaceColorFormat {
#[default]
Rgba8Unorm,
Rgba8UnormSrgb,
Bgra8Unorm,
Bgra8UnormSrgb,
Rgb10a2Unorm,
Rgba16Float,
}
impl SurfaceColorFormat {
pub fn wgpu_format(self) -> wgpu::TextureFormat {
match self {
Self::Rgba8Unorm => wgpu::TextureFormat::Rgba8Unorm,
Self::Rgba8UnormSrgb => wgpu::TextureFormat::Rgba8UnormSrgb,
Self::Bgra8Unorm => wgpu::TextureFormat::Bgra8Unorm,
Self::Bgra8UnormSrgb => wgpu::TextureFormat::Bgra8UnormSrgb,
Self::Rgb10a2Unorm => wgpu::TextureFormat::Rgb10a2Unorm,
Self::Rgba16Float => wgpu::TextureFormat::Rgba16Float,
}
}
pub fn is_hdr(self) -> bool {
matches!(self, Self::Rgba16Float)
}
pub fn bits_per_channel(self) -> u32 {
match self {
Self::Rgba8Unorm | Self::Rgba8UnormSrgb | Self::Bgra8Unorm | Self::Bgra8UnormSrgb => 8,
Self::Rgb10a2Unorm => 10,
Self::Rgba16Float => 16,
}
}
pub fn expects_linear_light(self) -> bool {
matches!(self, Self::Rgba16Float)
}
}
pub fn select_surface_format(
supported_formats: &[wgpu::TextureFormat],
prefer_hdr: bool,
) -> SurfaceColorFormat {
let mapped: Vec<SurfaceColorFormat> = supported_formats
.iter()
.filter_map(|&f| match f {
wgpu::TextureFormat::Rgba8Unorm => Some(SurfaceColorFormat::Rgba8Unorm),
wgpu::TextureFormat::Rgba8UnormSrgb => Some(SurfaceColorFormat::Rgba8UnormSrgb),
wgpu::TextureFormat::Bgra8Unorm => Some(SurfaceColorFormat::Bgra8Unorm),
wgpu::TextureFormat::Bgra8UnormSrgb => Some(SurfaceColorFormat::Bgra8UnormSrgb),
wgpu::TextureFormat::Rgb10a2Unorm => Some(SurfaceColorFormat::Rgb10a2Unorm),
wgpu::TextureFormat::Rgba16Float => Some(SurfaceColorFormat::Rgba16Float),
_ => None,
})
.collect();
if mapped.is_empty() {
return SurfaceColorFormat::default();
}
if prefer_hdr {
for candidate in &[
SurfaceColorFormat::Rgba16Float,
SurfaceColorFormat::Rgb10a2Unorm,
] {
if mapped.contains(candidate) {
return *candidate;
}
}
}
for candidate in &[
SurfaceColorFormat::Bgra8UnormSrgb,
SurfaceColorFormat::Rgba8UnormSrgb,
SurfaceColorFormat::Bgra8Unorm,
SurfaceColorFormat::Rgba8Unorm,
] {
if mapped.contains(candidate) {
return *candidate;
}
}
mapped[0]
}
pub struct HdrGpuContext {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub color_texture: wgpu::Texture,
pub color_view: wgpu::TextureView,
pub width: u32,
pub height: u32,
}
pub const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
impl HdrGpuContext {
pub fn headless(width: u32, height: u32) -> Result<Self, UiError> {
if width == 0 || height == 0 {
return Err(UiError::Unsupported(
"HdrGpuContext dimensions must be non-zero".to_string(),
));
}
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,
}))
.map_err(|e| UiError::Unsupported(format!("no GPU adapter: {e}")))?;
let fmt_features = adapter.get_texture_format_features(HDR_FORMAT);
if !fmt_features
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
{
return Err(UiError::Unsupported(
"adapter does not support Rgba16Float as a render attachment".to_string(),
));
}
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("oxiui-render-wgpu hdr 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,
}))
.map_err(|e| UiError::Backend(format!("HDR GPU device request failed: {e}")))?;
let color_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("oxiui-render-wgpu hdr target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: HDR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
Ok(Self {
device,
queue,
color_texture,
color_view,
width,
height,
})
}
pub fn readback_f16(&self) -> Result<Vec<u8>, UiError> {
let bytes_per_pixel = 8u32;
let unpadded_bytes_per_row = self.width * bytes_per_pixel;
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 = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("oxiui-render-wgpu hdr readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("oxiui-render-wgpu hdr 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,
},
);
self.queue.submit(Some(encoder.finish()));
let slice = readback.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
self.device
.poll(wgpu::PollType::wait_indefinitely())
.map_err(|e| UiError::Render(format!("HdrGpuContext 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)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn surface_color_format_is_hdr() {
assert!(SurfaceColorFormat::Rgba16Float.is_hdr());
assert!(!SurfaceColorFormat::Rgba8Unorm.is_hdr());
assert!(!SurfaceColorFormat::Bgra8UnormSrgb.is_hdr());
}
#[test]
fn surface_color_format_bits_per_channel() {
assert_eq!(SurfaceColorFormat::Rgba8Unorm.bits_per_channel(), 8);
assert_eq!(SurfaceColorFormat::Rgb10a2Unorm.bits_per_channel(), 10);
assert_eq!(SurfaceColorFormat::Rgba16Float.bits_per_channel(), 16);
}
#[test]
fn surface_color_format_expects_linear() {
assert!(SurfaceColorFormat::Rgba16Float.expects_linear_light());
assert!(!SurfaceColorFormat::Rgba8Unorm.expects_linear_light());
assert!(!SurfaceColorFormat::Rgba8UnormSrgb.expects_linear_light());
}
#[test]
fn select_surface_format_prefers_hdr_when_available() {
let fmts = &[
wgpu::TextureFormat::Bgra8UnormSrgb,
wgpu::TextureFormat::Rgba16Float,
];
let chosen = select_surface_format(fmts, true);
assert_eq!(chosen, SurfaceColorFormat::Rgba16Float);
}
#[test]
fn select_surface_format_falls_back_to_srgb_when_no_hdr() {
let fmts = &[
wgpu::TextureFormat::Rgba8Unorm,
wgpu::TextureFormat::Bgra8UnormSrgb,
];
let chosen = select_surface_format(fmts, true); assert_eq!(chosen, SurfaceColorFormat::Bgra8UnormSrgb);
}
#[test]
fn select_surface_format_prefers_srgb_without_hdr_preference() {
let fmts = &[
wgpu::TextureFormat::Rgba8Unorm,
wgpu::TextureFormat::Bgra8UnormSrgb,
wgpu::TextureFormat::Rgba16Float,
];
let chosen = select_surface_format(fmts, false);
assert_eq!(chosen, SurfaceColorFormat::Bgra8UnormSrgb);
}
#[test]
fn select_surface_format_empty_list_returns_default() {
let chosen = select_surface_format(&[], false);
assert_eq!(chosen, SurfaceColorFormat::default());
}
#[test]
fn hdr_gpu_context_creates_or_skips() {
match HdrGpuContext::headless(32, 32) {
Ok(ctx) => {
assert_eq!(ctx.width, 32);
assert_eq!(ctx.height, 32);
}
Err(e @ oxiui_core::UiError::Unsupported(_)) => {
println!("skip: HDR not supported: {e}");
}
Err(e) => {
panic!("unexpected error creating HdrGpuContext: {e}");
}
}
}
#[test]
fn hdr_format_is_rgba16float() {
assert_eq!(HDR_FORMAT, wgpu::TextureFormat::Rgba16Float);
}
}