refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la estructura vieja de eventloop) y suma el 3D: - bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 / accesskit_winit 0.33 / vello_hybrid 0.0.9. - nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido, montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox). - README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif). - excluido modules/allichay (arrastra deps fuera del alcance del front-door). - cargo check --workspace: verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+449
-30
@@ -1,7 +1,7 @@
|
||||
//! Backend GPU directo (Fases 2 + 3 del SDD §"GPU directo wgpu").
|
||||
//!
|
||||
//! Tres pipelines `wgpu` cacheadas en [`GpuPipelines`] (lines / tris /
|
||||
//! rects) + un acumulador [`GpuBatch`] que las apps usan por frame para
|
||||
//! Cuatro pipelines `wgpu` cacheadas en [`GpuPipelines`] (lines / tris /
|
||||
//! rects / discs) + un acumulador [`GpuBatch`] que las apps usan por frame para
|
||||
//! emitir centenares de miles a millones de primitivos en una draw call
|
||||
//! por tipo, sin pasar por vello.
|
||||
//!
|
||||
@@ -10,8 +10,11 @@
|
||||
//! - Vertex format triángulos: `[x: f32, y: f32, rgba: u32]` (12 B/vert).
|
||||
//! - Instance format líneas: `[x0, y0, x1, y1, rgba]` (20 B/seg).
|
||||
//! - Instance format rects: `[x, y, w, h, rgba]` (20 B/rect).
|
||||
//! - Sin texturas. Sin AA por shader — quien necesite AA fino sigue por
|
||||
//! vello. Para puntos densos el "popping" no se nota.
|
||||
//! - Instance format discos: `[cx, cy, r, stroke, rgba]` (20 B/disco).
|
||||
//! - Sin texturas. Rects/líneas/tris obtienen AA de **bordes** vía MSAA 4×
|
||||
//! (ver más abajo); los discos SÍ traen AA por SDF en el fragment
|
||||
//! (smoothstep sobre `fwidth`), que MSAA respeta. Así rects/tris/líneas
|
||||
//! instanciados salen con bordes suaves sin que el caller toque nada.
|
||||
//! - Blending alfa habilitado: el alpha del color es respetado.
|
||||
//! - El viewport `(width, height)` se pasa al flush y va en un uniform —
|
||||
//! los shaders convierten pixel → NDC ahí.
|
||||
@@ -25,10 +28,36 @@
|
||||
//! mismo frame. Sin reuso entre frames — Fase 4 (`GpuSceneCanvas`)
|
||||
//! introducirá el `GpuBuffers` persistente que dobla capacidad si
|
||||
//! aparece la necesidad.
|
||||
//!
|
||||
//! ## MSAA 4× (antialiasing de bordes)
|
||||
//!
|
||||
//! El pase no dibuja directo sobre el `view` que recibe `flush`. En su
|
||||
//! lugar rasteriza todos los primitivos a una textura **multisample 4×**
|
||||
//! (cleared a transparente), la *resuelve* a una textura single-sample
|
||||
//! scratch y **compone con alpha** ese resultado sobre el `view`. Así:
|
||||
//!
|
||||
//! - Los bordes de rects/tris/líneas quedan suaves (4 muestras/pixel),
|
||||
//! no escalonados.
|
||||
//! - El contenido previo del `view` (lo que vello pintó) se preserva,
|
||||
//! porque el composite es alpha-over con `LoadOp::Load` — exactamente
|
||||
//! la semántica que tenía el viejo render pass directo con `LoadOp::Load`.
|
||||
//! - `LoadOp::Clear(c)` se respeta: el `view` se limpia a `c` antes del
|
||||
//! composite (equivalente a la pasada directa anterior).
|
||||
//!
|
||||
//! Backward-compat: la firma pública de `flush` / `GpuPipelines::new` no
|
||||
//! cambia. Las texturas MSAA + scratch se crean por-flush dimensionadas
|
||||
//! al `viewport` (mismo patrón que los buffers por-frame), así el resize
|
||||
//! "sale gratis" — cada frame usa el tamaño que se le pasa, sin estado
|
||||
//! persistente que recrear. El pipeline de composite se compila una vez
|
||||
//! y se cachea en `GpuPipelines` (es `Sync`, vive en `OnceLock`).
|
||||
|
||||
use llimphi_hal::wgpu;
|
||||
use vello::peniko::Color;
|
||||
|
||||
/// Número de muestras del MSAA del pase GPU. 4× es el punto dulce
|
||||
/// universal (soportado por todo hardware moderno, coste moderado).
|
||||
const MSAA_SAMPLES: u32 = 4;
|
||||
|
||||
/// Pipelines cacheadas. Crear uno por proceso (o por surface format).
|
||||
///
|
||||
/// Para uso típico via [`GpuBatch`] los campos no se tocan directo. La
|
||||
@@ -46,7 +75,21 @@ pub struct GpuPipelines {
|
||||
pub lines: wgpu::RenderPipeline,
|
||||
pub tris: wgpu::RenderPipeline,
|
||||
pub rects: wgpu::RenderPipeline,
|
||||
/// Discos/anillos rellenos con AA por SDF en el fragment. Instance
|
||||
/// format: `[cx, cy, r, stroke, rgba]` (20 B/disco). `stroke <= 0`
|
||||
/// → disco lleno; `stroke > 0` → anillo de ese grosor (px). Ver
|
||||
/// [`GpuBatch::add_disc`] / [`GpuBatch::add_ring`].
|
||||
pub discs: wgpu::RenderPipeline,
|
||||
pub bind_layout: wgpu::BindGroupLayout,
|
||||
/// Pipeline de pantalla completa que compone (alpha-over) la textura
|
||||
/// scratch resuelta del MSAA sobre el `view` del `flush`. Single-sample.
|
||||
/// El formato del target es el `color_format` con el que se construyó.
|
||||
composite: wgpu::RenderPipeline,
|
||||
composite_bgl: wgpu::BindGroupLayout,
|
||||
composite_sampler: wgpu::Sampler,
|
||||
/// Formato de color del target — necesario para crear las texturas
|
||||
/// MSAA/scratch del `flush` con el mismo formato que el `view`.
|
||||
color_format: wgpu::TextureFormat,
|
||||
}
|
||||
|
||||
impl GpuPipelines {
|
||||
@@ -112,7 +155,10 @@ impl GpuPipelines {
|
||||
},
|
||||
primitive: tri_primitive(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: MSAA_SAMPLES,
|
||||
..Default::default()
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs"),
|
||||
@@ -155,7 +201,10 @@ impl GpuPipelines {
|
||||
},
|
||||
primitive: tri_primitive(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: MSAA_SAMPLES,
|
||||
..Default::default()
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs"),
|
||||
@@ -195,7 +244,10 @@ impl GpuPipelines {
|
||||
},
|
||||
primitive: tri_primitive(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: MSAA_SAMPLES,
|
||||
..Default::default()
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs"),
|
||||
@@ -206,11 +258,142 @@ impl GpuPipelines {
|
||||
cache: None,
|
||||
});
|
||||
|
||||
// Discos/anillos (instanced quad + SDF AA en el fragment). Cada
|
||||
// disco es una instancia de 24 B: `[cx, cy, r, stroke, rgba]`. El
|
||||
// VS expande un quad que cubre el disco (con 1 px de margen para
|
||||
// que el smoothstep del borde no se recorte) y pasa al FS la
|
||||
// posición local en px; el FS calcula la distancia al centro y
|
||||
// hace smoothstep sobre ~1 px (`fwidth`) → borde antialiased.
|
||||
let discs = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("llimphi-raster-gpu-discs"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_discs"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[wgpu::VertexBufferLayout {
|
||||
array_stride: 20,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: &[
|
||||
// cx, cy
|
||||
wgpu::VertexAttribute {
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
},
|
||||
// r, stroke
|
||||
wgpu::VertexAttribute {
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
offset: 8,
|
||||
shader_location: 1,
|
||||
},
|
||||
// rgba
|
||||
wgpu::VertexAttribute {
|
||||
format: wgpu::VertexFormat::Uint32,
|
||||
offset: 16,
|
||||
shader_location: 2,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
primitive: tri_primitive(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: MSAA_SAMPLES,
|
||||
..Default::default()
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_disc"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &color_targets,
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
// Pipeline de composite (alpha-over) de la scratch resuelta del
|
||||
// MSAA sobre el `view`. Single-sample (count = 1), pase fullscreen
|
||||
// de un triángulo. Asume alpha **premultiplicado** — el MSAA + el
|
||||
// blending de los primitivos producen color premultiplicado, así
|
||||
// que el over correcto es `src.rgb*1 + dst.rgb*(1-src.a)`.
|
||||
let composite_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("llimphi-raster-gpu-composite-bgl"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let composite_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("llimphi-raster-gpu-composite-pl"),
|
||||
bind_group_layouts: &[&composite_bgl],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let composite = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("llimphi-raster-gpu-composite"),
|
||||
layout: Some(&composite_pl),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_composite"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_composite"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
}),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
let composite_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("llimphi-raster-gpu-composite-sampler"),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Self {
|
||||
lines,
|
||||
tris,
|
||||
rects,
|
||||
discs,
|
||||
bind_layout,
|
||||
composite,
|
||||
composite_bgl,
|
||||
composite_sampler,
|
||||
color_format,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,10 +416,12 @@ pub struct GpuBatch<'a> {
|
||||
line_verts: Vec<u8>,
|
||||
tri_verts: Vec<u8>,
|
||||
rect_insts: Vec<u8>,
|
||||
disc_insts: Vec<u8>,
|
||||
line_width: f32,
|
||||
line_count: u32,
|
||||
tri_vert_count: u32,
|
||||
rect_count: u32,
|
||||
disc_count: u32,
|
||||
}
|
||||
|
||||
impl<'a> GpuBatch<'a> {
|
||||
@@ -246,10 +431,12 @@ impl<'a> GpuBatch<'a> {
|
||||
line_verts: Vec::new(),
|
||||
tri_verts: Vec::new(),
|
||||
rect_insts: Vec::new(),
|
||||
disc_insts: Vec::new(),
|
||||
line_width: 1.0,
|
||||
line_count: 0,
|
||||
tri_vert_count: 0,
|
||||
rect_count: 0,
|
||||
disc_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,9 +513,37 @@ impl<'a> GpuBatch<'a> {
|
||||
self.rect_count += 1;
|
||||
}
|
||||
|
||||
/// Añade un disco (círculo relleno) con AA por shader como instancia.
|
||||
/// `(cx, cy)` es el centro y `r` el radio, ambos en pixels del frame.
|
||||
/// El borde queda antialiased vía un SDF + `smoothstep` de ~1 px en
|
||||
/// el fragment — no escalonado, sin MSAA. El alpha del color se
|
||||
/// respeta (blending alfa activo).
|
||||
pub fn add_disc(&mut self, cx: f32, cy: f32, r: f32, color: Color) {
|
||||
self.push_disc(cx, cy, r, 0.0, color);
|
||||
}
|
||||
|
||||
/// Añade un anillo (círculo hueco / stroke circular) con AA por
|
||||
/// shader. `r` es el radio exterior; `stroke` el grosor del trazo en
|
||||
/// px (el agujero interior tiene radio `r - stroke`). `stroke <= 0`
|
||||
/// degenera en un disco lleno. Ambos bordes (externo e interno)
|
||||
/// quedan antialiased.
|
||||
pub fn add_ring(&mut self, cx: f32, cy: f32, r: f32, stroke: f32, color: Color) {
|
||||
self.push_disc(cx, cy, r, stroke.max(0.0), color);
|
||||
}
|
||||
|
||||
fn push_disc(&mut self, cx: f32, cy: f32, r: f32, stroke: f32, color: Color) {
|
||||
let rgba = pack_rgba(color);
|
||||
self.disc_insts.extend_from_slice(&cx.to_ne_bytes());
|
||||
self.disc_insts.extend_from_slice(&cy.to_ne_bytes());
|
||||
self.disc_insts.extend_from_slice(&r.to_ne_bytes());
|
||||
self.disc_insts.extend_from_slice(&stroke.to_ne_bytes());
|
||||
self.disc_insts.extend_from_slice(&rgba.to_ne_bytes());
|
||||
self.disc_count += 1;
|
||||
}
|
||||
|
||||
/// Cuenta total de primitivas pendientes (útil para benches).
|
||||
pub fn primitive_count(&self) -> u32 {
|
||||
self.line_count + self.rect_count + self.tri_vert_count / 3
|
||||
self.line_count + self.rect_count + self.disc_count + self.tri_vert_count / 3
|
||||
}
|
||||
|
||||
/// Despacha las primitivas acumuladas como 1 draw call por tipo
|
||||
@@ -348,7 +563,8 @@ impl<'a> GpuBatch<'a> {
|
||||
viewport: (f32, f32),
|
||||
load_op: wgpu::LoadOp<wgpu::Color>,
|
||||
) {
|
||||
let total = self.line_count + self.tri_vert_count + self.rect_count;
|
||||
let total =
|
||||
self.line_count + self.tri_vert_count + self.rect_count + self.disc_count;
|
||||
if total == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -407,13 +623,136 @@ impl<'a> GpuBatch<'a> {
|
||||
queue.write_buffer(&b, 0, &self.rect_insts);
|
||||
b
|
||||
});
|
||||
let discs_buf = (!self.disc_insts.is_empty()).then(|| {
|
||||
let b = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("llimphi-raster-gpu-discs-buf"),
|
||||
size: self.disc_insts.len() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
queue.write_buffer(&b, 0, &self.disc_insts);
|
||||
b
|
||||
});
|
||||
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("llimphi-raster-gpu-pass"),
|
||||
// ── MSAA 4× ──────────────────────────────────────────────────
|
||||
// Texturas por-flush dimensionadas al viewport (mismo patrón que
|
||||
// los buffers de arriba; el resize "sale gratis"). `tex_w/h` se
|
||||
// clampean a ≥1 para evitar Extent3d de 0 (un viewport degenerado
|
||||
// no debería llegar acá, pero defensivo).
|
||||
let tex_w = (viewport.0.round() as u32).max(1);
|
||||
let tex_h = (viewport.1.round() as u32).max(1);
|
||||
let extent = wgpu::Extent3d {
|
||||
width: tex_w,
|
||||
height: tex_h,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
let fmt = self.pipelines.color_format;
|
||||
// Color attachment multisample: lo rasterizan los 4 pipelines.
|
||||
let msaa_tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("llimphi-raster-gpu-msaa"),
|
||||
size: extent,
|
||||
mip_level_count: 1,
|
||||
sample_count: MSAA_SAMPLES,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: fmt,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
// Scratch single-sample: recibe el resolve del MSAA y luego se
|
||||
// samplea en el composite sobre el `view`.
|
||||
let resolve_tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("llimphi-raster-gpu-resolve"),
|
||||
size: extent,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: fmt,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let resolve_view =
|
||||
resolve_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
// Pase de primitivos: MSAA cleared a TRANSPARENT, resuelto al
|
||||
// scratch single-sample. El scratch queda con alpha
|
||||
// **premultiplicado** (el blending alfa de los pipelines sobre
|
||||
// fondo transparente produce `rgb = color*alpha`, `a = alpha`).
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("llimphi-raster-gpu-pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &msaa_view,
|
||||
resolve_target: Some(&resolve_view),
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_bind_group(0, &bind_group, &[]);
|
||||
|
||||
// Orden de draws: rects (fondo) → discos → tris → lines (encima).
|
||||
// Match de la convención usual "fill abajo, stroke arriba".
|
||||
if let Some(buf) = rects_buf.as_ref() {
|
||||
pass.set_pipeline(&self.pipelines.rects);
|
||||
pass.set_vertex_buffer(0, buf.slice(..));
|
||||
pass.draw(0..6, 0..self.rect_count);
|
||||
}
|
||||
if let Some(buf) = discs_buf.as_ref() {
|
||||
pass.set_pipeline(&self.pipelines.discs);
|
||||
pass.set_vertex_buffer(0, buf.slice(..));
|
||||
pass.draw(0..6, 0..self.disc_count);
|
||||
}
|
||||
if let Some(buf) = tris_buf.as_ref() {
|
||||
pass.set_pipeline(&self.pipelines.tris);
|
||||
pass.set_vertex_buffer(0, buf.slice(..));
|
||||
pass.draw(0..self.tri_vert_count, 0..1);
|
||||
}
|
||||
if let Some(buf) = lines_buf.as_ref() {
|
||||
pass.set_pipeline(&self.pipelines.lines);
|
||||
pass.set_vertex_buffer(0, buf.slice(..));
|
||||
pass.draw(0..6, 0..self.line_count);
|
||||
}
|
||||
}
|
||||
|
||||
// Composite del scratch resuelto sobre el `view`. Respeta el
|
||||
// `load_op` recibido:
|
||||
// - `Load` → alpha-over: preserva lo que ya está en `view`
|
||||
// (vello), exactamente como el viejo pase directo.
|
||||
// - `Clear` → limpia `view` al color pedido y luego compone
|
||||
// el scratch encima (mismo resultado que limpiar y
|
||||
// dibujar directo).
|
||||
let composite_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("llimphi-raster-gpu-composite-bg"),
|
||||
layout: &self.pipelines.composite_bgl,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&resolve_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(
|
||||
&self.pipelines.composite_sampler,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
let mut cpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("llimphi-raster-gpu-composite-pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
// El blend del pipeline ya hace el alpha-over; con
|
||||
// Load conserva el fondo, con Clear lo borra primero.
|
||||
load: load_op,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
@@ -422,25 +761,9 @@ impl<'a> GpuBatch<'a> {
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_bind_group(0, &bind_group, &[]);
|
||||
|
||||
// Orden de draws: rects (fondo) → tris → lines (encima). Match
|
||||
// de la convención usual "fill abajo, stroke arriba".
|
||||
if let Some(buf) = rects_buf.as_ref() {
|
||||
pass.set_pipeline(&self.pipelines.rects);
|
||||
pass.set_vertex_buffer(0, buf.slice(..));
|
||||
pass.draw(0..6, 0..self.rect_count);
|
||||
}
|
||||
if let Some(buf) = tris_buf.as_ref() {
|
||||
pass.set_pipeline(&self.pipelines.tris);
|
||||
pass.set_vertex_buffer(0, buf.slice(..));
|
||||
pass.draw(0..self.tri_vert_count, 0..1);
|
||||
}
|
||||
if let Some(buf) = lines_buf.as_ref() {
|
||||
pass.set_pipeline(&self.pipelines.lines);
|
||||
pass.set_vertex_buffer(0, buf.slice(..));
|
||||
pass.draw(0..6, 0..self.line_count);
|
||||
}
|
||||
cpass.set_pipeline(&self.pipelines.composite);
|
||||
cpass.set_bind_group(0, &composite_bg, &[]);
|
||||
cpass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,8 +869,104 @@ fn vs_lines(
|
||||
return out;
|
||||
}
|
||||
|
||||
// -------- discos/anillos: 1 instancia = (cxcy, r/stroke, rgba) --------
|
||||
//
|
||||
// Quad que cubre el disco con 1.5 px de margen (para que el smoothstep
|
||||
// del borde no se recorte). El VS pasa al FS la posición local en px
|
||||
// relativa al centro; el FS evalúa el SDF del círculo y hace smoothstep
|
||||
// sobre `fwidth` → borde antialiased. `stroke > 0` recorta un anillo.
|
||||
|
||||
struct DiscV2F {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) local: vec2<f32>, // px relativos al centro
|
||||
@location(2) params: vec2<f32>, // r, stroke (px)
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_discs(
|
||||
@builtin(vertex_index) vid: u32,
|
||||
@location(0) inst_c: vec2<f32>,
|
||||
@location(1) inst_rs: vec2<f32>,
|
||||
@location(2) inst_rgba: u32,
|
||||
) -> DiscV2F {
|
||||
var corners = array<vec2<f32>, 6>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>( 1.0, -1.0),
|
||||
vec2<f32>( 1.0, 1.0),
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>( 1.0, 1.0),
|
||||
vec2<f32>(-1.0, 1.0),
|
||||
);
|
||||
let r = inst_rs.x;
|
||||
let margin = r + 1.5; // 1.5 px de aire para el AA del borde
|
||||
let local = corners[vid] * margin;
|
||||
let px = inst_c + local;
|
||||
var out: DiscV2F;
|
||||
out.pos = vec4<f32>(px_to_ndc(px), 0.0, 1.0);
|
||||
out.color = unpack_rgba(inst_rgba);
|
||||
out.local = local;
|
||||
out.params = inst_rs;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_disc(in: DiscV2F) -> @location(0) vec4<f32> {
|
||||
let r = in.params.x;
|
||||
let stroke = in.params.y;
|
||||
let dist = length(in.local); // distancia al centro en px
|
||||
// Ancho del filtro AA en px (≈ 1 px en pantalla).
|
||||
let aa = fwidth(dist);
|
||||
// Borde exterior: cobertura 1 dentro de r, 0 fuera de r+aa.
|
||||
var cov = 1.0 - smoothstep(r - aa, r + aa, dist);
|
||||
// Anillo: si hay stroke, recortamos el agujero interior con AA.
|
||||
if (stroke > 0.0) {
|
||||
let inner = max(r - stroke, 0.0);
|
||||
cov = cov * smoothstep(inner - aa, inner + aa, dist);
|
||||
}
|
||||
if (cov <= 0.0) {
|
||||
discard;
|
||||
}
|
||||
return vec4<f32>(in.color.rgb, in.color.a * cov);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs(in: V2F) -> @location(0) vec4<f32> {
|
||||
return in.color;
|
||||
}
|
||||
|
||||
// -------- composite: blit fullscreen de la scratch resuelta del MSAA ----
|
||||
//
|
||||
// Triángulo de pantalla completa (3 vértices, sin vertex buffer). Samplea
|
||||
// la scratch (alpha **premultiplicado**) y la emite tal cual; el alpha-over
|
||||
// real lo hace el BlendState del pipeline (`One, OneMinusSrcAlpha`).
|
||||
|
||||
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
||||
@group(0) @binding(1) var src_samp: sampler;
|
||||
|
||||
struct CompV2F {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_composite(@builtin(vertex_index) vid: u32) -> CompV2F {
|
||||
// Triángulo gigante que cubre el viewport (técnica estándar).
|
||||
var uvs = array<vec2<f32>, 3>(
|
||||
vec2<f32>(0.0, 0.0),
|
||||
vec2<f32>(2.0, 0.0),
|
||||
vec2<f32>(0.0, 2.0),
|
||||
);
|
||||
let uv = uvs[vid];
|
||||
var out: CompV2F;
|
||||
out.pos = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
|
||||
// El framebuffer tiene Y hacia abajo; la textura, hacia arriba en UV.
|
||||
out.uv = vec2<f32>(uv.x, 1.0 - uv.y);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_composite(in: CompV2F) -> @location(0) vec4<f32> {
|
||||
return textureSample(src_tex, src_samp, in.uv);
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -10,6 +10,13 @@ use llimphi_hal::{Frame, Hal};
|
||||
pub use vello;
|
||||
pub use vello::kurbo;
|
||||
pub use vello::peniko;
|
||||
// Renderer "hybrid" CPU+GPU sin compute shaders (feature `hybrid`):
|
||||
// vello 0.7 trae `vello_hybrid::Renderer` como alternativa al `vello::Renderer`
|
||||
// estándar — sin compute, mejor compat WebGL2 + Adreno/Mali viejas. Lo
|
||||
// re-exportamos cuando la feature está activa para que apps avanzadas (web,
|
||||
// móvil entry-level) puedan instanciarlo sin agregar otra dep.
|
||||
#[cfg(feature = "hybrid")]
|
||||
pub use vello_hybrid;
|
||||
|
||||
pub mod gpu;
|
||||
pub use gpu::{GpuBatch, GpuPipelines};
|
||||
|
||||
Reference in New Issue
Block a user