use crate::Texture;
#[cfg(feature = "gpu")]
use crate::texture::TextureGPU;
use rustc_hash::FxHashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
const ATLAS_FRAME_PADDING: u32 = 4;
#[derive(Debug, Clone, Copy)]
pub struct AtlasEntry {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct AtlasTileMeta {
pub first_frame: u32,
pub frame_count: u32,
}
pub struct AtlasGpuTables {
pub metas: Vec<AtlasTileMeta>,
pub frames: Vec<AtlasEntry>,
}
#[derive(Debug, Clone)]
pub struct Tile {
pub w: u32,
pub h: u32,
pub frames: Vec<Vec<u8>>,
pub material_frames: Vec<Vec<u8>>,
}
pub struct SharedAtlasInner {
pub tiles_map: FxHashMap<Uuid, Tile>,
pub tiles_order: Vec<Uuid>,
pub atlas: Texture,
pub atlas_material: Texture,
pub atlas_dirty: bool,
pub layout_dirty: bool,
pub layout_version: u64,
pub tiles_index_map: FxHashMap<Uuid, u32>,
pub atlas_map: FxHashMap<Uuid, Vec<AtlasEntry>>,
pub auto_size: bool,
}
#[derive(Clone)]
pub struct SharedAtlas {
inner: Arc<Mutex<SharedAtlasInner>>,
}
impl SharedAtlas {
pub fn new(width: u32, height: u32) -> Self {
Self {
inner: Arc::new(Mutex::new(SharedAtlasInner {
tiles_map: FxHashMap::default(),
tiles_order: Vec::new(),
atlas: Texture::new(width, height),
atlas_material: Texture::new(width, height),
atlas_dirty: true,
layout_dirty: true,
layout_version: 0,
tiles_index_map: FxHashMap::default(),
atlas_map: FxHashMap::default(),
auto_size: true,
})),
}
}
pub fn dims(&self) -> (u32, u32) {
let guard = self.inner.lock().unwrap();
(guard.atlas.width, guard.atlas.height)
}
pub fn add_tile(
&self,
id: Uuid,
width: u32,
height: u32,
frames: Vec<Vec<u8>>,
material_frames: Vec<Vec<u8>>,
) {
let mut guard = self.inner.lock().unwrap();
guard.tiles_map.insert(
id,
Tile {
w: width,
h: height,
frames,
material_frames,
},
);
if !guard.tiles_order.contains(&id) {
guard.tiles_order.push(id);
}
guard.atlas_dirty = true;
guard.layout_dirty = true;
}
pub fn layout_version(&self) -> u64 {
let guard = self.inner.lock().unwrap();
guard.layout_version
}
pub fn tile_index(&self, id: &Uuid) -> Option<u32> {
let guard = self.inner.lock().unwrap();
guard.tiles_index_map.get(id).copied()
}
pub fn tile_index_has_translucency(&self, tile_index: u32) -> bool {
let guard = self.inner.lock().unwrap();
let Some(id) = guard.tiles_order.get(tile_index as usize) else {
return false;
};
let Some(tile) = guard.tiles_map.get(id) else {
return false;
};
if tile
.frames
.iter()
.any(|frame| frame.chunks_exact(4).any(|px| px[3] < 255))
{
return true;
}
if tile.material_frames.iter().any(|frame| {
frame
.chunks_exact(4)
.any(|px| (px.get(1).copied().unwrap_or(15) & 0x0F) < 15)
}) {
return true;
}
false
}
pub fn get_tile_data(&self, id: Uuid) -> Option<(u32, u32, Vec<u8>)> {
let guard = self.inner.lock().unwrap();
guard.tiles_map.get(&id).map(|tile| {
let frame_data = if tile.frames.is_empty() {
vec![]
} else {
tile.frames[0].clone()
};
(tile.w, tile.h, frame_data)
})
}
pub fn sample_tile_alpha(&self, id: &Uuid, anim_frame: u32, uv: [f32; 2]) -> Option<u8> {
let guard = self.inner.lock().unwrap();
let tile = guard.tiles_map.get(id)?;
let frame_count = tile.frames.len();
if tile.w == 0 || tile.h == 0 || frame_count == 0 {
return None;
}
let frame = &tile.frames[(anim_frame as usize) % frame_count];
let x = ((uv[0].clamp(0.0, 0.9999)) * tile.w as f32).floor() as usize;
let y = ((uv[1].clamp(0.0, 0.9999)) * tile.h as f32).floor() as usize;
let idx = (y * tile.w as usize + x) * 4;
frame.get(idx + 3).copied()
}
pub fn tile_pixel_matches_topleft_rgb(
&self,
id: &Uuid,
anim_frame: u32,
uv: [f32; 2],
) -> Option<bool> {
let guard = self.inner.lock().unwrap();
let tile = guard.tiles_map.get(id)?;
let frame_count = tile.frames.len();
if tile.w == 0 || tile.h == 0 || frame_count == 0 {
return None;
}
let frame = &tile.frames[(anim_frame as usize) % frame_count];
if frame.len() < 4 {
return None;
}
let x = ((uv[0].clamp(0.0, 0.9999)) * tile.w as f32).floor() as usize;
let y = ((uv[1].clamp(0.0, 0.9999)) * tile.h as f32).floor() as usize;
let idx = (y * tile.w as usize + x) * 4;
let px = frame.get(idx..idx + 3)?;
let key = &frame[0..3];
Some(px == key)
}
pub fn gpu_tile_tables(&self) -> AtlasGpuTables {
let guard = self.inner.lock().unwrap();
let mut metas: Vec<AtlasTileMeta> = Vec::new();
let mut frames: Vec<AtlasEntry> = Vec::new();
for id in &guard.tiles_order {
if let Some(rects) = guard.atlas_map.get(id) {
if guard.tiles_index_map.contains_key(id) && !rects.is_empty() {
let first = frames.len() as u32;
frames.extend(rects.iter().cloned());
metas.push(AtlasTileMeta {
first_frame: first,
frame_count: rects.len() as u32,
});
}
}
}
AtlasGpuTables { metas, frames }
}
pub fn remove_tile(&self, id: &Uuid) {
let mut guard = self.inner.lock().unwrap();
guard.tiles_map.remove(id);
guard.tiles_order.retain(|tid| tid != id);
guard.atlas_map.remove(id);
guard.atlas_dirty = true;
guard.layout_dirty = true;
}
pub fn clear(&self) {
let mut guard = self.inner.lock().unwrap();
guard.tiles_map.clear();
guard.tiles_order.clear();
guard.atlas.data.fill(0);
guard.atlas_material.data.fill(0);
guard.atlas_map.clear();
guard.atlas_dirty = true;
guard.layout_dirty = true;
}
pub fn with_tile_mut<R>(&self, id: &Uuid, f: impl FnOnce(&mut Tile) -> R) -> Option<R> {
let mut guard = self.inner.lock().unwrap();
let tile = guard.tiles_map.get_mut(id)?;
let out = f(tile);
guard.atlas_dirty = true;
Some(out)
}
pub fn atlas_pixels(&self) -> Vec<u8> {
let guard = self.inner.lock().unwrap();
guard.atlas.data.clone()
}
pub fn material_atlas_pixels(&self) -> Vec<u8> {
let guard = self.inner.lock().unwrap();
guard.atlas_material.data.clone()
}
pub fn copy_atlas_to_slice(&self, dst: &mut [u8], buf_w: u32, buf_h: u32) {
let guard = self.inner.lock().unwrap();
guard.atlas.copy_to_slice(dst, buf_w, buf_h);
}
pub fn copy_material_atlas_to_slice(&self, dst: &mut [u8], buf_w: u32, buf_h: u32) {
let guard = self.inner.lock().unwrap();
guard.atlas_material.copy_to_slice(dst, buf_w, buf_h);
}
#[cfg(feature = "gpu")]
pub fn upload_to_gpu_with(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
let mut guard = self.inner.lock().unwrap();
if guard.atlas_dirty {
guard.atlas.upload_to_gpu_with(device, queue);
guard.atlas_material.upload_to_gpu_with(device, queue);
guard.atlas_dirty = false;
}
}
#[cfg(feature = "gpu")]
pub fn download_from_gpu_with(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
let mut guard = self.inner.lock().unwrap();
guard.atlas.download_from_gpu_with(device, queue);
guard.atlas_material.download_from_gpu_with(device, queue);
}
#[cfg(feature = "gpu")]
pub fn with_views<R>(&self, f: impl FnOnce(&TextureGPU, &TextureGPU) -> R) -> Option<R> {
let guard = self.inner.lock().unwrap();
let a = guard.atlas.gpu.as_ref()?;
let mat = guard.atlas_material.gpu.as_ref()?;
Some(f(a, mat))
}
#[cfg(feature = "gpu")]
pub fn texture_views(&self) -> Option<(wgpu::TextureView, wgpu::TextureView)> {
self.with_views(|a, m| {
let atlas_view = a
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mat_view = m
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
(atlas_view, mat_view)
})
}
pub fn frame_rect(&self, id: &Uuid, anim_frame: u32) -> Option<AtlasEntry> {
let guard = self.inner.lock().unwrap();
let rects = guard.atlas_map.get(id)?;
if rects.is_empty() {
return None;
}
let idx = (anim_frame as usize) % rects.len();
rects.get(idx).cloned()
}
pub fn ensure_built(&self) -> bool {
let mut guard = self.inner.lock().unwrap();
if guard.layout_dirty {
if guard.auto_size {
auto_resize_atlas_inner(&mut guard);
}
build_atlas_inner(&mut guard);
guard.layout_dirty = false;
return true;
}
if guard.atlas_dirty {
repaint_atlas_pixels_inner(&mut guard);
}
false
}
pub fn sdf_uv4(&self, id: &Uuid, anim_frame: u32) -> Option<[f32; 4]> {
let mut guard = self.inner.lock().unwrap();
if guard.layout_dirty {
build_atlas_inner(&mut guard);
guard.layout_dirty = false;
}
let rects = guard.atlas_map.get(id)?;
if rects.is_empty() {
return None;
}
let idx = (anim_frame as usize) % rects.len();
let rect = rects.get(idx)?;
let w = guard.atlas.width.max(1) as f32;
let h = guard.atlas.height.max(1) as f32;
Some([
rect.x as f32 / w,
rect.y as f32 / h,
rect.w as f32 / w,
rect.h as f32 / h,
])
}
pub fn tiles_map_len(&self) -> usize {
let guard = self.inner.lock().unwrap();
guard.tiles_map.len()
}
pub fn resize(&self, width: u32, height: u32) {
let mut guard = self.inner.lock().unwrap();
if guard.atlas.width == width && guard.atlas.height == height {
return;
}
guard.atlas = Texture::new(width, height);
guard.atlas_material = Texture::new(width, height);
guard.atlas_dirty = true;
guard.layout_dirty = true;
guard.atlas_map.clear();
guard.tiles_index_map.clear();
guard.layout_version = guard.layout_version.wrapping_add(1);
guard.auto_size = false;
}
pub fn inner_arc(&self) -> Arc<Mutex<SharedAtlasInner>> {
Arc::clone(&self.inner)
}
}
fn build_atlas_inner(inner: &mut SharedAtlasInner) {
inner.atlas.data.fill(0);
inner.atlas_material.data.fill(0);
inner.atlas_map.clear();
let estimated_tiles = inner.tiles_order.len();
inner.atlas_map.reserve(estimated_tiles);
inner.tiles_index_map.reserve(estimated_tiles);
let mut pen_x: u32 = 0;
let mut pen_y: u32 = 0;
let mut shelf_h: u32 = 0;
for id in &inner.tiles_order {
let Some(tile) = inner.tiles_map.get(id) else {
continue;
};
let w = tile.w;
let h = tile.h;
if w == 0 || h == 0 {
continue;
}
let packed_w = w.saturating_add(ATLAS_FRAME_PADDING * 2);
let packed_h = h.saturating_add(ATLAS_FRAME_PADDING * 2);
let frames_len = tile.frames.len();
let mat_len = tile.material_frames.len();
let mut rects = Vec::with_capacity(frames_len);
let need_bytes = (w as usize) * (h as usize) * 4;
for f in 0..frames_len {
if pen_x + packed_w > inner.atlas.width {
pen_x = 0;
pen_y = pen_y.saturating_add(shelf_h);
shelf_h = 0;
}
if pen_y + packed_h > inner.atlas.height {
break;
}
shelf_h = shelf_h.max(packed_h);
let frame_owned = alpha_bleed_colors(&tile.frames[f], w, h, 4);
let mat_owned = if f < mat_len {
tile.material_frames[f].clone()
} else {
default_material_frame(need_bytes)
};
blit_rgba_into_with_border(
&mut inner.atlas.data,
inner.atlas.width,
&frame_owned,
w,
h,
pen_x,
pen_y,
ATLAS_FRAME_PADDING,
);
blit_rgba_into_with_border(
&mut inner.atlas_material.data,
inner.atlas_material.width,
&mat_owned,
w,
h,
pen_x,
pen_y,
ATLAS_FRAME_PADDING,
);
rects.push(AtlasEntry {
x: pen_x + ATLAS_FRAME_PADDING,
y: pen_y + ATLAS_FRAME_PADDING,
w,
h,
});
pen_x = pen_x.saturating_add(packed_w);
}
if !rects.is_empty() {
inner.atlas_map.insert(*id, rects);
}
}
inner.tiles_index_map.clear();
for id in &inner.tiles_order {
if inner.atlas_map.contains_key(id) {
let idx = inner.tiles_index_map.len() as u32;
inner.tiles_index_map.insert(*id, idx);
}
}
inner.layout_version = inner.layout_version.wrapping_add(1);
}
fn pack_fits(inner: &SharedAtlasInner, atlas_w: u32, atlas_h: u32) -> bool {
let mut pen_x: u32 = 0;
let mut pen_y: u32 = 0;
let mut shelf_h: u32 = 0;
for id in &inner.tiles_order {
let Some(tile) = inner.tiles_map.get(id) else {
continue;
};
if tile.w == 0 || tile.h == 0 {
continue;
}
let packed_w = tile.w.saturating_add(ATLAS_FRAME_PADDING * 2);
let packed_h = tile.h.saturating_add(ATLAS_FRAME_PADDING * 2);
if packed_w > atlas_w || packed_h > atlas_h {
return false;
}
for _ in 0..tile.frames.len() {
if pen_x + packed_w > atlas_w {
pen_x = 0;
pen_y = pen_y.saturating_add(shelf_h);
shelf_h = 0;
}
if pen_y + packed_h > atlas_h {
return false;
}
shelf_h = shelf_h.max(packed_h);
pen_x = pen_x.saturating_add(packed_w);
}
}
true
}
fn auto_resize_atlas_inner(inner: &mut SharedAtlasInner) {
const MIN_ATLAS_SIDE: u32 = 256;
const MAX_ATLAS_SIDE: u32 = 16384;
let mut max_dim = 1u32;
let mut total_area: u64 = 0;
let mut frame_count: u64 = 0;
for id in &inner.tiles_order {
let Some(tile) = inner.tiles_map.get(id) else {
continue;
};
if tile.w == 0 || tile.h == 0 || tile.frames.is_empty() {
continue;
}
let packed_w = tile.w.saturating_add(ATLAS_FRAME_PADDING * 2);
let packed_h = tile.h.saturating_add(ATLAS_FRAME_PADDING * 2);
max_dim = max_dim.max(packed_w.max(packed_h));
total_area = total_area
.saturating_add((packed_w as u64) * (packed_h as u64) * (tile.frames.len() as u64));
frame_count = frame_count.saturating_add(tile.frames.len() as u64);
}
let mut side = max_dim.max(MIN_ATLAS_SIDE).next_power_of_two();
if total_area > 0 {
let estimated = (total_area as f64).sqrt().ceil() as u32;
side = side.max(estimated.max(MIN_ATLAS_SIDE).next_power_of_two());
}
if frame_count == 0 {
side = MIN_ATLAS_SIDE;
}
side = side.min(MAX_ATLAS_SIDE);
while side < MAX_ATLAS_SIDE && !pack_fits(inner, side, side) {
side = (side.saturating_mul(2)).min(MAX_ATLAS_SIDE);
}
if side != inner.atlas.width || side != inner.atlas.height {
inner.atlas = Texture::new(side, side);
inner.atlas_material = Texture::new(side, side);
inner.atlas_dirty = true;
inner.layout_dirty = true;
inner.atlas_map.clear();
inner.tiles_index_map.clear();
inner.layout_version = inner.layout_version.wrapping_add(1);
}
}
fn repaint_atlas_pixels_inner(inner: &mut SharedAtlasInner) {
inner.atlas.data.fill(0);
inner.atlas_material.data.fill(0);
for id in &inner.tiles_order {
let Some(tile) = inner.tiles_map.get(id) else {
continue;
};
let Some(rects) = inner.atlas_map.get(id) else {
continue;
};
if tile.w == 0 || tile.h == 0 {
continue;
}
let need_bytes = (tile.w as usize) * (tile.h as usize) * 4;
for (f, rect) in rects.iter().enumerate() {
if f >= tile.frames.len() {
break;
}
let frame_owned = alpha_bleed_colors(&tile.frames[f], rect.w, rect.h, 4);
let mat_owned = if f < tile.material_frames.len() {
tile.material_frames[f].clone()
} else {
default_material_frame(need_bytes)
};
blit_rgba_into_with_border(
&mut inner.atlas.data,
inner.atlas.width,
&frame_owned,
rect.w,
rect.h,
rect.x.saturating_sub(ATLAS_FRAME_PADDING),
rect.y.saturating_sub(ATLAS_FRAME_PADDING),
ATLAS_FRAME_PADDING,
);
blit_rgba_into_with_border(
&mut inner.atlas_material.data,
inner.atlas_material.width,
&mat_owned,
rect.w,
rect.h,
rect.x.saturating_sub(ATLAS_FRAME_PADDING),
rect.y.saturating_sub(ATLAS_FRAME_PADDING),
ATLAS_FRAME_PADDING,
);
}
}
}
pub fn default_material_frame(bytes: usize) -> Vec<u8> {
if bytes == 0 {
return Vec::new();
}
let mut v = Vec::with_capacity(bytes);
let pixels = bytes / 4;
for _ in 0..pixels {
v.extend_from_slice(&[7u8, 15u8, 128u8, 128u8]);
}
if v.len() < bytes {
v.resize(bytes, 0);
}
v
}
fn blit_rgba_into(
dst: &mut [u8],
atlas_w: u32,
src: &[u8],
src_w: u32,
src_h: u32,
dst_x: u32,
dst_y: u32,
) {
if src.is_empty() {
return;
}
let atlas_w = atlas_w as usize;
let src_w = src_w as usize;
let src_h = src_h as usize;
let dx = dst_x as usize;
let dy = dst_y as usize;
for row in 0..src_h {
let src_off = row * src_w * 4;
let dst_off = ((dy + row) * atlas_w + dx) * 4;
let src_slice = &src[src_off..src_off + src_w * 4];
let dst_slice = &mut dst[dst_off..dst_off + src_w * 4];
dst_slice.copy_from_slice(src_slice);
}
}
fn alpha_bleed_colors(src: &[u8], w: u32, h: u32, iterations: u32) -> Vec<u8> {
if src.is_empty() || w == 0 || h == 0 {
return src.to_vec();
}
let mut out = src.to_vec();
let mut prev = out.clone();
let w_us = w as usize;
let h_us = h as usize;
let iters = iterations.max(1);
for _ in 0..iters {
prev.copy_from_slice(&out);
for y in 0..h_us {
for x in 0..w_us {
let i = (y * w_us + x) * 4;
if prev[i + 3] != 0 {
continue;
}
let mut rs: u32 = 0;
let mut gs: u32 = 0;
let mut bs: u32 = 0;
let mut n: u32 = 0;
let y0 = y.saturating_sub(1);
let y1 = (y + 1).min(h_us - 1);
let x0 = x.saturating_sub(1);
let x1 = (x + 1).min(w_us - 1);
for ny in y0..=y1 {
for nx in x0..=x1 {
if nx == x && ny == y {
continue;
}
let ni = (ny * w_us + nx) * 4;
if prev[ni + 3] > 0 {
rs += prev[ni] as u32;
gs += prev[ni + 1] as u32;
bs += prev[ni + 2] as u32;
n += 1;
}
}
}
if n > 0 {
out[i] = (rs / n) as u8;
out[i + 1] = (gs / n) as u8;
out[i + 2] = (bs / n) as u8;
}
}
}
}
out
}
fn blit_rgba_into_with_border(
dst: &mut [u8],
atlas_w: u32,
src: &[u8],
src_w: u32,
src_h: u32,
dst_x: u32,
dst_y: u32,
border: u32,
) {
if src.is_empty() || src_w == 0 || src_h == 0 {
return;
}
blit_rgba_into(
dst,
atlas_w,
src,
src_w,
src_h,
dst_x + border,
dst_y + border,
);
if border == 0 {
return;
}
let atlas_w_us = atlas_w as usize;
let src_w_us = src_w as usize;
let src_h_us = src_h as usize;
let border_us = border as usize;
let base_x = dst_x as usize;
let base_y = dst_y as usize;
for row in 0..src_h_us {
let src_row_off = row * src_w_us * 4;
let first_px = &src[src_row_off..src_row_off + 4];
let last_px = &src[src_row_off + (src_w_us - 1) * 4..src_row_off + src_w_us * 4];
let y = base_y + border_us + row;
for b in 0..border_us {
let lx = base_x + b;
let rx = base_x + border_us + src_w_us + b;
let l_off = (y * atlas_w_us + lx) * 4;
let r_off = (y * atlas_w_us + rx) * 4;
dst[l_off..l_off + 4].copy_from_slice(first_px);
dst[r_off..r_off + 4].copy_from_slice(last_px);
}
}
let padded_w = src_w_us + border_us * 2;
for b in 0..border_us {
let src_top_y = base_y + border_us;
let src_bot_y = base_y + border_us + src_h_us - 1;
let top_y = base_y + b;
let bot_y = base_y + border_us + src_h_us + b;
for x in 0..padded_w {
let sx = base_x + x;
let top_src_off = (src_top_y * atlas_w_us + sx) * 4;
let bot_src_off = (src_bot_y * atlas_w_us + sx) * 4;
let top_dst_off = (top_y * atlas_w_us + sx) * 4;
let bot_dst_off = (bot_y * atlas_w_us + sx) * 4;
let top_px = [
dst[top_src_off],
dst[top_src_off + 1],
dst[top_src_off + 2],
dst[top_src_off + 3],
];
let bot_px = [
dst[bot_src_off],
dst[bot_src_off + 1],
dst[bot_src_off + 2],
dst[bot_src_off + 3],
];
dst[top_dst_off..top_dst_off + 4].copy_from_slice(&top_px);
dst[bot_dst_off..bot_dst_off + 4].copy_from_slice(&bot_px);
}
}
}