Files
llimphi/llimphi-raster/tests/gpu_scissor.rs
T
Sergio 93253cc227 chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes
Regenerado con scripts/actualizar-standalone.py --llimphi. Vendoriza además
shared/{foreign-lottie,grafo}. Quedan fuera los crates acoplados al workspace
madre (wasm-*, video-plane, voxel-app/studio, allichay, plugin-host, shuma-term).
cargo check --workspace verde (112 miembros).
2026-07-25 20:57:22 +00:00

135 lines
5.0 KiB
Rust

//! Correctitud del `scissor` del GPU-directo (habilitador de damage /
//! partial-present): un `flush` con `scissor(x,y,w,h)` + `LoadOp::Load` sólo
//! pinta dentro del rect y **preserva** el resto del `view`. Es la propiedad que
//! el runtime necesita para re-pintar únicamente la región sucia (un cursor, un
//! spinner) sin tocar la pantalla entera. Certificado por texto (§8), llvmpipe.
use llimphi_hal::{wgpu, Hal};
use llimphi_raster::gpu::{GpuBatch, GpuPipelines};
use llimphi_raster::peniko::Color;
const N: u32 = 16;
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
fn read_all(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
let unpadded = N as usize * 4;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
let padded = unpadded.div_ceil(align) * align;
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("scissor-readback"),
size: (padded * N as usize) as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut enc = hal
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: target,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buf,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded as u32),
rows_per_image: Some(N),
},
},
wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
);
hal.queue.submit(std::iter::once(enc.finish()));
let slice = buf.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
rx.recv().unwrap().unwrap();
let data = slice.get_mapped_range();
let mut out = Vec::with_capacity((N * N * 4) as usize);
for row in 0..N as usize {
let s = row * padded;
out.extend_from_slice(&data[s..s + unpadded]);
}
drop(data);
buf.unmap();
out
}
fn px(buf: &[u8], x: u32, y: u32) -> [u8; 4] {
let o = ((y * N + x) * 4) as usize;
[buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]
}
#[test]
fn scissor_pinta_solo_la_region_sucia_y_preserva_el_resto() {
let hal = pollster::block_on(Hal::new_headless(true)).expect("hal");
let pipelines = GpuPipelines::new(&hal.device, FMT);
let target = hal.device.create_texture(&wgpu::TextureDescriptor {
label: Some("scissor-target"),
size: wgpu::Extent3d { width: N, height: N, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: FMT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
let mut enc = hal
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
// 1) Pintar TODO de azul (estado "previo" del view).
let mut base = GpuBatch::new(&pipelines);
base.add_rect(0.0, 0.0, N as f32, N as f32, Color::from_rgba8(0, 0, 255, 255));
base.flush(
&hal.device,
&hal.queue,
&mut enc,
&view,
(N as f32, N as f32),
wgpu::LoadOp::Clear(wgpu::Color::BLACK),
);
// 2) Un rect rojo full-viewport PERO con scissor a [4,4]-[12,12] y Load:
// debe pintar rojo sólo ahí y dejar el azul intacto afuera.
let mut dirty = GpuBatch::new(&pipelines);
dirty.add_rect(0.0, 0.0, N as f32, N as f32, Color::from_rgba8(255, 0, 0, 255));
dirty.scissor(4, 4, 8, 8);
dirty.flush(
&hal.device,
&hal.queue,
&mut enc,
&view,
(N as f32, N as f32),
wgpu::LoadOp::Load,
);
hal.queue.submit(std::iter::once(enc.finish()));
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
let buf = read_all(&hal, &target);
// Centro (8,8): dentro del scissor → rojo.
let c = px(&buf, 8, 8);
assert!(c[0] > 200 && c[2] < 60, "centro debería ser rojo, fue {c:?}");
// Esquinas fuera del scissor → azul preservado.
for (x, y) in [(1, 1), (14, 1), (1, 14), (14, 14)] {
let p = px(&buf, x, y);
assert!(
p[2] > 200 && p[0] < 60,
"({x},{y}) fuera del scissor debería seguir azul, fue {p:?}"
);
}
// Justo fuera del borde del scissor (3,3) azul; justo dentro (4,4) rojo.
let out = px(&buf, 3, 3);
assert!(out[2] > 200 && out[0] < 60, "(3,3) borde-fuera debería ser azul, fue {out:?}");
let inn = px(&buf, 5, 5);
assert!(inn[0] > 200 && inn[2] < 60, "(5,5) borde-dentro debería ser rojo, fue {inn:?}");
}