tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 238TOML 102Markdown 83Shell 24178 Definiciones

⎇ main llimphi-raster/src/gpu_atlas.rs Rust · 138 líneas · 5.3 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

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}