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//! 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}