Skip to main content

densa_render/
atlas.rs

1use densa_text::{AtlasUpdate, GlyphUpload};
2use std::fmt;
3
4/// Why the GPU atlas could not follow the glyph cache.
5///
6/// Every variant means the texture no longer mirrors the cache (a bug or an unsupported
7/// device). `GpuRenderer` latches the first one: it then draws nothing and keeps returning it.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum AtlasError {
11    /// The cache grew beyond the device's 2D texture limit.
12    TooLarge {
13        /// Requested width in texels.
14        width: u32,
15        /// Requested height in texels.
16        height: u32,
17        /// `max_texture_dimension_2d` of the device.
18        limit: u32,
19    },
20    /// A write does not lie inside the texture, or its pixel count does not match its size.
21    BadWrite {
22        /// The write's `(x, y, width, height)`.
23        rect: (u32, u32, u32, u32),
24        /// Bytes supplied.
25        bytes: usize,
26        /// Current texture `(width, height)`.
27        atlas: (u32, u32),
28    },
29    /// An update kind this renderer does not know (from a newer `densa-text`).
30    UnsupportedUpdate,
31}
32
33impl fmt::Display for AtlasError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::TooLarge {
37                width,
38                height,
39                limit,
40            } => write!(
41                f,
42                "glyph atlas of {width}x{height} exceeds the device's texture limit {limit}"
43            ),
44            Self::BadWrite { rect, bytes, atlas } => write!(
45                f,
46                "glyph write {rect:?} with {bytes} bytes does not fit the {atlas:?} atlas"
47            ),
48            Self::UnsupportedUpdate => f.write_str("unsupported glyph atlas update"),
49        }
50    }
51}
52
53impl std::error::Error for AtlasError {}
54
55/// The R8 coverage texture mirroring a `densa_text::GlyphCache`.
56#[derive(Debug)]
57pub(crate) struct GpuAtlas {
58    texture: wgpu::Texture,
59    /// Increments whenever `texture` is replaced.
60    version: u64,
61}
62
63impl GpuAtlas {
64    /// A 1x1 placeholder until the cache's first [`AtlasUpdate::Resize`] arrives.
65    pub(crate) fn new(device: &wgpu::Device) -> Self {
66        Self {
67            texture: create_texture(device, 1, 1),
68            version: 0,
69        }
70    }
71
72    /// Changes whenever the texture is replaced: views and bind groups of an older version
73    /// are stale.
74    pub(crate) fn version(&self) -> u64 {
75        self.version
76    }
77
78    pub(crate) fn size(&self) -> (u32, u32) {
79        (self.texture.width(), self.texture.height())
80    }
81
82    pub(crate) fn view(&self) -> wgpu::TextureView {
83        self.texture
84            .create_view(&wgpu::TextureViewDescriptor::default())
85    }
86
87    /// Applies `updates` in order; returns how many writes were applied.
88    pub(crate) fn apply(
89        &mut self,
90        device: &wgpu::Device,
91        queue: &wgpu::Queue,
92        updates: impl IntoIterator<Item = AtlasUpdate>,
93    ) -> Result<usize, AtlasError> {
94        let mut writes = 0;
95        for update in updates {
96            match update {
97                AtlasUpdate::Resize { width, height } => {
98                    self.grow(device, queue, width, height)?;
99                }
100                AtlasUpdate::Write(upload) => {
101                    self.write(queue, &upload)?;
102                    writes += 1;
103                }
104                _ => return Err(AtlasError::UnsupportedUpdate),
105            }
106        }
107        Ok(writes)
108    }
109
110    /// Makes the texture at least `width` x `height`, replacing it by a larger one holding
111    /// the same texels at the same place. Never shrinks: a smaller request (a fresh cache
112    /// attached to a grown texture) is satisfied already, since rects are absolute texels.
113    fn grow(
114        &mut self,
115        device: &wgpu::Device,
116        queue: &wgpu::Queue,
117        width: u32,
118        height: u32,
119    ) -> Result<(), AtlasError> {
120        let current = self.size();
121        let (width, height) = (width.max(current.0), height.max(current.1));
122        if (width, height) == current {
123            return Ok(());
124        }
125        let limit = device.limits().max_texture_dimension_2d;
126        if width > limit || height > limit {
127            return Err(AtlasError::TooLarge {
128                width,
129                height,
130                limit,
131            });
132        }
133
134        let grown = create_texture(device, width, height);
135        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
136            label: Some("densa atlas grow"),
137        });
138        encoder.copy_texture_to_texture(
139            self.texture.as_image_copy(),
140            grown.as_image_copy(),
141            self.texture.size(),
142        );
143        // Submitted now: writes already queued for the old texture run first (queue writes
144        // execute before the next submission), and writes after this target the new one.
145        queue.submit([encoder.finish()]);
146        self.texture = grown;
147        self.version += 1;
148        Ok(())
149    }
150
151    fn write(&self, queue: &wgpu::Queue, upload: &GlyphUpload) -> Result<(), AtlasError> {
152        let (width, height) = self.size();
153        let inside = upload
154            .x
155            .checked_add(upload.width)
156            .is_some_and(|r| r <= width)
157            && upload
158                .y
159                .checked_add(upload.height)
160                .is_some_and(|b| b <= height);
161        let complete =
162            upload.coverage.len() as u64 == u64::from(upload.width) * u64::from(upload.height);
163        if !inside || !complete {
164            return Err(AtlasError::BadWrite {
165                rect: (upload.x, upload.y, upload.width, upload.height),
166                bytes: upload.coverage.len(),
167                atlas: (width, height),
168            });
169        }
170        if upload.width == 0 || upload.height == 0 {
171            return Ok(());
172        }
173        queue.write_texture(
174            wgpu::TexelCopyTextureInfo {
175                texture: &self.texture,
176                mip_level: 0,
177                origin: wgpu::Origin3d {
178                    x: upload.x,
179                    y: upload.y,
180                    z: 0,
181                },
182                aspect: wgpu::TextureAspect::All,
183            },
184            &upload.coverage,
185            wgpu::TexelCopyBufferLayout {
186                offset: 0,
187                bytes_per_row: Some(upload.width),
188                rows_per_image: Some(upload.height),
189            },
190            wgpu::Extent3d {
191                width: upload.width,
192                height: upload.height,
193                depth_or_array_layers: 1,
194            },
195        );
196        Ok(())
197    }
198}
199
200/// An R8 coverage texture; wgpu zero-initializes it, so unwritten texels are transparent.
201fn create_texture(device: &wgpu::Device, width: u32, height: u32) -> wgpu::Texture {
202    device.create_texture(&wgpu::TextureDescriptor {
203        label: Some("densa glyph atlas"),
204        size: wgpu::Extent3d {
205            width,
206            height,
207            depth_or_array_layers: 1,
208        },
209        mip_level_count: 1,
210        sample_count: 1,
211        dimension: wgpu::TextureDimension::D2,
212        format: wgpu::TextureFormat::R8Unorm,
213        usage: wgpu::TextureUsages::TEXTURE_BINDING
214            | wgpu::TextureUsages::COPY_DST
215            | wgpu::TextureUsages::COPY_SRC,
216        view_formats: &[],
217    })
218}