#![allow(unsafe_code)]
use anyhow::{Context as _, Result};
use etagere::{size2, BucketedAtlasAllocator};
use parking_lot::Mutex;
use std::{borrow::Cow, collections::HashMap, ops, sync::Arc};
use super::{Bounds, DevicePixels, Point, Size};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AtlasKey(pub u64);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum AtlasTextureKind {
Monochrome,
Subpixel,
Polychrome,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AtlasTextureId {
pub index: u32,
pub kind: AtlasTextureKind,
}
#[derive(Clone, Debug)]
pub struct AtlasTile {
pub texture_id: AtlasTextureId,
pub tile_id: u32,
pub padding: u8,
pub bounds: Bounds<DevicePixels>,
}
fn device_size_to_etagere(size: Size<DevicePixels>) -> etagere::Size {
size2(size.width.0, size.height.0)
}
fn etagere_point_to_device(pt: etagere::Point) -> Point<DevicePixels> {
Point {
x: DevicePixels(pt.x),
y: DevicePixels(pt.y),
}
}
struct PendingUpload {
id: AtlasTextureId,
bounds: Bounds<DevicePixels>,
data: Vec<u8>,
}
struct WgpuAtlasState {
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
max_texture_size: u32,
storage: WgpuAtlasStorage,
tiles_by_key: HashMap<AtlasKey, AtlasTile>,
pending_uploads: Vec<PendingUpload>,
}
pub struct AndroidAtlas(Mutex<WgpuAtlasState>);
pub struct AtlasTextureInfo {
pub view: wgpu::TextureView,
pub format: wgpu::TextureFormat,
pub size: Size<DevicePixels>,
}
impl AndroidAtlas {
pub fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>) -> Self {
let max_texture_size = device.limits().max_texture_dimension_2d;
AndroidAtlas(Mutex::new(WgpuAtlasState {
device,
queue,
max_texture_size,
storage: WgpuAtlasStorage::default(),
tiles_by_key: HashMap::new(),
pending_uploads: Vec::new(),
}))
}
pub fn before_frame(&self) {
let mut lock = self.0.lock();
lock.flush_uploads();
}
pub fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
kind: AtlasTextureKind,
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>> {
let mut lock = self.0.lock();
if let Some(tile) = lock.tiles_by_key.get(key) {
return Ok(Some(tile.clone()));
}
let Some((size, bytes)) = build()? else {
return Ok(None);
};
let tile = lock
.allocate(size, kind)
.context("atlas: failed to allocate tile")?;
lock.upload_tile(tile.texture_id, tile.bounds, &bytes);
lock.tiles_by_key.insert(key.clone(), tile.clone());
Ok(Some(tile))
}
pub fn remove(&self, key: &AtlasKey) {
let mut lock = self.0.lock();
let Some(id) = lock.tiles_by_key.remove(key).map(|t| t.texture_id) else {
return;
};
let textures = &mut lock.storage[id.kind];
if let Some(slot) = textures.textures.get_mut(id.index as usize) {
if let Some(mut texture) = slot.take() {
texture.decrement_ref_count();
if texture.is_unreferenced() {
textures.free_list.push(id.index as usize);
} else {
*slot = Some(texture);
}
}
}
}
pub fn get_texture_info(&self, id: AtlasTextureId) -> AtlasTextureInfo {
let lock = self.0.lock();
let tex = &lock.storage[id];
AtlasTextureInfo {
view: tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default()),
format: tex.format,
size: Size {
width: DevicePixels(tex.width as i32),
height: DevicePixels(tex.height as i32),
},
}
}
pub fn texture_counts(&self) -> (usize, usize, usize) {
let lock = self.0.lock();
(
lock.storage
.monochrome
.textures
.iter()
.filter(|t| t.is_some())
.count(),
lock.storage
.subpixel
.textures
.iter()
.filter(|t| t.is_some())
.count(),
lock.storage
.polychrome
.textures
.iter()
.filter(|t| t.is_some())
.count(),
)
}
pub fn invalidate(&self) {
let mut lock = self.0.lock();
lock.tiles_by_key.clear();
lock.pending_uploads.clear();
for kind in [
AtlasTextureKind::Monochrome,
AtlasTextureKind::Subpixel,
AtlasTextureKind::Polychrome,
] {
for slot in lock.storage[kind].textures.iter_mut() {
if let Some(tex) = slot.as_mut() {
tex.allocator =
BucketedAtlasAllocator::new(size2(tex.width as i32, tex.height as i32));
tex.live_atlas_keys = 0;
}
}
}
}
}
impl WgpuAtlasState {
fn allocate(&mut self, size: Size<DevicePixels>, kind: AtlasTextureKind) -> Option<AtlasTile> {
{
let textures = &mut self.storage[kind];
if let Some(tile) = textures
.textures
.iter_mut()
.rev()
.filter_map(|slot| slot.as_mut())
.find_map(|tex| tex.allocate(size))
{
return Some(tile);
}
}
let tex = self.push_texture(size, kind);
tex.allocate(size)
}
fn push_texture(
&mut self,
min_size: Size<DevicePixels>,
kind: AtlasTextureKind,
) -> &mut WgpuAtlasTexture {
const DEFAULT_ATLAS_SIZE: u32 = 1024;
let max = self.max_texture_size;
let w = (min_size.width.0 as u32).max(DEFAULT_ATLAS_SIZE).min(max);
let h = (min_size.height.0 as u32).max(DEFAULT_ATLAS_SIZE).min(max);
let format = match kind {
AtlasTextureKind::Monochrome => wgpu::TextureFormat::R8Unorm,
AtlasTextureKind::Subpixel => wgpu::TextureFormat::Rgba8Unorm,
AtlasTextureKind::Polychrome => wgpu::TextureFormat::Rgba8Unorm,
};
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("android_atlas"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let storage = &mut self.storage[kind];
let index = storage.free_list.pop().unwrap_or(storage.textures.len());
let atlas_tex = WgpuAtlasTexture {
id: AtlasTextureId {
index: index as u32,
kind,
},
allocator: BucketedAtlasAllocator::new(size2(w as i32, h as i32)),
texture,
format,
width: w,
height: h,
live_atlas_keys: 0,
};
if index < storage.textures.len() {
storage.textures[index] = Some(atlas_tex);
storage.textures[index].as_mut().unwrap()
} else {
storage.textures.push(Some(atlas_tex));
storage.textures.last_mut().unwrap().as_mut().unwrap()
}
}
fn upload_tile(&mut self, id: AtlasTextureId, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
self.pending_uploads.push(PendingUpload {
id,
bounds,
data: bytes.to_vec(),
});
}
fn flush_uploads(&mut self) {
for upload in self.pending_uploads.drain(..) {
let texture = &self.storage[upload.id];
let bpp = bytes_per_pixel(texture.format);
let w = upload.bounds.size.width.0 as u32;
let h = upload.bounds.size.height.0 as u32;
if w == 0 || h == 0 || upload.data.is_empty() {
continue;
}
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: upload.bounds.origin.x.0 as u32,
y: upload.bounds.origin.y.0 as u32,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
&upload.data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w * bpp as u32),
rows_per_image: None,
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
}
}
}
struct AtlasTextureList {
textures: Vec<Option<WgpuAtlasTexture>>,
free_list: Vec<usize>,
}
impl Default for AtlasTextureList {
fn default() -> Self {
Self {
textures: Vec::new(),
free_list: Vec::new(),
}
}
}
impl AtlasTextureList {}
#[derive(Default)]
struct WgpuAtlasStorage {
monochrome: AtlasTextureList,
subpixel: AtlasTextureList,
polychrome: AtlasTextureList,
}
impl ops::Index<AtlasTextureKind> for WgpuAtlasStorage {
type Output = AtlasTextureList;
fn index(&self, kind: AtlasTextureKind) -> &Self::Output {
match kind {
AtlasTextureKind::Monochrome => &self.monochrome,
AtlasTextureKind::Subpixel => &self.subpixel,
AtlasTextureKind::Polychrome => &self.polychrome,
}
}
}
impl ops::IndexMut<AtlasTextureKind> for WgpuAtlasStorage {
fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output {
match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome,
AtlasTextureKind::Subpixel => &mut self.subpixel,
AtlasTextureKind::Polychrome => &mut self.polychrome,
}
}
}
impl ops::Index<AtlasTextureId> for WgpuAtlasStorage {
type Output = WgpuAtlasTexture;
fn index(&self, id: AtlasTextureId) -> &Self::Output {
let list = &self[id.kind];
list.textures
.get(id.index as usize)
.and_then(|s| s.as_ref())
.unwrap_or_else(|| panic!("atlas texture {:?} does not exist", id))
}
}
struct WgpuAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
texture: wgpu::Texture,
format: wgpu::TextureFormat,
width: u32,
height: u32,
live_atlas_keys: u32,
}
impl WgpuAtlasTexture {
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
let alloc = self.allocator.allocate(device_size_to_etagere(size))?;
self.live_atlas_keys += 1;
Some(AtlasTile {
texture_id: self.id,
tile_id: alloc.id.serialize(),
padding: 0,
bounds: Bounds {
origin: etagere_point_to_device(alloc.rectangle.min),
size,
},
})
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys = self.live_atlas_keys.saturating_sub(1);
}
fn is_unreferenced(&self) -> bool {
self.live_atlas_keys == 0
}
}
fn bytes_per_pixel(format: wgpu::TextureFormat) -> u8 {
match format {
wgpu::TextureFormat::R8Unorm => 1,
wgpu::TextureFormat::Rgba8Unorm
| wgpu::TextureFormat::Bgra8Unorm
| wgpu::TextureFormat::Rgba8UnormSrgb
| wgpu::TextureFormat::Bgra8UnormSrgb => 4,
_ => 4, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bytes_per_pixel_correct() {
assert_eq!(bytes_per_pixel(wgpu::TextureFormat::R8Unorm), 1);
assert_eq!(bytes_per_pixel(wgpu::TextureFormat::Rgba8Unorm), 4);
assert_eq!(bytes_per_pixel(wgpu::TextureFormat::Bgra8Unorm), 4);
}
#[test]
fn etagere_point_conversion() {
let pt = etagere_point_to_device(etagere::point(10, 20));
assert_eq!(pt.x, DevicePixels(10));
assert_eq!(pt.y, DevicePixels(20));
}
#[test]
fn etagere_size_conversion() {
let s = device_size_to_etagere(Size {
width: DevicePixels(128),
height: DevicePixels(256),
});
assert_eq!(s.width, 128);
assert_eq!(s.height, 256);
}
#[test]
fn texture_list_starts_empty() {
let list = AtlasTextureList::default();
assert!(list.textures.is_empty());
assert!(list.free_list.is_empty());
}
#[test]
fn storage_index_kind() {
let storage = WgpuAtlasStorage::default();
let _ = &storage[AtlasTextureKind::Monochrome];
let _ = &storage[AtlasTextureKind::Subpixel];
let _ = &storage[AtlasTextureKind::Polychrome];
}
#[test]
fn unreferenced_after_decrement() {
let mut count: u32 = 1;
count = count.saturating_sub(1);
assert_eq!(count, 0, "should be unreferenced after decrement");
}
#[test]
fn atlas_key_equality() {
let k1 = AtlasKey(42);
let k2 = AtlasKey(42);
let k3 = AtlasKey(99);
assert_eq!(k1, k2);
assert_ne!(k1, k3);
}
#[test]
fn pending_uploads_start_empty() {
let uploads: Vec<PendingUpload> = Vec::new();
assert!(uploads.is_empty());
}
#[test]
fn bytes_per_pixel_fallback() {
assert_eq!(bytes_per_pixel(wgpu::TextureFormat::Depth32Float), 4);
}
}