use densa_text::{AtlasUpdate, GlyphUpload};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AtlasError {
TooLarge {
width: u32,
height: u32,
limit: u32,
},
BadWrite {
rect: (u32, u32, u32, u32),
bytes: usize,
atlas: (u32, u32),
},
UnsupportedUpdate,
}
impl fmt::Display for AtlasError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooLarge {
width,
height,
limit,
} => write!(
f,
"glyph atlas of {width}x{height} exceeds the device's texture limit {limit}"
),
Self::BadWrite { rect, bytes, atlas } => write!(
f,
"glyph write {rect:?} with {bytes} bytes does not fit the {atlas:?} atlas"
),
Self::UnsupportedUpdate => f.write_str("unsupported glyph atlas update"),
}
}
}
impl std::error::Error for AtlasError {}
#[derive(Debug)]
pub(crate) struct GpuAtlas {
texture: wgpu::Texture,
version: u64,
}
impl GpuAtlas {
pub(crate) fn new(device: &wgpu::Device) -> Self {
Self {
texture: create_texture(device, 1, 1),
version: 0,
}
}
pub(crate) fn version(&self) -> u64 {
self.version
}
pub(crate) fn size(&self) -> (u32, u32) {
(self.texture.width(), self.texture.height())
}
pub(crate) fn view(&self) -> wgpu::TextureView {
self.texture
.create_view(&wgpu::TextureViewDescriptor::default())
}
pub(crate) fn apply(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
updates: impl IntoIterator<Item = AtlasUpdate>,
) -> Result<usize, AtlasError> {
let mut writes = 0;
for update in updates {
match update {
AtlasUpdate::Resize { width, height } => {
self.grow(device, queue, width, height)?;
}
AtlasUpdate::Write(upload) => {
self.write(queue, &upload)?;
writes += 1;
}
_ => return Err(AtlasError::UnsupportedUpdate),
}
}
Ok(writes)
}
fn grow(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
width: u32,
height: u32,
) -> Result<(), AtlasError> {
let current = self.size();
let (width, height) = (width.max(current.0), height.max(current.1));
if (width, height) == current {
return Ok(());
}
let limit = device.limits().max_texture_dimension_2d;
if width > limit || height > limit {
return Err(AtlasError::TooLarge {
width,
height,
limit,
});
}
let grown = create_texture(device, width, height);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("densa atlas grow"),
});
encoder.copy_texture_to_texture(
self.texture.as_image_copy(),
grown.as_image_copy(),
self.texture.size(),
);
queue.submit([encoder.finish()]);
self.texture = grown;
self.version += 1;
Ok(())
}
fn write(&self, queue: &wgpu::Queue, upload: &GlyphUpload) -> Result<(), AtlasError> {
let (width, height) = self.size();
let inside = upload
.x
.checked_add(upload.width)
.is_some_and(|r| r <= width)
&& upload
.y
.checked_add(upload.height)
.is_some_and(|b| b <= height);
let complete =
upload.coverage.len() as u64 == u64::from(upload.width) * u64::from(upload.height);
if !inside || !complete {
return Err(AtlasError::BadWrite {
rect: (upload.x, upload.y, upload.width, upload.height),
bytes: upload.coverage.len(),
atlas: (width, height),
});
}
if upload.width == 0 || upload.height == 0 {
return Ok(());
}
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: upload.x,
y: upload.y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
&upload.coverage,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(upload.width),
rows_per_image: Some(upload.height),
},
wgpu::Extent3d {
width: upload.width,
height: upload.height,
depth_or_array_layers: 1,
},
);
Ok(())
}
}
fn create_texture(device: &wgpu::Device, width: u32, height: u32) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("densa glyph atlas"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
})
}