//! Glyph atlas: guillotiere allocator over a single R8Unorm wgpu texture. use guillotiere::{AllocId, Allocation, AtlasAllocator, size2}; pub(crate) struct GlyphAtlas { pub(crate) texture: wgpu::Texture, pub(crate) view: wgpu::TextureView, pub(crate) size: u32, /// UV coordinate of a pre-uploaded white pixel; solid-color quads sample /// this to bypass the glyph atlas without needing a second pipeline. pub(crate) solid_uv: [f32; 2], allocator: AtlasAllocator, } #[derive(Copy, Clone)] pub(crate) struct AtlasSlot { pub(crate) _id: AllocId, pub(crate) uv_min: [f32; 2], pub(crate) uv_max: [f32; 2], pub(crate) px: [u32; 2], } impl GlyphAtlas { pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue, size: u32) -> Self { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("shop.glyph_atlas"), size: wgpu::Extent3d { width: size, height: size, 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, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let mut allocator = AtlasAllocator::new(size2(size as i32, size as i32)); // Reserve a 4x4 white block at the top-left and record its center UV. // Solid-color quads sample this to reuse the glyph pipeline. let solid_alloc = allocator .allocate(size2(4, 4)) .expect("atlas must fit a 4x4 solid reservation"); let sx = solid_alloc.rectangle.min.x as u32; let sy = solid_alloc.rectangle.min.y as u32; queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &texture, mip_level: 0, origin: wgpu::Origin3d { x: sx, y: sy, z: 0 }, aspect: wgpu::TextureAspect::All, }, &[255u8; 16], wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4), rows_per_image: Some(4), }, wgpu::Extent3d { width: 4, height: 4, depth_or_array_layers: 1, }, ); let s = size as f32; let solid_uv = [(sx as f32 + 2.0) / s, (sy as f32 + 2.0) / s]; Self { texture, view, size, solid_uv, allocator, } } /// Reserve a rectangle and upload the coverage bitmap. Returns `None` if /// the atlas is full (caller should evict or grow — MVP just skips glyph). pub(crate) fn upload( &mut self, queue: &wgpu::Queue, width: u32, height: u32, bitmap: &[u8], ) -> Option { if width == 0 || height == 0 { return Some(AtlasSlot { _id: AllocId::deserialize(0), uv_min: [0.0, 0.0], uv_max: [0.0, 0.0], px: [0, 0], }); } let Allocation { id, rectangle } = self .allocator .allocate(size2(width as i32 + 1, height as i32 + 1))?; let x = rectangle.min.x as u32; let y = rectangle.min.y as u32; queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &self.texture, mip_level: 0, origin: wgpu::Origin3d { x, y, z: 0 }, aspect: wgpu::TextureAspect::All, }, bitmap, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(width), rows_per_image: Some(height), }, wgpu::Extent3d { width, height, depth_or_array_layers: 1, }, ); let s = self.size as f32; Some(AtlasSlot { _id: id, uv_min: [x as f32 / s, y as f32 / s], uv_max: [(x + width) as f32 / s, (y + height) as f32 / s], px: [width, height], }) } }