# Atlas Texture Refactor: Array of Textures ## Problem When adding a new layer to the glyph atlas, the current implementation creates a new texture array with N+1 layers and copies all N existing layers to it. This causes performance issues that scale with the number of layers: - Layer 1→2: Copy 256MB (8192×8192×4 bytes) - Layer 2→3: Copy 512MB - Layer 3→4: Copy 768MB - etc. Observed frame times when adding layers: - Layer 1 added: 14.4ms - Layer 2 added: 21.9ms - Layer 3 added: 34.2ms ## Solution Instead of using a single `texture_2d_array` that must be reallocated and copied when growing, use a **`Vec` of separate 2D textures**. When a new layer is needed, simply create a new texture and add it to the vector. No copying of existing texture data is required. The bind group must be recreated to include the new texture, but this is a cheap CPU-side operation (just creating metadata/pointers). ## Current Implementation ### Rust (renderer.rs) **Struct fields:** ```rust atlas_texture: wgpu::Texture, // Single texture array atlas_view: wgpu::TextureView, // Single view atlas_num_layers: u32, // Number of layers in the array atlas_current_layer: u32, // Current layer being written to ``` **Bind group layout (binding 0):** ```rust ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2Array, // ... }, count: None, // Single texture ``` **Bind group entry:** ```rust wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&atlas_view), } ``` ### WGSL (glyph_shader.wgsl) **Texture declaration:** ```wgsl @group(0) @binding(0) var atlas_texture: texture_2d_array; ``` **Sampling:** ```wgsl let sample = textureSample(atlas_texture, atlas_sampler, uv, layer_index); ``` ## New Implementation ### Rust (renderer.rs) **Struct fields:** ```rust atlas_textures: Vec, // Vector of separate textures atlas_views: Vec, // Vector of views (one per texture) atlas_current_layer: u32, // Current layer being written to // atlas_num_layers removed - use atlas_textures.len() instead ``` **Bind group layout (binding 0):** ```rust ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, // Changed from D2Array // ... }, count: Some(NonZeroU32::new(MAX_ATLAS_LAYERS).unwrap()), // Array of textures ``` **Bind group entry:** ```rust wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureViewArray(&atlas_view_refs), } ``` Where `atlas_view_refs` is a `Vec<&wgpu::TextureView>` containing references to all views. **Note:** wgpu requires the bind group to have exactly `count` textures. We'll need to either: 1. Pre-create dummy textures to fill unused slots, OR 2. Recreate the bind group layout when adding textures (more complex) Option 1 is simpler: create small 1x1 dummy textures for unused slots up to MAX_ATLAS_LAYERS. ### WGSL (glyph_shader.wgsl) **Texture declaration:** ```wgsl @group(0) @binding(0) var atlas_textures: binding_array>; ``` **Sampling:** ```wgsl let sample = textureSample(atlas_textures[layer_index], atlas_sampler, uv); ``` **Note:** `binding_array` requires the `binding_array` feature in wgpu, which should be enabled by default on most backends. ## Implementation Steps ### Step 1: Update Struct Fields In `renderer.rs`, change: ```rust // Old atlas_texture: wgpu::Texture, atlas_view: wgpu::TextureView, atlas_num_layers: u32, // New atlas_textures: Vec, atlas_views: Vec, ``` ### Step 2: Create Helper for New Atlas Layer ```rust fn create_atlas_layer(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView) { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Glyph Atlas Layer"), size: wgpu::Extent3d { width: ATLAS_SIZE, height: ATLAS_SIZE, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); (texture, view) } ``` ### Step 3: Update Bind Group Layout ```rust use std::num::NonZeroU32; let glyph_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Glyph Bind Group Layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: false }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: Some(NonZeroU32::new(MAX_ATLAS_LAYERS).unwrap()), }, // ... sampler entry unchanged ], }); ``` ### Step 4: Initialize with Dummy Textures At initialization, create one real texture and fill the rest with 1x1 dummy textures: ```rust fn create_dummy_texture(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView) { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Dummy Atlas Texture"), size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); (texture, view) } // In new(): let mut atlas_textures = Vec::with_capacity(MAX_ATLAS_LAYERS as usize); let mut atlas_views = Vec::with_capacity(MAX_ATLAS_LAYERS as usize); // First texture is real let (tex, view) = create_atlas_layer(&device); atlas_textures.push(tex); atlas_views.push(view); // Fill rest with dummies for _ in 1..MAX_ATLAS_LAYERS { let (tex, view) = create_dummy_texture(&device); atlas_textures.push(tex); atlas_views.push(view); } ``` ### Step 5: Update Bind Group Creation ```rust fn create_atlas_bind_group(&self) -> wgpu::BindGroup { let view_refs: Vec<&wgpu::TextureView> = self.atlas_views.iter().collect(); self.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Glyph Bind Group"), layout: &self.glyph_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureViewArray(&view_refs), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.atlas_sampler), }, ], }) } ``` ### Step 6: Update add_atlas_layer / ensure_atlas_layer_capacity ```rust fn ensure_atlas_layer_capacity(&mut self, target_layer: u32) { // Count real layers (non-dummy) let current_real_layers = self.atlas_current_layer + 1; if target_layer < current_real_layers { return; // Already have this layer } if target_layer >= MAX_ATLAS_LAYERS { log::error!("Atlas layer limit reached"); return; } log::info!("Adding atlas layer {}", target_layer); // Create new real texture let (texture, view) = Self::create_atlas_layer(&self.device); // Replace dummy at this index with real texture self.atlas_textures[target_layer as usize] = texture; self.atlas_views[target_layer as usize] = view; // Recreate bind group with updated view self.glyph_bind_group = self.create_atlas_bind_group(); } ``` ### Step 7: Update upload_cell_canvas_to_atlas Change texture reference from `&self.atlas_texture` to `&self.atlas_textures[layer as usize]`: ```rust self.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &self.atlas_textures[layer as usize], // Changed mip_level: 0, origin: wgpu::Origin3d { x: self.atlas_cursor_x, y: self.atlas_cursor_y, z: 0, // Always 0 now, layer is selected by texture index }, aspect: wgpu::TextureAspect::All, }, // ... rest unchanged ); ``` ### Step 8: Update Shader In `glyph_shader.wgsl`: ```wgsl // Old @group(0) @binding(0) var atlas_texture: texture_2d_array; // New @group(0) @binding(0) var atlas_textures: binding_array>; ``` Update all `textureSample` calls: ```wgsl // Old let sample = textureSample(atlas_texture, atlas_sampler, uv, layer_index); // New let sample = textureSample(atlas_textures[layer_index], atlas_sampler, uv); ``` **Locations to update in glyph_shader.wgsl:** - Line 91: Declaration - Line 106: `fs_main` sampling - Line 700: Cursor sprite sampling in `fs_cell` - Line 723: Glyph sampling in `fs_cell` - Line 747: Underline sampling in `fs_cell` - Line 761: Strikethrough sampling in `fs_cell` ### Step 9: Update statusline_shader.wgsl The statusline shader also uses the atlas. Check and update similarly. ### Step 10: Update Other References Search for all uses of: - `atlas_texture` - `atlas_view` - `atlas_num_layers` - `D2Array` And update accordingly. ## Testing 1. Build and run: `cargo build && cargo run` 2. Verify glyphs render correctly 3. Use terminal heavily to trigger layer additions 4. Check logs for "Adding atlas layer" messages 5. Verify no slow frame warnings during layer addition 6. Test with emoji (color glyphs) to ensure they still work ## Rollback Plan If issues arise, the changes can be reverted by: 1. Restoring `texture_2d_array` in shaders 2. Restoring single `atlas_texture`/`atlas_view` in Rust 3. Restoring the copy-based layer addition ## References - wgpu binding arrays: https://docs.rs/wgpu/latest/wgpu/enum.BindingResource.html#variant.TextureViewArray - WGSL binding_array: https://www.w3.org/TR/WGSL/#binding-array - Kitty's approach: `/tmp/kitty/kitty/shaders.c` (uses `GL_TEXTURE_2D_ARRAY` with `glCopyImageSubData`)