textslabs/
setup.rs

1use crate::*;
2
3pub(crate) const INITIAL_BUFFER_SIZE: u64 = 4096;
4
5
6const ATLAS_BIND_GROUP_LAYOUT: BindGroupLayoutDescriptor = wgpu::BindGroupLayoutDescriptor {
7    entries: &[
8        BindGroupLayoutEntry {
9            binding: 0,
10            visibility: ShaderStages::VERTEX.union(ShaderStages::FRAGMENT),
11            ty: BindingType::Texture {
12                multisampled: false,
13                view_dimension: TextureViewDimension::D2,
14                sample_type: TextureSampleType::Float { filterable: true },
15            },
16            count: None,
17        },
18        BindGroupLayoutEntry {
19            binding: 1,
20            visibility: ShaderStages::FRAGMENT,
21            ty: BindingType::Sampler(SamplerBindingType::Filtering),
22            count: None,
23        },
24    ],
25    label: Some("atlas bind group layout"),
26};
27
28/// Configuration parameters for the text renderer.
29pub struct TextRendererParams {
30    /// Size of texture atlas pages used for glyph caching.
31    pub atlas_page_size: AtlasPageSize,
32}
33impl Default for TextRendererParams {
34    fn default() -> Self {
35        // 2048 is guaranteed to work everywhere that webgpu supports, and it seems both small enough that it's fine to allocate it upfront even if a smaller one would have been fine, and big enough that even on gpus that could hold 8k textures, I don't feel too bad about using multiple 2k pages instead of a single big 8k one
36        // Ideally you'd still with small pages and grow them until the max texture dim, but having cache eviction, multiple pages, AND page growing seems a bit too much for now
37        let atlas_page_size = AtlasPageSize::DownlevelWrbgl2Max; // 2048
38        Self { atlas_page_size }
39    }
40}
41/// Determines the size of texture atlas pages for glyph storage.
42pub enum AtlasPageSize {
43    /// Fixed size in pixels.
44    Flat(u32),
45    /// Use the current device's maximum texture size.
46    CurrentDeviceMax,
47    /// Use WebGL2 downlevel maximum (2048px).
48    DownlevelWrbgl2Max,
49    /// Use general downlevel maximum (2048px).
50    DownlevelMax,
51    /// Use WGPU's default maximum (8192px).
52    WgpuMax,
53}
54impl AtlasPageSize {
55    fn size(self, device: &Device) -> u32 {
56        match self {
57            AtlasPageSize::Flat(i) => i,
58            AtlasPageSize::DownlevelWrbgl2Max => Limits::downlevel_defaults().max_texture_dimension_2d,
59            AtlasPageSize::DownlevelMax => Limits::downlevel_webgl2_defaults().max_texture_dimension_2d,
60            AtlasPageSize::WgpuMax => Limits::default().max_texture_dimension_2d,
61            AtlasPageSize::CurrentDeviceMax => device.limits().max_texture_dimension_2d,
62        }
63    }
64}
65
66fn create_vertex_buffer(device: &Device, size: u64) -> Buffer {
67    device.create_buffer(&BufferDescriptor {
68        label: Some("shared vertex buffer"),
69        size,
70        usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
71        mapped_at_creation: false,
72    })
73}
74
75impl ContextlessTextRenderer {
76    pub fn new_with_params(
77        device: &Device,
78        _queue: &Queue,
79        format: TextureFormat,
80        depth_stencil: Option<DepthStencilState>,
81        params: TextRendererParams,
82    ) -> Self {
83        let _srgb = format.is_srgb();
84        // todo put this in the uniform and use it
85        
86        let atlas_size = params.atlas_page_size.size(device);
87
88        let mask_texture = device.create_texture(&TextureDescriptor {
89            label: Some("atlas"),
90            size: Extent3d {
91                width: atlas_size,
92                height: atlas_size,
93                depth_or_array_layers: 1,
94            },
95            mip_level_count: 1,
96            sample_count: 1,
97            dimension: TextureDimension::D2,
98            format: TextureFormat::R8Unorm,
99            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
100            view_formats: &[],
101        });
102        let mask_texture_view = mask_texture.create_view(&TextureViewDescriptor::default());
103
104
105        let sampler = device.create_sampler(&SamplerDescriptor {
106            label: Some("sampler"),
107            min_filter: FilterMode::Nearest,
108            mag_filter: FilterMode::Nearest,
109            mipmap_filter: FilterMode::Nearest,
110            lod_min_clamp: 0f32,
111            lod_max_clamp: 0f32,
112            ..Default::default()
113        });
114
115        let shader = device.create_shader_module(ShaderModuleDescriptor {
116            label: Some("shader"),
117            source: ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
118        });
119
120        let vertex_buffer_layout = wgpu::VertexBufferLayout {
121            array_stride: std::mem::size_of::<Quad>() as wgpu::BufferAddress,
122            step_mode: wgpu::VertexStepMode::Instance,
123            attributes: &wgpu::vertex_attr_array![
124                0 => Sint32x2,
125                1 => Uint32,
126                2 => Uint32,
127                3 => Uint32,
128                4 => Float32,
129                5 => Uint32,
130                6 => Sint16x4,
131            ],
132        };
133
134        let params = Params {
135            screen_resolution_width: 0.0,
136            screen_resolution_height: 0.0,
137            _pad: [0, 0],
138        };
139
140        let params_buffer = device.create_buffer(&BufferDescriptor {
141            label: Some("params"),
142            size: mem::size_of::<Params>() as u64,
143            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
144            mapped_at_creation: false,
145        });
146
147        let params_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
148            entries: &[BindGroupLayoutEntry {
149                binding: 0,
150                visibility: ShaderStages::VERTEX,
151                ty: BindingType::Buffer {
152                    ty: BufferBindingType::Uniform,
153                    has_dynamic_offset: false,
154                    min_binding_size: NonZeroU64::new(mem::size_of::<Params>() as u64),
155                },
156                count: None,
157            }],
158            label: Some("uniforms bind group layout"),
159        });
160
161        let params_bind_group = device.create_bind_group(&BindGroupDescriptor {
162            layout: &params_layout,
163            entries: &[BindGroupEntry {
164                binding: 0,
165                resource: params_buffer.as_entire_binding(),
166            }],
167            label: Some("uniforms bind group"),
168        });
169
170        let atlas_bind_group_layout = device.create_bind_group_layout(&ATLAS_BIND_GROUP_LAYOUT);
171        let mask_bind_group = device.create_bind_group(&BindGroupDescriptor {
172            layout: &atlas_bind_group_layout,
173            entries: &[
174                BindGroupEntry {
175                    binding: 0,
176                    resource: BindingResource::TextureView(&mask_texture_view),
177                },
178                BindGroupEntry {
179                    binding: 1,
180                    resource: BindingResource::Sampler(&sampler),
181                },
182            ],
183            label: Some("atlas bind group"),
184        });
185
186        let glyph_cache = LruCache::unbounded_with_hasher(BuildHasherDefault::<FxHasher>::default());
187
188        let mask_atlas_pages = vec![AtlasPage::<GrayImage> {
189            image: GrayImage::from_pixel(atlas_size, atlas_size, Luma([0])),
190            packer: BucketedAtlasAllocator::new(size2(atlas_size as i32, atlas_size as i32)),
191            quads: Vec::<Quad>::with_capacity(300),
192            gpu: Some(GpuAtlasPage {
193                texture: mask_texture,
194                bind_group: mask_bind_group,
195            }),
196            quad_count_before_render: 0,
197        }];
198
199        let color_texture = device.create_texture(&TextureDescriptor {
200            label: Some("atlas"),
201            size: Extent3d {
202                width: atlas_size,
203                height: atlas_size,
204                depth_or_array_layers: 1,
205            },
206            mip_level_count: 1,
207            sample_count: 1,
208            dimension: TextureDimension::D2,
209            format: TextureFormat::Rgba8Unorm,
210            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
211            view_formats: &[],
212        });
213        let color_texture_view = color_texture.create_view(&TextureViewDescriptor::default());
214
215
216        let color_bind_group = device.create_bind_group(&BindGroupDescriptor {
217            layout: &atlas_bind_group_layout,
218            entries: &[
219                BindGroupEntry {
220                    binding: 0,
221                    resource: BindingResource::TextureView(&color_texture_view),
222                },
223                BindGroupEntry {
224                    binding: 1,
225                    resource: BindingResource::Sampler(&sampler),
226                },
227            ],
228            label: Some("atlas bind group"),
229        });
230
231        let color_atlas_pages = vec![AtlasPage::<RgbaImage> {
232            image: RgbaImage::from_pixel(atlas_size, atlas_size, Rgba([0, 0, 0, 0])),
233            packer: BucketedAtlasAllocator::new(size2(atlas_size as i32, atlas_size as i32)),
234            quads: Vec::<Quad>::with_capacity(300),
235            gpu: Some(GpuAtlasPage {
236                texture: color_texture,
237                bind_group: color_bind_group,
238            }),
239            quad_count_before_render: 0,
240        }];
241
242        let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
243            label: None,
244            bind_group_layouts: &[&atlas_bind_group_layout, &params_layout],
245            push_constant_ranges: &[],
246        });
247
248        let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
249            label: Some("textslabs pipeline"),
250            layout: Some(&pipeline_layout),
251            vertex: VertexState {
252                module: &shader,
253                entry_point: Some("vs_main"),
254                buffers: &[vertex_buffer_layout],
255                compilation_options: PipelineCompilationOptions::default(),
256            },
257            fragment: Some(FragmentState {
258                module: &shader,
259                entry_point: Some("fs_main"),
260                targets: &[Some(ColorTargetState {
261                    // todo: is this the format that needs to be the same as outside?
262                    format: TextureFormat::Bgra8UnormSrgb,
263                    blend: Some(BlendState::ALPHA_BLENDING),
264                    write_mask: ColorWrites::default(),
265                })],
266                compilation_options: PipelineCompilationOptions::default(),
267            }),
268            primitive: PrimitiveState {
269                topology: PrimitiveTopology::TriangleStrip,
270                ..Default::default()
271            },
272            depth_stencil,
273            multisample: MultisampleState::default(),
274            multiview: None,
275            cache: None,
276        });
277
278        let tmp_image = Image::new();
279        let frame = 1;
280        
281        let vertex_buffer = create_vertex_buffer(device, INITIAL_BUFFER_SIZE);
282        
283        Self {
284            frame,
285            atlas_size,
286            tmp_image,
287            mask_atlas_pages,
288            color_atlas_pages,
289            decorations: Vec::with_capacity(50),
290            pipeline,
291            atlas_bind_group_layout,
292            sampler,
293            params,
294            params_buffer,
295            params_bind_group,
296            glyph_cache,
297            last_frame_evicted: 0,
298            // cached_scaler: None,
299            vertex_buffer,
300            needs_gpu_sync: true,
301        }
302    }
303}
304
305impl ContextlessTextRenderer {
306    pub fn gpu_load(&mut self, device: &Device, queue: &Queue) {
307        if !self.needs_gpu_sync {
308            return;
309        }
310
311        let bytes: &[u8] = bytemuck::cast_slice(std::slice::from_ref(&self.params));
312        queue.write_buffer(&self.params_buffer, 0, bytes);
313
314        // Calculate total number of quads across all pages plus decorations
315        let total_quads = self.mask_atlas_pages.iter().map(|p| p.quads.len()).sum::<usize>()
316                        + self.color_atlas_pages.iter().map(|p| p.quads.len()).sum::<usize>()
317                        + self.decorations.len();
318        
319        let required_size = (total_quads * std::mem::size_of::<Quad>()) as u64;
320        
321        // Grow shared vertex buffer if needed
322        if self.vertex_buffer.size() < required_size {
323            let min_size = u64::max(required_size, INITIAL_BUFFER_SIZE);
324            let growth_size = min_size * 3 / 2;
325            let current_growth = self.vertex_buffer.size() * 3 / 2;
326            let new_size = u64::max(growth_size, current_growth);
327            
328            self.vertex_buffer = create_vertex_buffer(device, new_size);
329        }
330
331        let mut buffer_offset = 0u64;
332        
333        for page in &self.mask_atlas_pages {
334            if !page.quads.is_empty() {
335                let bytes: &[u8] = bytemuck::cast_slice(&page.quads);
336                queue.write_buffer(&self.vertex_buffer, buffer_offset, bytes);
337                buffer_offset += bytes.len() as u64;
338            }
339        }
340        
341        for page in &self.color_atlas_pages {
342            if !page.quads.is_empty() {
343                let bytes: &[u8] = bytemuck::cast_slice(&page.quads);
344                queue.write_buffer(&self.vertex_buffer, buffer_offset, bytes);
345                buffer_offset += bytes.len() as u64;
346            }
347        }
348        
349        if !self.decorations.is_empty() {
350            let bytes: &[u8] = bytemuck::cast_slice(&self.decorations);
351            queue.write_buffer(&self.vertex_buffer, buffer_offset, bytes);
352        }
353
354        // Handle mask atlas pages
355        for page in &mut self.mask_atlas_pages {
356            if page.gpu.is_none() {
357                let texture = device.create_texture(&TextureDescriptor {
358                    label: Some("atlas"),
359                    size: Extent3d {
360                        width: self.atlas_size,
361                        height: self.atlas_size,
362                        depth_or_array_layers: 1,
363                    },
364                    mip_level_count: 1,
365                    sample_count: 1,
366                    dimension: TextureDimension::D2,
367                    format: TextureFormat::R8Unorm,
368                    usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
369                    view_formats: &[],
370                });
371                let texture_view = texture.create_view(&TextureViewDescriptor::default());
372
373                let bind_group = device.create_bind_group(&BindGroupDescriptor {
374                    layout: &self.atlas_bind_group_layout,
375                    entries: &[
376                        BindGroupEntry {
377                            binding: 0,
378                            resource: BindingResource::TextureView(&texture_view),
379                        },
380                        BindGroupEntry {
381                            binding: 1,
382                            resource: BindingResource::Sampler(&self.sampler),
383                        },
384                    ],
385                    label: Some("atlas bind group"),
386                });
387        
388                page.gpu = Some(GpuAtlasPage {
389                    texture,
390                    bind_group,
391                })
392            }
393
394            queue.write_texture(
395                ImageCopyTexture {
396                    texture: &page.gpu.as_ref().unwrap().texture,
397                    mip_level: 0,
398                    origin: Origin3d { x: 0, y: 0, z: 0 },
399                    aspect: TextureAspect::All,
400                },
401                &page.image.as_raw(),
402                ImageDataLayout {
403                    offset: 0,
404                    bytes_per_row: Some(page.image.width()),
405                    rows_per_image: None,
406                },
407                Extent3d {
408                    width: page.image.width(),
409                    height: page.image.height(),
410                    depth_or_array_layers: 1,
411                },
412            );
413        }
414
415        // Handle color atlas pages
416        for page in &mut self.color_atlas_pages {
417            if page.gpu.is_none() {
418                let texture = device.create_texture(&TextureDescriptor {
419                    label: Some("atlas"),
420                    size: Extent3d {
421                        width: self.atlas_size,
422                        height: self.atlas_size,
423                        depth_or_array_layers: 1,
424                    },
425                    mip_level_count: 1,
426                    sample_count: 1,
427                    dimension: TextureDimension::D2,
428                    format: TextureFormat::Rgba8Unorm,
429                    usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
430                    view_formats: &[],
431                });
432                let texture_view = texture.create_view(&TextureViewDescriptor::default());
433
434                let bind_group = device.create_bind_group(&BindGroupDescriptor {
435                    layout: &self.atlas_bind_group_layout,
436                    entries: &[
437                        BindGroupEntry {
438                            binding: 0,
439                            resource: BindingResource::TextureView(&texture_view),
440                        },
441                        BindGroupEntry {
442                            binding: 1,
443                            resource: BindingResource::Sampler(&self.sampler),
444                        },
445                    ],
446                    label: Some("atlas bind group"),
447                });
448        
449                page.gpu = Some(GpuAtlasPage {
450                    texture,
451                    bind_group,
452                })
453            }
454    
455            queue.write_texture(
456                ImageCopyTexture {
457                    texture: &page.gpu.as_ref().unwrap().texture,
458                    mip_level: 0,
459                    origin: Origin3d { x: 0, y: 0, z: 0 },
460                    aspect: TextureAspect::All,
461                },
462                &page.image.as_raw(),
463                ImageDataLayout {
464                    offset: 0,
465                    bytes_per_row: Some(page.image.width() * 4),
466                    rows_per_image: None,
467                },
468                Extent3d {
469                    width: page.image.width(),
470                    height: page.image.height(),
471                    depth_or_array_layers: 1,
472                },
473            );
474        }
475
476        self.needs_gpu_sync = false;
477    }
478}