use alloc::borrow::ToOwned;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec::Vec;
use core::cell::RefCell;
use core::fmt;
use num_traits::ToPrimitive;
use crate::codec::{self, DecodedRgba};
use waterui_core::layout::{ProposalSize, Size, StretchAxis, ViewDimensions};
use waterui_core::{Binding, Environment, SignalExt, View};
use waterui_graphics::{
GpuContext, GpuFrame, GpuRuntime, GpuSurface, GpuView, OffscreenRenderConfig,
OffscreenRenderError, OffscreenRenderOutput, OffscreenRenderOutputHdr, TextureRowLayout,
upload_texture,
};
use waterui_layout::frame::Frame;
pub use crate::codec::DecodePath;
#[derive(Debug)]
pub struct Image {
renderer: ImageRenderer,
width: u32,
height: u32,
resizable: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SourcePixelFormat {
Rgba8UnormSrgb,
Rgba16Float,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Interpolation {
#[default]
Linear,
Nearest,
}
impl Interpolation {
const fn to_wgpu_filter(self) -> wgpu::FilterMode {
match self {
Self::Linear => wgpu::FilterMode::Linear,
Self::Nearest => wgpu::FilterMode::Nearest,
}
}
const fn to_wgpu_mipmap_filter(self) -> wgpu::MipmapFilterMode {
match self {
Self::Linear => wgpu::MipmapFilterMode::Linear,
Self::Nearest => wgpu::MipmapFilterMode::Nearest,
}
}
}
impl Image {
#[must_use]
pub fn new(pixels: Vec<u8>, width: u32, height: u32) -> Self {
assert_eq!(
pixels.len(),
(width * height * 4) as usize,
"Pixel data length must be width * height * 4"
);
Self {
resizable: false,
renderer: ImageRenderer::new(
pixels,
width,
height,
SourcePixelFormat::Rgba8UnormSrgb,
false,
false,
),
width,
height,
}
}
#[must_use]
pub fn new_rgba16f(pixels: Vec<u8>, width: u32, height: u32) -> Self {
Self::new_rgba16f_with_metadata(pixels, width, height, true, false)
}
#[must_use]
fn new_rgba16f_with_metadata(
pixels: Vec<u8>,
width: u32,
height: u32,
source_is_hdr: bool,
source_is_wide_gamut: bool,
) -> Self {
assert_eq!(
pixels.len(),
(width * height * 8) as usize,
"Pixel data length must be width * height * 8 for RGBA16F"
);
Self {
resizable: false,
renderer: ImageRenderer::new(
pixels,
width,
height,
SourcePixelFormat::Rgba16Float,
source_is_hdr,
source_is_wide_gamut,
),
width,
height,
}
}
#[must_use]
pub const fn interpolation(mut self, mode: Interpolation) -> Self {
self.renderer.interpolation = mode;
self
}
#[must_use]
pub const fn resizable(mut self) -> Self {
self.resizable = true;
self
}
#[must_use]
pub const fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
#[must_use]
pub const fn width(&self) -> u32 {
self.width
}
#[must_use]
pub const fn height(&self) -> u32 {
self.height
}
pub fn from_encoded(data: &[u8]) -> Result<Self, String> {
codec::decode_to_rgba8(data).map(Self::from_decoded)
}
pub fn from_encoded_with_path(data: &[u8]) -> Result<(Self, DecodePath), String> {
codec::decode_to_rgba8_with_path(data)
.map(|(decoded, path)| (Self::from_decoded(decoded), path))
}
#[must_use]
pub fn stream_decoder(content_type: Option<&str>) -> ImageStreamDecoder {
ImageStreamDecoder::new(content_type)
}
#[expect(
clippy::future_not_send,
reason = "image rendering awaits the UI-local offscreen GpuView environment"
)]
pub async fn render_offscreen(
self,
runtime: &GpuRuntime,
config: OffscreenRenderConfig,
env: &mut waterui_core::Environment,
) -> Result<OffscreenRenderOutput, OffscreenRenderError> {
GpuSurface::new(self.renderer)
.render_offscreen(runtime, config, env)
.await
}
#[expect(
clippy::future_not_send,
reason = "image rendering awaits the UI-local offscreen GpuView environment"
)]
pub async fn render_offscreen_hdr(
self,
runtime: &GpuRuntime,
config: OffscreenRenderConfig,
env: &mut waterui_core::Environment,
) -> Result<OffscreenRenderOutputHdr, OffscreenRenderError> {
GpuSurface::new(self.renderer)
.render_offscreen_hdr(runtime, config, env)
.await
}
fn from_decoded(decoded: DecodedRgba) -> Self {
match decoded.pixel_format {
waterkit_codec::DecodedPixelFormat::Rgba8UnormSrgb => {
Self::new(decoded.pixels, decoded.width, decoded.height)
}
waterkit_codec::DecodedPixelFormat::Rgba16Float => Self::new_rgba16f_with_metadata(
decoded.pixels,
decoded.width,
decoded.height,
decoded.hdr,
decoded.wide_gamut,
),
other => {
panic!("Image::from_decoded: unsupported decoded pixel format: {other:?}");
}
}
}
}
impl View for Image {
fn body(self, _env: &Environment) -> impl View {
let width = u32_to_f32(self.width);
let height = u32_to_f32(self.height);
let resizable = self.resizable;
let surface = GpuSurface::new(self.renderer);
let frame = Frame::new(surface);
if resizable {
frame
} else {
frame.width(width).height(height)
}
}
}
struct ImageFrame {
pixels: Vec<u8>,
width: u32,
height: u32,
source_pixel_format: SourcePixelFormat,
source_is_hdr: bool,
source_is_wide_gamut: bool,
interpolation: Interpolation,
}
impl fmt::Debug for ImageFrame {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ImageFrame")
.field("width", &self.width)
.field("height", &self.height)
.field("source_pixel_format", &self.source_pixel_format)
.field("source_is_hdr", &self.source_is_hdr)
.field("source_is_wide_gamut", &self.source_is_wide_gamut)
.field("interpolation", &self.interpolation)
.finish_non_exhaustive()
}
}
impl Image {
fn into_frame(mut self) -> ImageFrame {
let pixels = self
.renderer
.pending_pixels
.take()
.expect("Image can only enter a reactive image before GPU setup");
ImageFrame {
pixels,
width: self.width,
height: self.height,
source_pixel_format: self.renderer.source_pixel_format,
source_is_hdr: self.renderer.source_is_hdr,
source_is_wide_gamut: self.renderer.source_is_wide_gamut,
interpolation: self.renderer.interpolation,
}
}
}
#[derive(Debug, Default)]
struct PendingImageUpdate {
dirty: bool,
frame: Option<ImageFrame>,
}
#[derive(Debug)]
struct ReactiveImageState {
pending: RefCell<PendingImageUpdate>,
dimensions: Binding<Option<(u32, u32)>>,
redraw: RefCell<Option<waterui_graphics::RedrawHandle>>,
}
impl ReactiveImageState {
fn publish(&self, frame: Option<ImageFrame>) {
self.dimensions
.set(frame.as_ref().map(|frame| (frame.width, frame.height)));
*self.pending.borrow_mut() = PendingImageUpdate { dirty: true, frame };
if let Some(redraw) = self.redraw.borrow().as_ref() {
redraw.request_redraw();
}
}
fn take_pending(&self) -> PendingImageUpdate {
core::mem::take(&mut *self.pending.borrow_mut())
}
}
#[derive(Clone, Debug)]
pub struct ReactiveImageHandle {
state: Rc<ReactiveImageState>,
}
impl ReactiveImageHandle {
pub fn set(&self, image: Image) {
self.state.publish(Some(image.into_frame()));
}
pub fn clear(&self) {
self.state.publish(None);
}
}
#[derive(Debug)]
pub struct ReactiveImage {
state: Rc<ReactiveImageState>,
resizable: bool,
}
impl ReactiveImage {
#[must_use]
pub const fn resizable(mut self) -> Self {
self.resizable = true;
self
}
}
impl View for ReactiveImage {
fn body(self, _env: &Environment) -> impl View {
let width = self
.state
.dimensions
.map(|dimensions| dimensions.map_or(0.0, |(width, _)| u32_to_f32(width)))
.computed();
let height = self
.state
.dimensions
.map(|dimensions| dimensions.map_or(0.0, |(_, height)| u32_to_f32(height)))
.computed();
let surface = GpuSurface::new(ReactiveImageRenderer {
image: ImageRenderer::empty(),
state: Rc::clone(&self.state),
});
let frame = Frame::new(surface);
if self.resizable {
frame
} else {
frame.width(width).height(height)
}
}
}
#[must_use]
pub fn reactive_image() -> (ReactiveImageHandle, ReactiveImage) {
let state = Rc::new(ReactiveImageState {
pending: RefCell::new(PendingImageUpdate::default()),
dimensions: Binding::container(None),
redraw: RefCell::new(None),
});
(
ReactiveImageHandle {
state: Rc::clone(&state),
},
ReactiveImage {
state,
resizable: false,
},
)
}
struct ImageRenderer {
pending_pixels: Option<Vec<u8>>,
source_pixel_format: SourcePixelFormat,
source_is_hdr: bool,
source_is_wide_gamut: bool,
width: u32,
height: u32,
interpolation: Interpolation,
texture: Option<wgpu::Texture>,
render_pipeline: Option<wgpu::RenderPipeline>,
bind_group: Option<wgpu::BindGroup>,
sampler: Option<wgpu::Sampler>,
}
impl fmt::Debug for ImageRenderer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageRenderer")
.field("width", &self.width)
.field("height", &self.height)
.finish_non_exhaustive()
}
}
impl ImageRenderer {
const fn new(
pixels: Vec<u8>,
width: u32,
height: u32,
source_pixel_format: SourcePixelFormat,
source_is_hdr: bool,
source_is_wide_gamut: bool,
) -> Self {
Self {
pending_pixels: Some(pixels),
source_pixel_format,
source_is_hdr,
source_is_wide_gamut,
width,
height,
interpolation: Interpolation::Linear,
texture: None,
render_pipeline: None,
bind_group: None,
sampler: None,
}
}
const fn empty() -> Self {
Self {
pending_pixels: None,
source_pixel_format: SourcePixelFormat::Rgba8UnormSrgb,
source_is_hdr: false,
source_is_wide_gamut: false,
width: 0,
height: 0,
interpolation: Interpolation::Linear,
texture: None,
render_pipeline: None,
bind_group: None,
sampler: None,
}
}
fn accept_frame(&mut self, frame: ImageFrame) {
self.pending_pixels = Some(frame.pixels);
self.source_pixel_format = frame.source_pixel_format;
self.source_is_hdr = frame.source_is_hdr;
self.source_is_wide_gamut = frame.source_is_wide_gamut;
self.width = frame.width;
self.height = frame.height;
self.interpolation = frame.interpolation;
}
fn clear_frame(&mut self) {
self.pending_pixels = None;
self.texture = None;
self.render_pipeline = None;
self.bind_group = None;
self.sampler = None;
self.width = 0;
self.height = 0;
}
#[inline]
const fn bytes_per_pixel(source_pixel_format: SourcePixelFormat) -> u32 {
match source_pixel_format {
SourcePixelFormat::Rgba8UnormSrgb => 4,
SourcePixelFormat::Rgba16Float => 8,
}
}
fn create_render_pipeline(
device: &wgpu::Device,
format: wgpu::TextureFormat,
hdr_to_sdr_tonemap: bool,
interpolation: Interpolation,
) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout, wgpu::Sampler) {
let blend = Some(wgpu::BlendState::ALPHA_BLENDING);
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Image bind group layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Image pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let fragment_entry_point = if hdr_to_sdr_tonemap {
"fs_main_tonemap"
} else {
"fs_main"
};
let (vertex_shader, fragment_shader) = crate::IMAGE_RENDER_SHADER.create_render_stages(
device,
"vs_main",
fragment_entry_point,
);
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Image render pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: vertex_shader.module(),
entry_point: Some(vertex_shader.entry_point()),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: fragment_shader.module(),
entry_point: Some(fragment_shader.entry_point()),
targets: &[Some(wgpu::ColorTargetState {
format,
blend,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let filter = interpolation.to_wgpu_filter();
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Image sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: filter,
min_filter: filter,
mipmap_filter: interpolation.to_wgpu_mipmap_filter(),
..Default::default()
});
(render_pipeline, bind_group_layout, sampler)
}
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
target_format: wgpu::TextureFormat,
) {
let Some(pixels) = self.pending_pixels.take() else {
return;
};
let texture_format = match self.source_pixel_format {
SourcePixelFormat::Rgba8UnormSrgb => wgpu::TextureFormat::Rgba8UnormSrgb,
SourcePixelFormat::Rgba16Float => wgpu::TextureFormat::Rgba16Float,
};
let row_layout = TextureRowLayout::new(
self.width,
self.height,
Self::bytes_per_pixel(self.source_pixel_format),
);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Image source texture"),
size: row_layout.extent(),
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: texture_format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
upload_texture(queue, &texture, &pixels, row_layout);
let hdr_to_sdr_tonemap =
should_tonemap_hdr_to_sdr(self.source_pixel_format, self.source_is_hdr, target_format);
let (render_pipeline, bind_group_layout, sampler) = Self::create_render_pipeline(
device,
target_format,
hdr_to_sdr_tonemap,
self.interpolation,
);
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Image bind group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
self.texture = Some(texture);
self.render_pipeline = Some(render_pipeline);
self.bind_group = Some(bind_group);
self.sampler = Some(sampler);
}
}
impl GpuView for ImageRenderer {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let intrinsic = Size::new(u32_to_f32(self.width), u32_to_f32(self.height));
ViewDimensions::new(Size::new(
proposal.width.unwrap_or(intrinsic.width),
proposal.height.unwrap_or(intrinsic.height),
))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn setup(
&mut self,
ctx: &GpuContext<'_>,
_env: &mut waterui_core::Environment,
) -> impl core::future::Future<Output = ()> {
tracing::debug!(
"[ImageRenderer] setup() called with format: {:?}, size: {}x{}, source_hdr={}, source_wide_gamut={}",
ctx.surface_format,
self.width,
self.height,
self.source_is_hdr,
self.source_is_wide_gamut
);
self.prepare(ctx.device, ctx.queue, ctx.surface_format);
core::future::ready(())
}
fn render(&mut self, frame: &mut GpuFrame) {
tracing::debug!(
"[ImageRenderer] render() called, format: {:?}, size: {}x{}, has_pipeline: {}",
frame.format,
frame.width,
frame.height,
self.render_pipeline.is_some()
);
let mut encoder = frame
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Image render encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Image render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame.view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if let (Some(render_pipeline), Some(bind_group)) =
(&self.render_pipeline, &self.bind_group)
{
render_pass.set_pipeline(render_pipeline);
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
}
frame.queue.submit([encoder.finish()]);
}
}
struct ReactiveImageRenderer {
image: ImageRenderer,
state: Rc<ReactiveImageState>,
}
impl GpuView for ReactiveImageRenderer {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let (width, height) = self.state.dimensions.get().unwrap_or((0, 0));
let intrinsic = Size::new(u32_to_f32(width), u32_to_f32(height));
ViewDimensions::new(Size::new(
proposal.width.unwrap_or(intrinsic.width),
proposal.height.unwrap_or(intrinsic.height),
))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn setup(
&mut self,
ctx: &GpuContext<'_>,
env: &mut waterui_core::Environment,
) -> impl core::future::Future<Output = ()> {
*self.state.redraw.borrow_mut() = Some(ctx.redraw_handle.clone());
let update = self.state.take_pending();
if update.dirty {
match update.frame {
Some(frame) => self.image.accept_frame(frame),
None => self.image.clear_frame(),
}
}
self.image.setup(ctx, env)
}
fn render(&mut self, frame: &mut GpuFrame) {
let update = self.state.take_pending();
if update.dirty {
match update.frame {
Some(image) => {
self.image.accept_frame(image);
self.image.prepare(frame.device, frame.queue, frame.format);
}
None => self.image.clear_frame(),
}
}
self.image.render(frame);
}
}
impl Drop for ReactiveImageRenderer {
fn drop(&mut self) {
self.state.redraw.borrow_mut().take();
}
}
const fn should_tonemap_hdr_to_sdr(
source_pixel_format: SourcePixelFormat,
source_is_hdr: bool,
target_format: wgpu::TextureFormat,
) -> bool {
source_is_hdr
&& matches!(source_pixel_format, SourcePixelFormat::Rgba16Float)
&& !matches!(
target_format,
wgpu::TextureFormat::Rgba16Float | wgpu::TextureFormat::Rgba32Float
)
}
#[must_use]
pub fn image(pixels: Vec<u8>, width: u32, height: u32) -> Image {
Image::new(pixels, width, height)
}
#[derive(Debug, Clone)]
pub struct ImageStreamDecoder {
content_type: Option<String>,
bytes: Vec<u8>,
attempts: usize,
next_attempt_at: usize,
last_fingerprint: Option<u64>,
}
impl ImageStreamDecoder {
const FIRST_ATTEMPT_BYTES: usize = 24 * 1024;
const ATTEMPT_STEP_BYTES: usize = 96 * 1024;
const MAX_ATTEMPTS: usize = 10;
const MAX_BUFFER_BYTES: usize = 8 * 1024 * 1024;
#[must_use]
pub fn new(content_type: Option<&str>) -> Self {
Self {
content_type: content_type.map(ToOwned::to_owned),
bytes: Vec::new(),
attempts: 0,
next_attempt_at: Self::FIRST_ATTEMPT_BYTES,
last_fingerprint: None,
}
}
#[must_use]
pub fn push_chunk(&mut self, chunk: &[u8]) -> Option<Image> {
if chunk.is_empty() {
return None;
}
self.bytes.extend_from_slice(chunk);
let total_len = self.bytes.len();
if self.attempts >= Self::MAX_ATTEMPTS
|| total_len < self.next_attempt_at
|| total_len > Self::MAX_BUFFER_BYTES
|| !codec::is_progressive_candidate(self.content_type.as_deref(), &self.bytes)
{
return None;
}
self.attempts += 1;
self.next_attempt_at = total_len.saturating_add(Self::ATTEMPT_STEP_BYTES);
let decoded = codec::decode_progressive_frame(&self.bytes)?;
let fingerprint = frame_fingerprint(&decoded);
if self.last_fingerprint == Some(fingerprint) {
return None;
}
self.last_fingerprint = Some(fingerprint);
Some(Image::from_decoded(decoded))
}
pub fn finish(self) -> Result<Image, String> {
if self.bytes.is_empty() {
return Err(String::from("image response body was empty"));
}
Image::from_encoded(&self.bytes)
}
}
fn frame_fingerprint(decoded: &DecodedRgba) -> u64 {
let len = decoded.pixels.len();
if len == 0 {
return 0;
}
let first = u64::from(decoded.pixels[0]);
let mid = u64::from(decoded.pixels[len / 2]);
let last = u64::from(decoded.pixels[len - 1]);
(u64::from(decoded.width) << 32)
^ u64::from(decoded.height)
^ (u64::try_from(len).expect("image fingerprint length must fit in u64") << 8)
^ first
^ (mid << 16)
^ (last << 24)
}
fn u32_to_f32(value: u32) -> f32 {
value
.to_f32()
.expect("image dimensions must be representable as f32")
}
#[cfg(test)]
mod tests {
use super::{Image, SourcePixelFormat, reactive_image, should_tonemap_hdr_to_sdr};
#[test]
fn reactive_image_replaces_frame_without_replacing_view() {
let (handle, _view) = reactive_image();
handle.set(Image::new(vec![0, 0, 0, 255], 1, 1));
assert_eq!(handle.state.dimensions.get(), Some((1, 1)));
let update = handle.state.take_pending();
assert!(update.dirty, "published frame must be pending");
let frame = update.frame.expect("published update must contain a frame");
assert_eq!((frame.width, frame.height), (1, 1));
assert!(!handle.state.take_pending().dirty);
handle.clear();
assert_eq!(handle.state.dimensions.get(), None);
let update = handle.state.take_pending();
assert!(update.dirty);
assert!(update.frame.is_none());
}
#[test]
fn tonemap_only_for_hdr_float_source_into_sdr_target() {
assert!(should_tonemap_hdr_to_sdr(
SourcePixelFormat::Rgba16Float,
true,
wgpu::TextureFormat::Rgba8Unorm
));
assert!(!should_tonemap_hdr_to_sdr(
SourcePixelFormat::Rgba16Float,
false,
wgpu::TextureFormat::Rgba8Unorm
));
assert!(!should_tonemap_hdr_to_sdr(
SourcePixelFormat::Rgba16Float,
true,
wgpu::TextureFormat::Rgba16Float
));
assert!(!should_tonemap_hdr_to_sdr(
SourcePixelFormat::Rgba8UnormSrgb,
true,
wgpu::TextureFormat::Rgba8Unorm
));
}
}