tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! Textura de cobertura de un canal para el camino GPU-directo.
2//!
3//! Es el recurso que le faltaba a [`crate::gpu::GpuBatch`] para pintar texto:
4//! una textura `R8Unorm` donde alguien —hoy `llimphi-glifos`— deja mapas de
5//! cobertura ya rasterizados, y un pipeline que dibuja quads muestreándola.
6//!
7//! **A propósito no sabe nada de fuentes.** Acá adentro no hay glifo ni layout
8//! ni parley: hay bytes de cobertura y un rect UV. Eso mantiene `llimphi-raster`
9//! como lo que es —el que habla con wgpu— y deja la tipografía del lado que
10//! corresponde. La misma textura sirve igual para máscaras de sprites o para un
11//! atlas de íconos monocromos, sin tocar una línea.
12//!
13//! El ciclo típico por frame:
14//!
15//! ```ignore
16//! // Una vez, y se guarda entre frames: recrearla por cuadro tiraría el caché.
17//! let mut tex = AtlasTextura::new(&device, &pipelines, atlas.lado());
18//! // Por frame: subir SÓLO lo que cambió desde la última vez.
19//! if let Some(s) = atlas.tomar_sucio() {
20//! tex.subir(&queue, atlas.pixeles(), (s.x, s.y, s.ancho, s.alto));
21//! }
22//! let mut batch = GpuBatch::new(&pipelines);
23//! batch.atlas(&tex);
24//! for q in quads { batch.add_glyph(q.x, q.y, q.w, q.h, (q.u0, q.v0, q.u1, q.v1), color); }
25//! batch.flush(..);
26//! ```
27
28use llimphi_hal::wgpu;
29
30/// El formato del atlas: un byte de cobertura por píxel. Se muestrea como
31/// `.r` y multiplica el alfa del color del quad.
32pub const FORMATO_ATLAS: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm;
33
34/// Una textura de cobertura viva en GPU, con su bind group listo.
35///
36/// Se construye una vez y se mantiene entre frames: subir el atlas entero cada
37/// cuadro anularía todo el punto de cachear los glifos.
38pub struct AtlasTextura {
39 textura: wgpu::Texture,
40 pub(crate) bind_group: wgpu::BindGroup,
41 lado: u32,
42}
43
44impl AtlasTextura {
45 /// Crea la textura cuadrada y su bind group contra el layout del pipeline
46 /// de quads. `lado` es el mismo del atlas de CPU.
47 pub fn new(device: &wgpu::Device, pipelines: &crate::gpu::GpuPipelines, lado: u32) -> Self {
48 let lado = lado.max(1);
49 let textura = device.create_texture(&wgpu::TextureDescriptor {
50 label: Some("llimphi-raster-gpu-atlas"),
51 size: wgpu::Extent3d {
52 width: lado,
53 height: lado,
54 depth_or_array_layers: 1,
55 },
56 mip_level_count: 1,
57 sample_count: 1,
58 dimension: wgpu::TextureDimension::D2,
59 format: FORMATO_ATLAS,
60 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
61 view_formats: &[],
62 });
63 let vista = textura.create_view(&wgpu::TextureViewDescriptor::default());
64 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
65 label: Some("llimphi-raster-gpu-atlas-bg"),
66 layout: pipelines.atlas_layout(),
67 entries: &[
68 wgpu::BindGroupEntry {
69 binding: 0,
70 resource: wgpu::BindingResource::TextureView(&vista),
71 },
72 wgpu::BindGroupEntry {
73 binding: 1,
74 resource: wgpu::BindingResource::Sampler(pipelines.atlas_sampler()),
75 },
76 ],
77 });
78 Self { textura, bind_group, lado }
79 }
80
81 pub fn lado(&self) -> u32 {
82 self.lado
83 }
84
85 /// Sube una región rectangular del atlas de CPU. `pixeles` es el atlas
86 /// **entero** (filas de `lado` bytes); `rect` es qué parte de él subir.
87 ///
88 /// Subir sólo el rect sucio y no el atlas completo no es microoptimización:
89 /// un atlas de 1024² son 1 MiB, y hacerlo por frame sería mover un megabyte
90 /// por PCIe para agregar una letra.
91 pub fn subir(
92 &mut self,
93 queue: &wgpu::Queue,
94 pixeles: &[u8],
95 rect: (u32, u32, u32, u32),
96 ) {
97 let (x, y, w, h) = rect;
98 if w == 0 || h == 0 {
99 return;
100 }
101 // Clamp: un rect fuera de la textura sería un panic de wgpu, y quien
102 // pasa el rect viene de otro crate.
103 let x = x.min(self.lado);
104 let y = y.min(self.lado);
105 let w = w.min(self.lado - x);
106 let h = h.min(self.lado - y);
107 if w == 0 || h == 0 {
108 return;
109 }
110 // Las filas del rect no son contiguas en el atlas de CPU, así que se
111 // compactan. `write_texture` no exige alineación de fila (a diferencia
112 // de `copy_buffer_to_texture`), así que `bytes_per_row = w` va.
113 let mut compacto = Vec::with_capacity((w * h) as usize);
114 for fila in 0..h {
115 let ini = ((y + fila) * self.lado + x) as usize;
116 compacto.extend_from_slice(&pixeles[ini..ini + w as usize]);
117 }
118 queue.write_texture(
119 wgpu::TexelCopyTextureInfo {
120 texture: &self.textura,
121 mip_level: 0,
122 origin: wgpu::Origin3d { x, y, z: 0 },
123 aspect: wgpu::TextureAspect::All,
124 },
125 &compacto,
126 wgpu::TexelCopyBufferLayout {
127 offset: 0,
128 bytes_per_row: Some(w),
129 rows_per_image: Some(h),
130 },
131 wgpu::Extent3d {
132 width: w,
133 height: h,
134 depth_or_array_layers: 1,
135 },
136 );
137 }
138}