gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main llimphi-raster/tests/gpu_lotes.rs Rust · 195 líneas · 7.2 KB Historia Quién tocó qué crudo

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

1//! Troceado de buffers del GPU-directo: una escena partida en lotes tiene que
2//! dar **exactamente** los mismos píxeles que la misma escena en un buffer
3//! único.
4//!
5//! Por qué importa: `max_buffer_size` es un límite duro (256 MB en la Iris Xe
6//! del operador). A 32 B por primitiva son 8,4 M en una sola reserva, y pasado
7//! eso wgpu **rechaza** — no «va más lento», directamente no anda. El bench del
8//! PLAN-MOTOR-PROFUNDO documentaba la fila de 10 M como «no entra». Ahora entra,
9//! y este test certifica que entrar no cambió el dibujo.
10//!
11//! Lo que se prueba de verdad es el **orden**: los cuatro tipos van con alpha,
12//! superpuestos, de modo que cualquier lote que se dibuje fuera de secuencia
13//! cambia el color resultante. Se compara byte a byte, sin tolerancia.
14//!
15//! Certificado por texto (§8), llvmpipe.
16
17use llimphi_hal::{wgpu, Hal};
18use llimphi_raster::gpu::{GpuBatch, GpuPipelines};
19use llimphi_raster::peniko::Color;
20
21const N: u32 = 32;
22const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
23
24fn read_all(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
25    let unpadded = N as usize * 4;
26    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
27    let padded = unpadded.div_ceil(align) * align;
28    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
29        label: Some("lotes-readback"),
30        size: (padded * N as usize) as u64,
31        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
32        mapped_at_creation: false,
33    });
34    let mut enc = hal
35        .device
36        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
37    enc.copy_texture_to_buffer(
38        wgpu::TexelCopyTextureInfo {
39            texture: target,
40            mip_level: 0,
41            origin: wgpu::Origin3d::ZERO,
42            aspect: wgpu::TextureAspect::All,
43        },
44        wgpu::TexelCopyBufferInfo {
45            buffer: &buf,
46            layout: wgpu::TexelCopyBufferLayout {
47                offset: 0,
48                bytes_per_row: Some(padded as u32),
49                rows_per_image: Some(N),
50            },
51        },
52        wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
53    );
54    hal.queue.submit(std::iter::once(enc.finish()));
55    let slice = buf.slice(..);
56    let (tx, rx) = std::sync::mpsc::channel();
57    slice.map_async(wgpu::MapMode::Read, move |r| {
58        let _ = tx.send(r);
59    });
60    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
61    rx.recv().unwrap().unwrap();
62    let data = slice.get_mapped_range();
63    let mut out = Vec::with_capacity((N * N * 4) as usize);
64    for row in 0..N as usize {
65        let s = row * padded;
66        out.extend_from_slice(&data[s..s + unpadded]);
67    }
68    drop(data);
69    buf.unmap();
70    out
71}
72
73/// Una escena con los cuatro tipos, todos con alpha y superpuestos: si un lote
74/// se dibujara fuera de orden, el alpha-over daría otro color.
75fn escena(b: &mut GpuBatch<'_>) {
76    for i in 0..9 {
77        let t = i as f32;
78        b.add_rect(
79            2.0 + t,
80            2.0 + t,
81            18.0,
82            18.0,
83            Color::from_rgba8(200, 40 + (i as u8) * 20, 30, 120),
84        );
85    }
86    for i in 0..7 {
87        b.add_disc(
88            8.0 + i as f32 * 2.0,
89            20.0,
90            6.0,
91            Color::from_rgba8(20, 200, 60 + (i as u8) * 10, 110),
92        );
93    }
94    for i in 0..5 {
95        let y = 4.0 + i as f32 * 5.0;
96        b.add_tri(
97            (1.0, y),
98            (30.0, y + 2.0),
99            (14.0, y + 6.0),
100            Color::from_rgba8(90, 90, 240, 100),
101            Color::from_rgba8(240, 200, 90, 100),
102            Color::from_rgba8(60, 240, 200, 100),
103        );
104    }
105    for i in 0..11 {
106        let x = 1.0 + i as f32 * 2.7;
107        b.add_line((x, 1.0), (31.0 - x, 30.0), Color::from_rgba8(255, 255, 255, 90));
108    }
109}
110
111/// Pinta la escena con el techo de buffer indicado y devuelve los píxeles.
112fn pintar(hal: &Hal, pipelines: &GpuPipelines, techo: Option<u64>) -> Vec<u8> {
113    let target = hal.device.create_texture(&wgpu::TextureDescriptor {
114        label: Some("lotes-target"),
115        size: wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
116        mip_level_count: 1,
117        sample_count: 1,
118        dimension: wgpu::TextureDimension::D2,
119        format: FMT,
120        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
121        view_formats: &[],
122    });
123    let view = target.create_view(&wgpu::TextureViewDescriptor::default());
124    let mut enc = hal
125        .device
126        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
127
128    let mut b = GpuBatch::new(pipelines);
129    escena(&mut b);
130    if let Some(t) = techo {
131        b.max_buffer_bytes(t);
132    }
133    b.flush(
134        &hal.device,
135        &hal.queue,
136        &mut enc,
137        &view,
138        (N as f32, N as f32),
139        wgpu::LoadOp::Clear(wgpu::Color::BLACK),
140    );
141    hal.queue.submit(std::iter::once(enc.finish()));
142    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
143    read_all(hal, &target)
144}
145
146fn distintos(a: &[u8], b: &[u8]) -> usize {
147    a.chunks(4).zip(b.chunks(4)).filter(|(x, y)| x != y).count()
148}
149
150#[test]
151fn una_escena_troceada_da_los_mismos_pixeles_que_entera() {
152    let hal = pollster::block_on(Hal::new_headless(true)).expect("hal");
153    let pipelines = GpuPipelines::new(&hal.device, FMT);
154
155    let entera = pintar(&hal, &pipelines, None);
156
157    // 32 B = exactamente **una** instancia por lote: el troceado más agresivo
158    // posible. 9 rects, 7 discos, 11 líneas → un draw cada uno; los triángulos
159    // caen al mínimo de 3 vértices, o sea un triángulo por lote.
160    let troceada = pintar(&hal, &pipelines, Some(32));
161    assert_eq!(
162        distintos(&entera, &troceada),
163        0,
164        "el troceado extremo cambió el dibujo: no se conservó el orden de los lotes"
165    );
166
167    // Y un corte que NO cae en borde de primitiva para ninguno de los cuatro
168    // tipos (100 B no es múltiplo ni de 32 ni de 24): el reparto se apoya en el
169    // stride, no en que el número sea redondo.
170    let irregular = pintar(&hal, &pipelines, Some(100));
171    assert_eq!(distintos(&entera, &irregular), 0, "corte irregular cambió el dibujo");
172
173    // Guarda del arnés: si la escena fuera invisible, los tres serían iguales
174    // por vacíos y el test no probaría nada (el bench del plan reportó 23.000
175    // fps midiendo justamente eso).
176    let pintados = entera.chunks(4).filter(|p| p[0] > 8 || p[1] > 8 || p[2] > 8).count();
177    assert!(
178        pintados > 400,
179        "la escena de referencia casi no pintó ({pintados} px): el test no probaría nada"
180    );
181}
182
183#[test]
184fn el_techo_por_defecto_sale_de_la_gpu_y_no_trocea_una_escena_normal() {
185    // Nadie tiene que setear nada: sin `max_buffer_bytes` el techo es el de la
186    // GPU, y una escena de juguete entra en un lote por tipo.
187    let hal = pollster::block_on(Hal::new_headless(true)).expect("hal");
188    let techo = hal.device.limits().max_buffer_size;
189    assert!(techo >= 128 * 1024 * 1024, "techo insólito: {techo}");
190    assert!(
191        llimphi_raster::units_per_chunk(llimphi_raster::RECT_INSTANCE_STRIDE, techo, 1)
192            > 1_000_000,
193        "con el techo real tienen que entrar millones de rects en un solo lote"
194    );
195}