use std::cell::Cell;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
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, hash_sampled};
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, Copy, Debug, PartialEq, Eq, Hash)]
struct CacheKey {
source_hash: u64,
extent: Extent,
}
fn svg_extent(
(width, height): (f32, f32),
scale: f32,
target: Option<(f32, f32)>,
) -> 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 extent = ((width * scale).ceil(), (height * scale).ceil());
(extent.0 >= 1.0 && extent.1 >= 1.0).then_some((extent.0 as u32, extent.1 as u32))
}
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>>,
last_svg_raster: FxHashMap<u64, 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) -> Self {
Self {
quad: TexturedQuadPipeline::new(device, format, "ImageQuad"),
atlas: ImageAtlas::default(),
texture_cache: FxHashMap::default(),
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();
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))
.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 self.last_svg_raster.get(&key.source_hash) == Some(&key.extent) {
self.last_svg_raster.remove(&key.source_hash);
}
}
}
}
#[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 hash_source(source: &ImageSource) -> u64 {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
match source {
ImageSource::Path(path) => {
"path".hash(&mut hasher);
path.hash(&mut hasher);
}
ImageSource::Bytes(bytes) => {
"bytes".hash(&mut hasher);
hash_sampled(bytes, &mut hasher);
}
ImageSource::Rgba {
width,
height,
pixels,
} => {
"rgba".hash(&mut hasher);
width.hash(&mut hasher);
height.hash(&mut hasher);
hash_sampled(pixels, &mut hasher);
}
ImageSource::SvgPath(path) => {
"svg_path".hash(&mut hasher);
path.hash(&mut hasher);
}
ImageSource::SvgBytes(bytes) => {
"svg_bytes".hash(&mut hasher);
hash_sampled(bytes, &mut hasher);
}
}
hasher.finish()
}
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)?),
None => None,
};
let key = CacheKey {
source_hash: Self::hash_source(source),
extent,
};
let texture = match self.texture_cache.get(&key) {
Some(cached) => Some(cached.clone()),
None => self.load_texture(device, queue, source, key, decoded),
};
let texture = match texture {
Some(texture) => {
if extent.is_some() {
self.last_svg_raster.insert(key.source_hash, extent);
}
texture
}
None => {
let last = self
.last_svg_raster
.get(&key.source_hash)
.and_then(|&extent| self.texture_cache.get(&CacheKey { extent, ..key }));
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;
};
self.upload_raster(device, queue, pixels.width, pixels.height, &pixels.rgba)?
}
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> {
if width == 0 || height == 0 {
return None;
}
self.store(
device,
queue,
width,
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::shape(&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]) {
self.quad.draw(render_pass, quads, Pipeline::Images);
}
}