gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main llimphi-raster/tests/gpu_grosores.rs Rust · 236 lines · 8.1 KB History Who touched what raw

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

1//! Varios grosores de línea en **un solo** `flush` del GPU-directo.
2//!
3//! Hasta 2026-08-05 el grosor era un uniforme del lote entero. Eso convertía
4//! «una draw call para todo» en una promesa que se rompía con el dibujo más
5//! común que hay: una grilla fina con una curva gruesa encima. Había que cortar
6//! el lote, y **cada `flush` arma su propia textura MSAA 4×, la resuelve y la
7//! compone** — o sea que el segundo grosor no costaba una draw call más, costaba
8//! el ciclo entero otra vez.
9//!
10//! El oráculo no es una imagen bendecida: es **la forma vieja**. Un lote con dos
11//! grosores tiene que dar exactamente los mismos píxeles que dos lotes de un
12//! grosor cada uno, que es lo que un caller estaba obligado a escribir antes.
13//!
14//! Certificado por texto (§8), llvmpipe.
15
16use llimphi_hal::{wgpu, Hal};
17use llimphi_raster::gpu::{GpuBatch, GpuPipelines};
18use llimphi_raster::peniko::Color;
19
20const N: u32 = 64;
21const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
22
23const FINA: f32 = 1.0;
24const GRUESA: f32 = 9.0;
25const X_FINA: f32 = 16.0;
26const X_GRUESA: f32 = 44.0;
27
28fn hal() -> Hal {
29    pollster::block_on(Hal::new_headless(true)).expect("hal headless")
30}
31
32fn textura(hal: &Hal) -> wgpu::Texture {
33    hal.device.create_texture(&wgpu::TextureDescriptor {
34        label: Some("grosores-target"),
35        size: wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
36        mip_level_count: 1,
37        sample_count: 1,
38        dimension: wgpu::TextureDimension::D2,
39        format: FMT,
40        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
41        view_formats: &[],
42    })
43}
44
45fn leer(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
46    let unpadded = N as usize * 4;
47    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
48    let padded = unpadded.div_ceil(align) * align;
49    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
50        label: Some("grosores-readback"),
51        size: (padded * N as usize) as u64,
52        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
53        mapped_at_creation: false,
54    });
55    let mut enc = hal
56        .device
57        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
58    enc.copy_texture_to_buffer(
59        wgpu::TexelCopyTextureInfo {
60            texture: target,
61            mip_level: 0,
62            origin: wgpu::Origin3d::ZERO,
63            aspect: wgpu::TextureAspect::All,
64        },
65        wgpu::TexelCopyBufferInfo {
66            buffer: &buf,
67            layout: wgpu::TexelCopyBufferLayout {
68                offset: 0,
69                bytes_per_row: Some(padded as u32),
70                rows_per_image: Some(N),
71            },
72        },
73        wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
74    );
75    hal.queue.submit(std::iter::once(enc.finish()));
76    let slice = buf.slice(..);
77    let (tx, rx) = std::sync::mpsc::channel();
78    slice.map_async(wgpu::MapMode::Read, move |r| {
79        let _ = tx.send(r);
80    });
81    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
82    rx.recv().unwrap().unwrap();
83    let data = slice.get_mapped_range();
84    let mut out = Vec::with_capacity((N * N * 4) as usize);
85    for row in 0..N as usize {
86        let s = row * padded;
87        out.extend_from_slice(&data[s..s + unpadded]);
88    }
89    drop(data);
90    buf.unmap();
91    out
92}
93
94/// Dos verticales que **no se tocan**: así el alpha-over de la segunda pasada
95/// del camino viejo cae sobre fondo intacto y la comparación mide el grosor, no
96/// el orden de composición.
97fn fina(b: &mut GpuBatch<'_>) {
98    b.line_width(FINA);
99    b.add_line((X_FINA, 4.0), (X_FINA, 60.0), Color::from_rgba8(255, 80, 80, 255));
100}
101
102fn gruesa(b: &mut GpuBatch<'_>) {
103    b.line_width(GRUESA);
104    b.add_line((X_GRUESA, 4.0), (X_GRUESA, 60.0), Color::from_rgba8(80, 200, 255, 255));
105}
106
107/// Píxeles no-negros de la fila `y`, en el rango de columnas `[x0, x1)`.
108fn ancho_en_fila(px: &[u8], y: u32, x0: u32, x1: u32) -> u32 {
109    (x0..x1)
110        .filter(|x| {
111            let o = ((y * N + x) * 4) as usize;
112            px[o] > 8 || px[o + 1] > 8 || px[o + 2] > 8
113        })
114        .count() as u32
115}
116
117#[test]
118fn un_lote_con_dos_grosores_da_lo_mismo_que_dos_lotes_de_uno() {
119    let hal = hal();
120    let pipes = GpuPipelines::new(&hal.device, FMT);
121
122    // Camino nuevo: un solo flush, la pluma cambia entre línea y línea.
123    let t_uno = textura(&hal);
124    let v_uno = t_uno.create_view(&wgpu::TextureViewDescriptor::default());
125    let mut enc = hal
126        .device
127        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
128    let mut b = GpuBatch::new(&pipes);
129    fina(&mut b);
130    gruesa(&mut b);
131    b.flush(
132        &hal.device,
133        &hal.queue,
134        &mut enc,
135        &v_uno,
136        (N as f32, N as f32),
137        wgpu::LoadOp::Clear(wgpu::Color::BLACK),
138    );
139    hal.queue.submit(std::iter::once(enc.finish()));
140    let uno = leer(&hal, &t_uno);
141
142    // Camino viejo: dos flush, uno por grosor. Es literalmente lo que un caller
143    // estaba obligado a escribir, y es el oráculo de este test.
144    let t_dos = textura(&hal);
145    let v_dos = t_dos.create_view(&wgpu::TextureViewDescriptor::default());
146    let mut enc = hal
147        .device
148        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
149    let mut b1 = GpuBatch::new(&pipes);
150    fina(&mut b1);
151    b1.flush(
152        &hal.device,
153        &hal.queue,
154        &mut enc,
155        &v_dos,
156        (N as f32, N as f32),
157        wgpu::LoadOp::Clear(wgpu::Color::BLACK),
158    );
159    let mut b2 = GpuBatch::new(&pipes);
160    gruesa(&mut b2);
161    b2.flush(
162        &hal.device,
163        &hal.queue,
164        &mut enc,
165        &v_dos,
166        (N as f32, N as f32),
167        wgpu::LoadOp::Load,
168    );
169    hal.queue.submit(std::iter::once(enc.finish()));
170    let dos = leer(&hal, &t_dos);
171
172    let distintos = uno
173        .chunks_exact(4)
174        .zip(dos.chunks_exact(4))
175        .filter(|(a, b)| a != b)
176        .count();
177    assert_eq!(distintos, 0, "el lote único no da los mismos píxeles que los dos lotes");
178
179    // Guarda del arnés: si las dos líneas salieran del mismo grosor, la igualdad
180    // de arriba se cumpliría igual y no probaría nada.
181    let a_fina = ancho_en_fila(&uno, 32, 0, N / 2);
182    let a_gruesa = ancho_en_fila(&uno, 32, N / 2, N);
183    assert!(
184        (1..=3).contains(&a_fina),
185        "la línea de {FINA} px salió de {a_fina} px de ancho"
186    );
187    assert!(
188        (8..=11).contains(&a_gruesa),
189        "la línea de {GRUESA} px salió de {a_gruesa} px de ancho"
190    );
191    assert!(
192        a_gruesa >= a_fina * 4,
193        "los dos grosores salieron parecidos ({a_fina} vs {a_gruesa}): \
194         el grosor por instancia no está llegando"
195    );
196}
197
198#[test]
199fn la_pluma_vale_desde_que_se_la_cambia_y_no_hacia_atras() {
200    // El cambio de semántica que trae el grosor por instancia, dicho por un
201    // test: antes `line_width` era un uniforme y valía para TODO el lote sin
202    // importar cuándo se llamara. Ahora es una pluma, y cada línea se queda con
203    // el grosor que había puesto al agregarla. Si esto se rompiera, un caller
204    // que alterna grosores dibujaría todo del último.
205    let hal = hal();
206    let pipes = GpuPipelines::new(&hal.device, FMT);
207    let t = textura(&hal);
208    let v = t.create_view(&wgpu::TextureViewDescriptor::default());
209    let mut enc = hal
210        .device
211        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
212
213    let mut b = GpuBatch::new(&pipes);
214    // Fina primero, gruesa después, y **después** se vuelve a poner fina sin
215    // agregar nada: si el grosor fuera del lote, la gruesa saldría fina.
216    fina(&mut b);
217    gruesa(&mut b);
218    b.line_width(FINA);
219    b.flush(
220        &hal.device,
221        &hal.queue,
222        &mut enc,
223        &v,
224        (N as f32, N as f32),
225        wgpu::LoadOp::Clear(wgpu::Color::BLACK),
226    );
227    hal.queue.submit(std::iter::once(enc.finish()));
228    let px = leer(&hal, &t);
229
230    let a_gruesa = ancho_en_fila(&px, 32, N / 2, N);
231    assert!(
232        (8..=11).contains(&a_gruesa),
233        "un line_width posterior le cambió el grosor a una línea ya agregada: \
234         salió de {a_gruesa} px"
235    );
236}