use std::borrow::Cow;
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};
use rustc_hash::FxHashMap;
use wgpu::{
BindGroup, Device, Extent3d, Queue, RenderPass, Texture, TextureDimension, TextureFormat,
TextureUsages,
};
use super::commands::DrawCommand;
use super::constants::SVG_QUALITY_MULTIPLIER;
use super::flatten::FlattenedCommand;
use super::image_atlas::{AtlasEntry, ImageAtlas};
use super::textured_quad::{QuadDraw, TexturedQuadPipeline};
use super::textured_vertex::{NO_TINT, QuadClip, TexturedVertex};
use crate::image_decode::{DecodeKey, DecodedImage, Extent, SampledBytes};
use crate::render_stats::Pipeline;
use crate::widgets::Color;
use crate::widgets::Rect;
use crate::widgets::image::{ContentFit, ImageSource};
pub struct PreparedImageQuad {
texture: Rc<CachedTexture>,
quad: u32,
}
impl QuadDraw for PreparedImageQuad {
fn bind_group(&self) -> &Rc<BindGroup> {
&self.texture.bind_group
}
fn quad(&self) -> u32 {
self.quad
}
}
enum Backing {
Own(Texture),
Atlas(AtlasEntry),
}
struct CachedTexture {
backing: Backing,
bind_group: Rc<BindGroup>,
uv_rect: [f32; 4],
intrinsic_width: f32,
intrinsic_height: f32,
last_used: Cell<Instant>,
decoded_from: Option<(DecodeKey, Extent)>,
}
impl CachedTexture {
fn bytes(&self) -> usize {
match &self.backing {
Backing::Own(texture) => texture_bytes(texture),
Backing::Atlas(entry) => entry.bytes(),
}
}
}
impl Drop for CachedTexture {
fn drop(&mut self) {
if let Some((key, extent)) = self.decoded_from.take() {
crate::image_decode::texture_evicted(key, extent);
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct CacheKey {
source: SourceKey,
extent: Extent,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum SourceKey {
Decoded(DecodedImage),
Rgba(RgbaKey),
}
impl SourceKey {
fn of(source: &ImageSource, decoded: Option<&DecodedImage>) -> Option<Self> {
match source {
ImageSource::Rgba {
width,
height,
pixels,
} => Some(Self::Rgba(RgbaKey {
width: *width,
height: *height,
pixels: SampledBytes(pixels.clone()),
})),
_ => decoded.cloned().map(Self::Decoded),
}
}
fn orphaned(&self) -> bool {
matches!(self, Self::Rgba(key) if Arc::strong_count(&key.pixels.0) == 1)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct RgbaKey {
width: u32,
height: u32,
pixels: SampledBytes,
}
fn svg_extent(
(width, height): (f32, f32),
scale: f32,
target: Option<(f32, f32)>,
limit: u32,
) -> Option<(u32, u32)> {
let scale = (scale * 4.0).round() / 4.0;
let scale = match target {
Some((tw, th)) if tw >= 1.0 && th >= 1.0 => {
scale * (tw.round() / width).min(th.round() / height)
}
_ => scale,
};
let scale = scale.min(limit as f32 / width.max(height));
let extent = ((width * scale).ceil(), (height * scale).ceil());
(extent.0 >= 1.0 && extent.1 >= 1.0)
.then_some(((extent.0 as u32).min(limit), (extent.1 as u32).min(limit)))
}
fn resized_raster(width: u32, height: u32, rgba: &[u8], limit: u32) -> Option<(u32, u32, Vec<u8>)> {
let source = image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(width, height, rgba)?;
let longest = width.max(height) as u64;
if longest == 0 || limit == 0 {
return None;
}
let new_width = ((width as u64 * limit as u64 + longest / 2) / longest).max(1) as u32;
let new_height = ((height as u64 * limit as u64 + longest / 2) / longest).max(1) as u32;
let pixels = image::imageops::resize(
&source,
new_width,
new_height,
image::imageops::FilterType::Triangle,
);
Some((new_width, new_height, pixels.into_raw()))
}
fn raster_upload_pixels(
width: u32,
height: u32,
rgba: &[u8],
limit: u32,
) -> Option<(u32, u32, Cow<'_, [u8]>)> {
if width == 0 || height == 0 {
return None;
}
if width > limit || height > limit {
let Some((w, h, pixels)) = resized_raster(width, height, rgba, limit) else {
log::warn!("Failed to resize image {width}x{height} to texture limit {limit}");
return None;
};
log::warn!("Resized image {width}x{height} to {w}x{h} for texture limit {limit}");
Some((w, h, Cow::Owned(pixels)))
} else {
Some((width, height, Cow::Borrowed(rgba)))
}
}
fn own_texture(device: &Device, queue: &Queue, width: u32, height: u32, rgba: &[u8]) -> Texture {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Image Texture"),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
texture
}
pub struct ImageQuadRenderer {
quad: TexturedQuadPipeline,
atlas: ImageAtlas,
texture_cache: FxHashMap<CacheKey, Rc<CachedTexture>>,
texture_limit: u32,
last_svg_raster: FxHashMap<DecodedImage, Extent>,
frame_started: Instant,
cached_bytes: usize,
}
fn opacity_and_tint(opacity: f32, tint: Option<Color>) -> (f32, [f32; 4]) {
match tint {
Some(c) => (opacity * c.a, [c.r, c.g, c.b, 1.0]),
None => (opacity, NO_TINT),
}
}
const KEEP_UNUSED: Duration = Duration::from_secs(1);
fn texture_bytes(texture: &Texture) -> usize {
let size = texture.size();
let texel = texture.format().block_copy_size(None).unwrap_or(4) as usize;
(0..texture.mip_level_count())
.map(|level| {
let width = (size.width >> level).max(1) as usize;
let height = (size.height >> level).max(1) as usize;
width * height * texel
})
.sum()
}
impl ImageQuadRenderer {
pub fn new(
device: &Device,
format: TextureFormat,
clip_records: &wgpu::BindGroupLayout,
) -> Self {
Self {
quad: TexturedQuadPipeline::new(device, format, clip_records, "ImageQuad"),
atlas: ImageAtlas::default(),
texture_cache: FxHashMap::default(),
texture_limit: device.limits().max_texture_dimension_2d,
last_svg_raster: FxHashMap::default(),
frame_started: Instant::now(),
cached_bytes: 0,
}
}
pub fn set_screen_size(&mut self, width: f32, height: f32) {
self.quad.set_screen_size(width, height);
}
pub fn begin_frame(&mut self) {
self.frame_started = Instant::now();
self.quad.begin_frame();
}
pub fn upload(&mut self, device: &Device, queue: &Queue) {
self.quad.upload(device, queue);
}
pub fn trim(&mut self, budget: usize) {
self.atlas.drop_empty_pages();
self.texture_cache.retain(|key, texture| {
let orphaned = key.source.orphaned();
if orphaned {
self.cached_bytes -= texture.bytes();
}
!orphaned
});
if self.cached_bytes <= budget {
return;
}
let mut idle: Vec<(Instant, CacheKey)> = self
.texture_cache
.iter()
.filter(|(_, texture)| {
self.frame_started
.saturating_duration_since(texture.last_used.get())
> KEEP_UNUSED
})
.map(|(key, texture)| (texture.last_used.get(), key.clone()))
.collect();
idle.sort_unstable_by_key(|(last_used, _)| *last_used);
for (_, key) in idle {
if self.cached_bytes <= budget {
break;
}
if let Some(texture) = self.texture_cache.remove(&key) {
self.cached_bytes -= texture.bytes();
if let SourceKey::Decoded(decoded) = &key.source
&& self.last_svg_raster.get(decoded) == Some(&key.extent)
{
self.last_svg_raster.remove(decoded);
}
}
}
}
#[cfg(feature = "testing")]
pub(crate) fn texture_count(&self) -> usize {
self.texture_cache.len()
}
#[cfg(feature = "testing")]
pub(crate) fn forget_textures(&mut self) {
self.texture_cache.clear();
self.last_svg_raster.clear();
self.cached_bytes = 0;
}
#[cfg(feature = "testing")]
pub(crate) fn atlas_pages(&self) -> usize {
self.atlas.pages()
}
fn get_or_create_texture(
&mut self,
device: &Device,
queue: &Queue,
source: &ImageSource,
render_scale: f32,
svg_target: Option<(f32, f32)>,
decoded: Option<&DecodedImage>,
) -> Option<Rc<CachedTexture>> {
let extent = match decoded.and_then(DecodedImage::svg_size) {
Some(size) => Some(svg_extent(
size,
render_scale,
svg_target,
self.texture_limit,
)?),
None => None,
};
let key = CacheKey {
source: SourceKey::of(source, decoded)?,
extent,
};
let texture = match self.texture_cache.get_key_value(&key) {
Some((cached_key, cached)) => {
let cached = cached.clone();
if cached_key.source.orphaned() {
self.texture_cache.remove(&key);
self.texture_cache.insert(key.clone(), cached.clone());
}
Some(cached)
}
None => self.load_texture(device, queue, source, key.clone(), decoded),
};
let svg = extent.and(decoded);
let texture = match texture {
Some(texture) => {
if let Some(svg) = svg
&& self.last_svg_raster.get(svg) != Some(&extent)
{
self.last_svg_raster.insert(svg.clone(), extent);
}
texture
}
None => {
let last = svg
.and_then(|svg| self.last_svg_raster.get(svg))
.and_then(|&extent| {
self.texture_cache.get(&CacheKey {
source: key.source.clone(),
extent,
})
});
match (last, decoded) {
(Some(last), _) => last.clone(),
(None, Some(decoded)) if extent.is_some() => {
decoded.drew_nothing();
return None;
}
(None, _) => return None,
}
}
};
texture.last_used.set(self.frame_started);
Some(texture)
}
fn load_texture(
&mut self,
device: &Device,
queue: &Queue,
source: &ImageSource,
key: CacheKey,
decoded: Option<&DecodedImage>,
) -> Option<Rc<CachedTexture>> {
let mut texture = match source {
ImageSource::Path(_) | ImageSource::Bytes(_) => {
let Some(pixels) = decoded.and_then(DecodedImage::take) else {
crate::image_decode::texture_missing(source);
return None;
};
match self.upload_raster(device, queue, pixels.width, pixels.height, &pixels.rgba) {
Some(texture) => texture,
None => {
decoded?.upload_failed();
return None;
}
}
}
ImageSource::Rgba {
width,
height,
pixels,
} => {
let expected = (*width as usize)
.checked_mul(*height as usize)
.and_then(|v| v.checked_mul(4));
if Some(pixels.len()) != expected {
log::warn!(
"Rgba image source size mismatch: {}x{} expects {:?} bytes, got {}",
width,
height,
expected,
pixels.len()
);
return None;
}
let pixels = crate::image_decode::premultiplied(pixels);
self.upload_raster(device, queue, *width, *height, &pixels)?
}
ImageSource::SvgPath(_) | ImageSource::SvgBytes(_) => {
let decoded = decoded?;
let pixels = decoded.take_or_rasterize(key.extent)?;
self.store(
device,
queue,
pixels.width,
pixels.height,
&pixels.rgba,
decoded.svg_size()?,
)?
}
};
texture.decoded_from = decoded.map(|decoded| (decoded.key().clone(), key.extent));
let cached = Rc::new(texture);
self.cached_bytes += cached.bytes();
self.texture_cache.insert(key, cached.clone());
Some(cached)
}
fn upload_raster(
&mut self,
device: &Device,
queue: &Queue,
width: u32,
height: u32,
rgba: &[u8],
) -> Option<CachedTexture> {
let (upload_width, upload_height, rgba) =
raster_upload_pixels(width, height, rgba, self.texture_limit)?;
self.store(
device,
queue,
upload_width,
upload_height,
&rgba,
(width as f32, height as f32),
)
}
fn store(
&mut self,
device: &Device,
queue: &Queue,
width: u32,
height: u32,
rgba: &[u8],
(intrinsic_width, intrinsic_height): (f32, f32),
) -> Option<CachedTexture> {
let (backing, bind_group, uv_rect) = match self
.atlas
.insert(device, queue, &self.quad, width, height, rgba)
{
Some((entry, uv_rect)) => {
let bind_group = entry.bind_group().clone();
(Backing::Atlas(entry), bind_group, uv_rect)
}
None => {
let texture = own_texture(device, queue, width, height, rgba);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self
.quad
.bind_texture(device, &view, "ImageQuad Bind Group");
(Backing::Own(texture), bind_group, [0.0, 0.0, 1.0, 1.0])
}
};
Some(CachedTexture {
backing,
bind_group,
uv_rect,
intrinsic_width,
intrinsic_height,
last_used: Cell::new(self.frame_started),
decoded_from: None,
})
}
pub fn prepare(
&mut self,
device: &Device,
queue: &Queue,
commands: &[FlattenedCommand],
scale_factor: f32,
out: &mut Vec<PreparedImageQuad>,
) {
out.extend(
commands
.iter()
.filter_map(|cmd| self.prepare_single(device, queue, cmd, scale_factor)),
);
}
fn prepare_single(
&mut self,
device: &Device,
queue: &Queue,
cmd: &FlattenedCommand,
scale_factor: f32,
) -> Option<PreparedImageQuad> {
let (source, decoded, rect, content_fit, tint) = match &*cmd.command {
DrawCommand::Image {
source,
decoded,
rect,
content_fit,
tint,
} => (source, decoded, rect, content_fit, tint),
_ => return None,
};
let transform_scale = cmd.world_transform.extract_scale().max(1.0);
let svg_target = (*content_fit != ContentFit::None).then_some((rect.width, rect.height));
let render_scale = if source.is_svg() {
transform_scale * scale_factor * SVG_QUALITY_MULTIPLIER
} else {
1.0
};
let cached = self.get_or_create_texture(
device,
queue,
source,
render_scale,
svg_target,
decoded.as_ref(),
)?;
let (display_rect, (u_min, v_min, u_max, v_max)) = self.calculate_display_rect_and_uv(
rect,
cached.intrinsic_width,
cached.intrinsic_height,
*content_fit,
);
let [x, y, width, height] = cached.uv_rect;
let uv = (
x + u_min * width,
y + v_min * height,
x + u_max * width,
y + v_max * height,
);
let clip = cmd.clip().map_or(QuadClip::NONE, |clip| {
QuadClip::resolved(&clip, scale_factor)
});
let corners = self.compute_vertices(
&display_rect,
&cmd.world_transform,
uv,
scale_factor,
clip,
opacity_and_tint(cmd.opacity, *tint),
);
Some(PreparedImageQuad {
quad: self.quad.push(corners),
texture: cached,
})
}
fn calculate_display_rect_and_uv(
&self,
rect: &Rect,
intrinsic_width: f32,
intrinsic_height: f32,
content_fit: ContentFit,
) -> (Rect, (f32, f32, f32, f32)) {
let img_width = intrinsic_width;
let img_height = intrinsic_height;
let img_aspect = img_width / img_height;
let widget_aspect = rect.width / rect.height;
match content_fit {
ContentFit::Fill => {
(*rect, (0.0, 0.0, 1.0, 1.0))
}
ContentFit::Contain => {
let (scaled_w, scaled_h) = if widget_aspect > img_aspect {
(rect.height * img_aspect, rect.height)
} else {
(rect.width, rect.width / img_aspect)
};
let offset_x = (rect.width - scaled_w) / 2.0;
let offset_y = (rect.height - scaled_h) / 2.0;
(
Rect::new(rect.x + offset_x, rect.y + offset_y, scaled_w, scaled_h),
(0.0, 0.0, 1.0, 1.0),
)
}
ContentFit::Cover => {
let (u_min, v_min, u_max, v_max) = if widget_aspect > img_aspect {
let visible_height = img_aspect / widget_aspect;
let v_offset = (1.0 - visible_height) / 2.0;
(0.0, v_offset, 1.0, v_offset + visible_height)
} else {
let visible_width = widget_aspect / img_aspect;
let u_offset = (1.0 - visible_width) / 2.0;
(u_offset, 0.0, u_offset + visible_width, 1.0)
};
(*rect, (u_min, v_min, u_max, v_max))
}
ContentFit::None => {
let offset_x = (rect.width - img_width) / 2.0;
let offset_y = (rect.height - img_height) / 2.0;
(
Rect::new(rect.x + offset_x, rect.y + offset_y, img_width, img_height),
(0.0, 0.0, 1.0, 1.0),
)
}
}
}
fn compute_vertices(
&self,
rect: &Rect,
world_transform: &crate::transform::Transform,
uv: (f32, f32, f32, f32),
scale_factor: f32,
clip: QuadClip,
(opacity, tint): (f32, [f32; 4]),
) -> [TexturedVertex; 4] {
let local_corners = [
(rect.x, rect.y), (rect.x + rect.width, rect.y), (rect.x, rect.y + rect.height), (rect.x + rect.width, rect.y + rect.height), ];
let screen_corners: [(f32, f32); 4] = [
{
let (sx, sy) =
world_transform.transform_point(local_corners[0].0, local_corners[0].1);
(sx * scale_factor, sy * scale_factor)
},
{
let (sx, sy) =
world_transform.transform_point(local_corners[1].0, local_corners[1].1);
(sx * scale_factor, sy * scale_factor)
},
{
let (sx, sy) =
world_transform.transform_point(local_corners[2].0, local_corners[2].1);
(sx * scale_factor, sy * scale_factor)
},
{
let (sx, sy) =
world_transform.transform_point(local_corners[3].0, local_corners[3].1);
(sx * scale_factor, sy * scale_factor)
},
];
let (u_min, v_min, u_max, v_max) = uv;
let uvs = [
[u_min, v_min],
[u_max, v_min],
[u_min, v_max],
[u_max, v_max],
];
std::array::from_fn(|i| {
let (x, y) = screen_corners[i];
TexturedVertex::corner(self.quad.to_ndc(x, y), uvs[i], (x, y), &clip, opacity, tint)
})
}
pub fn render<'a>(
&'a self,
render_pass: &mut RenderPass<'a>,
quads: &'a [PreparedImageQuad],
clip_records: &'a wgpu::BindGroup,
) {
self.quad
.draw(render_pass, quads, clip_records, Pipeline::Images);
}
}
#[cfg(test)]
mod limit_tests {
use super::*;
#[test]
fn supported_raster_uploads_borrow_the_original_pixels() {
for (width, height) in [(7, 1), (1, 7), (8, 1), (1, 8), (8, 8)] {
let pixels = vec![255; (width * height * 4) as usize];
let (w, h, upload) = raster_upload_pixels(width, height, &pixels, 8).unwrap();
assert_eq!((w, h), (width, height));
assert!(
matches!(upload, Cow::Borrowed(_)),
"copied {width}x{height}"
);
assert_eq!(upload.as_ptr(), pixels.as_ptr());
}
}
#[test]
fn oversized_raster_uploads_own_only_the_resized_pixels() {
for (width, height) in [(9, 1), (1, 9), (9, 9)] {
let pixels = vec![255; (width * height * 4) as usize];
let (w, h, upload) = raster_upload_pixels(width, height, &pixels, 8).unwrap();
assert_eq!((w, h), (width.min(8), height.min(8)));
assert!(matches!(upload, Cow::Owned(_)));
assert_eq!(upload.as_ref(), vec![255; (w * h * 4) as usize]);
}
assert!(raster_upload_pixels(0, 1, &[], 8).is_none());
assert!(raster_upload_pixels(1, 0, &[], 8).is_none());
assert!(raster_upload_pixels(9, 1, &[0; 4], 8).is_none());
assert!(raster_upload_pixels(1, 1, &[0; 4], 0).is_none());
}
#[test]
fn svg_raster_extents_fit_both_axes() {
for ((width, height), expected) in [
((8193.0, 1.0), (8192, 1)),
((1.0, 8193.0), (1, 8192)),
((12000.0, 6000.0), (8192, 4096)),
((6000.0, 12000.0), (4096, 8192)),
] {
assert_eq!(svg_extent((width, height), 2.0, None, 8192), Some(expected));
}
assert_eq!(svg_extent((20.0, 10.0), 2.0, None, 8192), Some((40, 20)));
assert_eq!(
svg_extent((8193.0, 1.0), 2.0, Some((20.0, 20.0)), 8192),
Some((40, 1))
);
assert_eq!(svg_extent((0.0, 10.0), 2.0, None, 8192), None);
assert_eq!(svg_extent((10.0, 0.0), 2.0, None, 8192), None);
assert_eq!(svg_extent((20.0, 10.0), 0.0, None, 8192), None);
}
#[test]
fn raster_resize_preserves_aspect_ratio_and_premultiplied_color() {
for (width, height, expected) in [
(12, 6, (8, 4)),
(6, 12, (4, 8)),
(12, 1, (8, 1)),
(1, 12, (1, 8)),
] {
let pixels = [128, 0, 0, 128].repeat((width * height) as usize);
let (w, h, resized) = resized_raster(width, height, &pixels, 8).unwrap();
assert_eq!((w, h), expected);
assert_eq!(resized.len(), (w * h * 4) as usize);
assert!(
resized
.as_chunks::<4>()
.0
.iter()
.all(|pixel| *pixel == [128, 0, 0, 128])
);
}
}
#[test]
fn raster_resize_rounds_nonintegral_dimensions_to_the_nearest_texel() {
for (width, height, limit, expected) in [
(9, 2, 8, (8, 2)),
(2, 9, 8, (2, 8)),
(9, 3, 8, (8, 3)),
(3, 9, 8, (3, 8)),
(5, 2, 3, (3, 1)),
(2, 5, 3, (1, 3)),
(6, 3, 3, (3, 2)),
(3, 6, 3, (2, 3)),
] {
let pixels = [255; 4].repeat((width * height) as usize);
let (w, h, _) = resized_raster(width, height, &pixels, limit).unwrap();
assert_eq!((w, h), expected);
}
}
#[test]
fn raster_resize_keeps_distinct_nearly_unscaled_rows_and_columns() {
for (width, height, expected) in [(9, 2, (8, 2)), (2, 9, (2, 8))] {
let mut pixels = Vec::new();
for y in 0..height {
for x in 0..width {
pixels.extend_from_slice(
if (height == 2 && y == 0) || (width == 2 && x == 0) {
&[255, 0, 0, 255]
} else {
&[0, 0, 255, 255]
},
);
}
}
let (w, h, resized) = resized_raster(width, height, &pixels, 8).unwrap();
assert_eq!((w, h), expected);
for y in 0..h {
for x in 0..w {
let offset = ((y * w + x) * 4) as usize;
let expected = if (h == 2 && y == 0) || (w == 2 && x == 0) {
[255, 0, 0, 255]
} else {
[0, 0, 255, 255]
};
assert_eq!(&resized[offset..offset + 4], &expected);
}
}
}
}
#[test]
fn raster_shrinking_keeps_thin_lines_between_sample_centers() {
for (width, height) in [(40, 1), (1, 40)] {
let mut pixels = Vec::new();
for y in 0..height {
for x in 0..width {
pixels.extend_from_slice(if (x + y) % 5 == 0 {
&[128, 0, 0, 128]
} else {
&[0, 0, 0, 0]
});
}
}
let (_, _, resized) = resized_raster(width, height, &pixels, 8).unwrap();
for pixel in resized.as_chunks::<4>().0 {
assert!((16..=36).contains(&pixel[0]), "lost line: {pixel:?}");
assert_eq!(*pixel, [pixel[0], 0, 0, pixel[0]]);
}
}
}
#[test]
fn raster_resize_filters_both_axes_in_premultiplied_space() {
let pixels = [
128, 0, 0, 128, 0, 128, 0, 128, 0, 0, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 64, 0,
0, 64, 0, 64, 0, 64, 0, 0, 64, 64,
];
let (width, height, resized) = resized_raster(4, 2, &pixels, 2).unwrap();
assert_eq!((width, height), (2, 1));
assert_eq!(resized, [41, 32, 9, 82, 32, 50, 69, 96]);
}
#[test]
fn an_unresizable_raster_returns_no_pixels() {
assert!(resized_raster(12, 1, &[0; 4], 8).is_none());
assert!(resized_raster(12, 1, &[0; 48], 0).is_none());
assert!(resized_raster(0, 0, &[], 8).is_none());
}
#[test]
fn raster_uploads_below_the_limit_keep_their_texel_dimensions() {
let Some(gpu) = crate::or_skip(crate::renderer::GpuContext::try_new()) else {
return;
};
let mut renderer = ImageQuadRenderer::new(
&gpu.device,
TextureFormat::Rgba8Unorm,
super::super::clip_records::ClipRecords::new(&gpu.device).layout(),
);
let limit = gpu.device.limits().max_texture_dimension_2d;
for (width, height) in [(limit - 1, 1), (1, limit - 1)] {
let pixels = vec![255; (width * height * 4) as usize];
let texture = renderer
.upload_raster(&gpu.device, &gpu.queue, width, height, &pixels)
.unwrap();
let Backing::Own(texture) = &texture.backing else {
panic!("a near-limit image has its own texture");
};
assert_eq!((texture.width(), texture.height()), (width, height));
}
}
}