Skip to main content

beamterm_core/gl/
texture.rs

1use beamterm_data::FontAtlasData;
2use glow::HasContext;
3
4use crate::error::Error;
5
6/// Number of glyphs stored per texture layer (1x32 vertical grid)
7const GLYPHS_PER_LAYER: i32 = 32;
8
9/// Platform-agnostic rasterized glyph data for texture upload.
10#[derive(Debug, Clone)]
11pub struct RasterizedGlyph {
12    pub pixels: Vec<u8>,
13    pub width: u32,
14    pub height: u32,
15}
16
17impl RasterizedGlyph {
18    pub fn new(pixels: Vec<u8>, width: u32, height: u32) -> Self {
19        Self { pixels, width, height }
20    }
21
22    /// Returns true if the glyph produced no visible pixels.
23    pub fn is_empty(&self) -> bool {
24        self.pixels
25            .iter()
26            .skip(3)
27            .step_by(4)
28            .all(|&a| a == 0)
29    }
30}
31
32#[derive(Debug)]
33pub struct Texture {
34    gl_texture: glow::Texture,
35    pub format: u32,
36    /// Texture dimensions (width, height, layers)
37    dimensions: (i32, i32, i32),
38}
39
40impl Texture {
41    pub fn from_font_atlas_data(
42        gl: &glow::Context,
43        format: u32,
44        atlas: &FontAtlasData,
45    ) -> Result<Self, Error> {
46        let (width, height, layers) = atlas.texture_dimensions;
47
48        // prepare texture
49        let gl_texture = unsafe { gl.create_texture() }.map_err(Error::texture_creation_failed)?;
50        unsafe {
51            gl.bind_texture(glow::TEXTURE_2D_ARRAY, Some(gl_texture));
52            gl.tex_storage_3d(
53                glow::TEXTURE_2D_ARRAY,
54                1,
55                glow::RGBA8,
56                width,
57                height,
58                layers,
59            );
60
61            // upload the texture data; convert to u8 array
62            gl.tex_sub_image_3d(
63                glow::TEXTURE_2D_ARRAY,
64                0, // level
65                0,
66                0,
67                0, // offset
68                width,
69                height,
70                layers, // texture size
71                glow::RGBA,
72                glow::UNSIGNED_BYTE,
73                glow::PixelUnpackData::Slice(Some(&atlas.texture_data)),
74            );
75        }
76
77        Self::setup_sampling(gl);
78
79        let (width, height, layers) = atlas.texture_dimensions;
80        Ok(Self {
81            gl_texture,
82            format,
83            dimensions: (width, height, layers),
84        })
85    }
86
87    /// Creates an empty texture array for dynamic glyph rasterization.
88    ///
89    /// Allocates a fixed-size 2D texture array and initializes all layers to transparent
90    /// black (RGBA 0,0,0,0).
91    ///
92    /// **LRU eviction**: When the glyph cache evicts old entries, the texture slots
93    /// are reused. The new glyph completely overwrites the slot, so no explicit
94    /// clearing is needed on eviction.
95    ///
96    /// # Arguments
97    /// * `gl` - GL context
98    /// * `format` - Texture format
99    /// * `cell_size` - (width, height) of each glyph cell in pixels
100    /// * `initial_layers` - Number of texture layers to allocate initially
101    pub fn for_dynamic_font_atlas(
102        gl: &glow::Context,
103        format: u32,
104        cell_size: (i32, i32),
105        initial_layers: i32,
106    ) -> Result<Self, Error> {
107        let (cell_w, cell_h) = cell_size;
108
109        // Each layer holds 32 glyphs in a 1x32 vertical grid
110        // Match static atlas layout: single cell width per layer
111        // (double-width glyphs like emoji use two consecutive glyph slots)
112        let width = cell_w;
113        let height = cell_h * GLYPHS_PER_LAYER;
114
115        let gl_texture = unsafe { gl.create_texture() }.map_err(Error::texture_creation_failed)?;
116
117        unsafe {
118            gl.bind_texture(glow::TEXTURE_2D_ARRAY, Some(gl_texture));
119            gl.tex_storage_3d(
120                glow::TEXTURE_2D_ARRAY,
121                1, // mip levels
122                glow::RGBA8,
123                width,
124                height,
125                initial_layers,
126            );
127
128            // Initialize all layers to transparent black to prevent undefined memory artifacts.
129            // See doc comment above for rationale. We upload all layers in a single call to
130            // minimize GPU state changes (1 call vs 128 per-layer calls).
131            let empty_data = vec![0u8; (width * height * initial_layers * 4) as usize];
132            gl.tex_sub_image_3d(
133                glow::TEXTURE_2D_ARRAY,
134                0, // mip level
135                0, // x offset
136                0, // y offset
137                0, // z offset (first layer)
138                width,
139                height,
140                initial_layers, // all layers at once
141                glow::RGBA,
142                glow::UNSIGNED_BYTE,
143                glow::PixelUnpackData::Slice(Some(&empty_data)),
144            );
145        }
146
147        Self::setup_sampling(gl);
148
149        Ok(Self {
150            gl_texture,
151            format,
152            dimensions: (width, height, initial_layers),
153        })
154    }
155
156    /// Uploads a rasterized glyph to the texture at the position determined by its ID.
157    ///
158    /// Glyph positions follow the layout: layer = id / 32, y = (id % 32) * cell_height
159    pub fn upload_glyph(
160        &self,
161        gl: &glow::Context,
162        glyph_id: u16,
163        padded_cell_size: (i32, i32),
164        rasterized: &RasterizedGlyph,
165    ) -> Result<(), Error> {
166        let (_cell_w, cell_h) = padded_cell_size;
167
168        // Calculate position in texture array
169        let layer = (glyph_id as i32) / GLYPHS_PER_LAYER;
170        let glyph_index = (glyph_id as i32) % GLYPHS_PER_LAYER;
171        let y_offset = glyph_index * cell_h;
172
173        if layer >= self.dimensions.2 {
174            return Err(Error::texture_creation_failed(format_args!(
175                "glyph id {glyph_id} exceeds texture layer count {}",
176                self.dimensions.2
177            )));
178        }
179
180        unsafe {
181            gl.bind_texture(glow::TEXTURE_2D_ARRAY, Some(self.gl_texture));
182
183            gl.tex_sub_image_3d(
184                glow::TEXTURE_2D_ARRAY,
185                0, // level
186                0,
187                y_offset,
188                layer, // x, y, z offset
189                rasterized.width as i32,
190                rasterized.height as i32,
191                1, // depth (single layer)
192                glow::RGBA,
193                glow::UNSIGNED_BYTE,
194                glow::PixelUnpackData::Slice(Some(&rasterized.pixels)),
195            );
196        }
197
198        Ok(())
199    }
200
201    /// Returns the texture dimensions (width, height, layers)
202    pub fn dimensions(&self) -> (i32, i32, i32) {
203        self.dimensions
204    }
205
206    pub fn bind(&self, gl: &glow::Context) {
207        unsafe {
208            gl.bind_texture(glow::TEXTURE_2D_ARRAY, Some(self.gl_texture));
209        }
210    }
211
212    pub fn delete(&self, gl: &glow::Context) {
213        unsafe {
214            gl.delete_texture(self.gl_texture);
215        }
216    }
217
218    fn setup_sampling(gl: &glow::Context) {
219        unsafe {
220            gl.tex_parameter_i32(
221                glow::TEXTURE_2D_ARRAY,
222                glow::TEXTURE_MIN_FILTER,
223                glow::NEAREST as i32,
224            );
225            gl.tex_parameter_i32(
226                glow::TEXTURE_2D_ARRAY,
227                glow::TEXTURE_MAG_FILTER,
228                glow::NEAREST as i32,
229            );
230            gl.tex_parameter_i32(
231                glow::TEXTURE_2D_ARRAY,
232                glow::TEXTURE_WRAP_S,
233                glow::CLAMP_TO_EDGE as i32,
234            );
235            gl.tex_parameter_i32(
236                glow::TEXTURE_2D_ARRAY,
237                glow::TEXTURE_WRAP_T,
238                glow::CLAMP_TO_EDGE as i32,
239            );
240        }
241    }
242}