gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-raster/tests/gpu_scissor.rs Rust · 225 siq'ikuna · 9.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! Correctitud del `scissor` del GPU-directo (habilitador de damage /
2//! partial-present): un `flush` con `scissor(x,y,w,h)` + `LoadOp::Load` sólo
3//! pinta dentro del rect y **preserva** el resto del `view`. Es la propiedad que
4//! el runtime necesita para re-pintar únicamente la región sucia (un cursor, un
5//! spinner) sin tocar la pantalla entera. Certificado por texto (§8), llvmpipe.
6
7use llimphi_hal::{wgpu, Hal};
8use llimphi_raster::gpu::{GpuBatch, GpuPipelines};
9use llimphi_raster::peniko::Color;
10
11const N: u32 = 16;
12const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
13
14fn read_all(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
15    let unpadded = N as usize * 4;
16    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
17    let padded = unpadded.div_ceil(align) * align;
18    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
19        label: Some("scissor-readback"),
20        size: (padded * N as usize) as u64,
21        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
22        mapped_at_creation: false,
23    });
24    let mut enc = hal
25        .device
26        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
27    enc.copy_texture_to_buffer(
28        wgpu::TexelCopyTextureInfo {
29            texture: target,
30            mip_level: 0,
31            origin: wgpu::Origin3d::ZERO,
32            aspect: wgpu::TextureAspect::All,
33        },
34        wgpu::TexelCopyBufferInfo {
35            buffer: &buf,
36            layout: wgpu::TexelCopyBufferLayout {
37                offset: 0,
38                bytes_per_row: Some(padded as u32),
39                rows_per_image: Some(N),
40            },
41        },
42        wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
43    );
44    hal.queue.submit(std::iter::once(enc.finish()));
45    let slice = buf.slice(..);
46    let (tx, rx) = std::sync::mpsc::channel();
47    slice.map_async(wgpu::MapMode::Read, move |r| {
48        let _ = tx.send(r);
49    });
50    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
51    rx.recv().unwrap().unwrap();
52    let data = slice.get_mapped_range();
53    let mut out = Vec::with_capacity((N * N * 4) as usize);
54    for row in 0..N as usize {
55        let s = row * padded;
56        out.extend_from_slice(&data[s..s + unpadded]);
57    }
58    drop(data);
59    buf.unmap();
60    out
61}
62
63fn px(buf: &[u8], x: u32, y: u32) -> [u8; 4] {
64    let o = ((y * N + x) * 4) as usize;
65    [buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]
66}
67
68/// Pinta una escena de AA fino y devuelve el buffer entero.
69///
70/// Con `recorte`, el `flush` rasteriza a una textura **del tamaño del rect** y
71/// el vertex shader corre las coordenadas; sin él, a una de pantalla completa.
72/// Son dos caminos distintos que tienen que dar el mismo píxel.
73fn escena_con_bordes(hal: &Hal, pipes: &GpuPipelines, recorte: Option<(u32, u32, u32, u32)>) -> Vec<u8> {
74    let target = hal.device.create_texture(&wgpu::TextureDescriptor {
75        label: Some("bordes-target"),
76        size: wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
77        mip_level_count: 1,
78        sample_count: 1,
79        dimension: wgpu::TextureDimension::D2,
80        format: FMT,
81        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
82        view_formats: &[],
83    });
84    let view = target.create_view(&wgpu::TextureViewDescriptor::default());
85    let mut enc = hal
86        .device
87        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
88    let mut b = GpuBatch::new(pipes);
89    // Coordenadas fraccionarias a propósito: el AA del MSAA y el SDF del disco
90    // dependen de la posición subpíxel. Medio píxel de desfase en el
91    // corrimiento del shader cambiaría estos bordes y no el interior.
92    b.add_disc(7.3, 8.6, 4.4, Color::from_rgba8(255, 220, 40, 255));
93    b.add_line((3.5, 3.5), (12.25, 13.75), Color::from_rgba8(40, 255, 220, 255));
94    b.add_rect(5.2, 5.9, 5.3, 4.1, Color::from_rgba8(220, 40, 255, 160));
95    if let Some((x, y, w, h)) = recorte {
96        b.scissor(x, y, w, h);
97    }
98    b.flush(
99        &hal.device,
100        &hal.queue,
101        &mut enc,
102        &view,
103        (N as f32, N as f32),
104        wgpu::LoadOp::Clear(wgpu::Color::BLACK),
105    );
106    hal.queue.submit(std::iter::once(enc.finish()));
107    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
108    read_all(hal, &target)
109}
110
111#[test]
112fn el_recorte_cae_en_el_mismo_pixel_exacto_que_la_pasada_completa() {
113    // Desde 2026-08-05 el `flush` con scissor rasteriza a una textura del
114    // tamaño del rect y **corre las coordenadas** en el vertex shader (antes
115    // rasterizaba a pantalla completa y recortaba con `set_scissor_rect`). Es
116    // el cambio que hace que el clear y el resolve del MSAA sean del rect y no
117    // de la pantalla — pero mueve el origen, y un desfase de medio píxel ahí
118    // saldría sólo en los bordes antialiaseados, no en el relleno.
119    let hal = pollster::block_on(Hal::new_headless(true)).expect("hal");
120    let pipes = GpuPipelines::new(&hal.device, FMT);
121
122    let hal = pollster::block_on(Hal::new_headless(true)).expect("hal");
123    let pipes = GpuPipelines::new(&hal.device, FMT);
124    let completa = escena_con_bordes(&hal, &pipes, None);
125
126    // Orígenes **impares** primero: son los que rompían. El AA del disco sale
127    // de `fwidth`, cuyas derivadas se calculan por bloques de 2×2 fragmentos
128    // alineados a la grilla del target; correr el origen un número impar de
129    // píxeles reagrupaba esos bloques y la orla del disco salía hasta 11/255
130    // distinta. Por eso el `flush` baja el origen a par. Las líneas y los rects
131    // pasaban igual — su AA es del MSAA, que no mira vecinos.
132    for (rx, ry, rw, rh) in [(3, 3, 10, 11), (5, 7, 9, 9), (2, 4, 12, 10), (0, 0, 16, 16)] {
133        let recortada = escena_con_bordes(&hal, &pipes, Some((rx, ry, rw, rh)));
134        let (mut distintos, mut con_color, mut peor) = (0, 0, 0i32);
135        for y in ry..ry + rh {
136            for x in rx..rx + rw {
137                let (a, b) = (px(&completa, x, y), px(&recortada, x, y));
138                if a != b {
139                    distintos += 1;
140                    peor = peor.max(
141                        (0..4).map(|i| (a[i] as i32 - b[i] as i32).abs()).max().unwrap(),
142                    );
143                }
144                if a[0] > 8 || a[1] > 8 || a[2] > 8 {
145                    con_color += 1;
146                }
147            }
148        }
149        // Guarda del arnés: si el rect cayera sobre un vacío, comparar nada con
150        // nada pasaría siempre.
151        assert!(con_color > 30, "el rect ({rx},{ry},{rw},{rh}) casi no tiene dibujo ({con_color} px)");
152        assert_eq!(
153            distintos, 0,
154            "rect ({rx},{ry},{rw},{rh}): {distintos} px distintos del completo (peor canal Δ{peor})"
155        );
156    }
157}
158
159#[test]
160fn scissor_pinta_solo_la_region_sucia_y_preserva_el_resto() {
161    let hal = pollster::block_on(Hal::new_headless(true)).expect("hal");
162    let pipelines = GpuPipelines::new(&hal.device, FMT);
163    let target = hal.device.create_texture(&wgpu::TextureDescriptor {
164        label: Some("scissor-target"),
165        size: wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
166        mip_level_count: 1,
167        sample_count: 1,
168        dimension: wgpu::TextureDimension::D2,
169        format: FMT,
170        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
171        view_formats: &[],
172    });
173    let view = target.create_view(&wgpu::TextureViewDescriptor::default());
174
175    let mut enc = hal
176        .device
177        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
178
179    // 1) Pintar TODO de azul (estado "previo" del view).
180    let mut base = GpuBatch::new(&pipelines);
181    base.add_rect(0.0, 0.0, N as f32, N as f32, Color::from_rgba8(0, 0, 255, 255));
182    base.flush(
183        &hal.device,
184        &hal.queue,
185        &mut enc,
186        &view,
187        (N as f32, N as f32),
188        wgpu::LoadOp::Clear(wgpu::Color::BLACK),
189    );
190
191    // 2) Un rect rojo full-viewport PERO con scissor a [4,4]-[12,12] y Load:
192    //    debe pintar rojo sólo ahí y dejar el azul intacto afuera.
193    let mut dirty = GpuBatch::new(&pipelines);
194    dirty.add_rect(0.0, 0.0, N as f32, N as f32, Color::from_rgba8(255, 0, 0, 255));
195    dirty.scissor(4, 4, 8, 8);
196    dirty.flush(
197        &hal.device,
198        &hal.queue,
199        &mut enc,
200        &view,
201        (N as f32, N as f32),
202        wgpu::LoadOp::Load,
203    );
204    hal.queue.submit(std::iter::once(enc.finish()));
205    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
206
207    let buf = read_all(&hal, &target);
208
209    // Centro (8,8): dentro del scissor → rojo.
210    let c = px(&buf, 8, 8);
211    assert!(c[0] > 200 && c[2] < 60, "centro debería ser rojo, fue {c:?}");
212    // Esquinas fuera del scissor → azul preservado.
213    for (x, y) in [(1, 1), (14, 1), (1, 14), (14, 14)] {
214        let p = px(&buf, x, y);
215        assert!(
216            p[2] > 200 && p[0] < 60,
217            "({x},{y}) fuera del scissor debería seguir azul, fue {p:?}"
218        );
219    }
220    // Justo fuera del borde del scissor (3,3) azul; justo dentro (4,4) rojo.
221    let out = px(&buf, 3, 3);
222    assert!(out[2] > 200 && out[0] < 60, "(3,3) borde-fuera debería ser azul, fue {out:?}");
223    let inn = px(&buf, 5, 5);
224    assert!(inn[0] > 200 && inn[2] < 60, "(5,5) borde-dentro debería ser rojo, fue {inn:?}");
225}