facett-graph3d/src/logo3d/gpu.rs
//! GPU render path for the 3D znippy rabbit logo — a small, **depth-tested**,
//! lit `egui_wgpu` paint callback. Mirrors the structure of facett-map's GPU
//! kernel (a `*Renderer` stored in `callback_resources`, installed once via
//! [`install_renderer`], driven by a [`CallbackTrait`](egui_wgpu::CallbackTrait)),
//! but draws an indexed triangle mesh with a real Z-buffer and Lambert shading
//! instead of culled line ways.
//!
//! Upload-once: the rabbit mesh is uploaded to VRAM on the first paint; per-frame
//! only the rotation uniform (yaw/tilt) + accent colour change.
//!
//! CONS-CORE Phase C (step 10): the logo now renders into the **shared
//! [`OffscreenColorDepth`](facett_core::render::gpu::OffscreenColorDepth)** colour +
//! depth32 target (a real GPU Z-buffer — `CompareFunction::Less`) and **blits** that
//! into egui's colour-only pass, exactly like `facett-map3d`. This is the parity
//! fold: before, the logo drew straight into egui's pass and relied on back-face
//! culling + CCW winding for occlusion (egui supplies no depth attachment); now it
//! shares map3d's offscreen+depth+blit helper, so a concave/self-occluding mesh gets
//! true per-pixel occlusion. The CPU painter remains the headless / no-GPU fallback
//! (the snapshot golden path), unchanged.
#![allow(clippy::too_many_lines, reason = "one self-contained lit pipeline")]
use egui_wgpu::wgpu;
use facett_core::rabbit::RabbitMesh;
use facett_core::render::gpu::offscreen::{OffscreenColorDepth, DEPTH_FORMAT, OFFSCREEN_FORMAT};
/// One mesh vertex: position + normal. Matches `VsIn` in `logo.wgsl`.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
pub pos: [f32; 3],
pub nrm: [f32; 3],
}
/// Per-frame uniform: yaw/tilt rotation + accent colour. 32 bytes.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
yaw: f32,
tilt: f32,
scale: f32,
_pad0: f32,
accent: [f32; 3],
_pad1: f32,
}
/// CPU-side mesh payload uploaded once.
pub struct GpuGeometry {
pub vertices: Vec<Vertex>,
pub indices: Vec<u32>,
}
/// Build the upload payload from the extruded [`RabbitMesh`].
pub fn build_gpu_geometry(mesh: &RabbitMesh) -> GpuGeometry {
let vertices = mesh
.positions
.iter()
.zip(mesh.normals.iter())
.map(|(&pos, &nrm)| Vertex { pos, nrm })
.collect();
GpuGeometry { vertices, indices: mesh.indices.clone() }
}
/// Persistent GPU resources for the logo.
pub struct LogoRenderer {
pipeline: wgpu::RenderPipeline,
uniform: wgpu::Buffer,
bind: wgpu::BindGroup,
vbuf: Option<wgpu::Buffer>,
ibuf: Option<wgpu::Buffer>,
index_count: u32,
/// Shared offscreen colour+depth target + blit (CONS-CORE L0; the parity fold
/// with facett-map3d). The mesh draws into this with a real depth test, then it
/// blits into egui's pass.
offscreen: OffscreenColorDepth,
}
impl LogoRenderer {
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("facett_logo3d"),
source: wgpu::ShaderSource::Wgsl(include_str!("logo.wgsl").into()),
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("logo_uniforms"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("logo_bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("logo_bind"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("logo_pl"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("logo_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3],
})],
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: Some(wgpu::Face::Back),
front_face: wgpu::FrontFace::Ccw,
..Default::default()
},
// CONS-CORE Phase C: the logo now draws into the shared offscreen
// colour+depth32 target with a real GPU Z-buffer (`Less`), so concave
// self-occluding geometry occludes per-pixel (the parity fold with
// map3d). The CPU painter (headless snapshot) still does the full
// painter's-algorithm sort, unchanged.
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: facett_core::render::gpu::msaa_state(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs"),
// Render into the offscreen colour target (then blit to egui).
targets: &[Some(wgpu::ColorTargetState {
format: OFFSCREEN_FORMAT,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
});
let offscreen = OffscreenColorDepth::new(device, target_format);
Self { pipeline, uniform, bind, vbuf: None, ibuf: None, index_count: 0, offscreen }
}
fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, geo: &GpuGeometry) {
use wgpu::util::DeviceExt;
// Idempotent: keep the first uploaded mesh. The host sends geometry every
// frame (so whichever frame's callback prepare runs first does the upload —
// robust under egui_kittest's run_steps); this guard makes the later frames'
// geometry a no-op. Matches facett-map3d's upload contract.
if geo.vertices.is_empty() || self.vbuf.is_some() {
return;
}
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("logo_vbuf"),
contents: bytemuck::cast_slice(&geo.vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let ibuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("logo_ibuf"),
contents: bytemuck::cast_slice(&geo.indices),
usage: wgpu::BufferUsages::INDEX,
});
let _ = queue;
self.vbuf = Some(vbuf);
self.ibuf = Some(ibuf);
self.index_count = geo.indices.len() as u32;
}
}
/// The paint callback for one logo frame.
pub struct LogoPaintCallback {
/// `Some` on the first frame to upload the mesh; `None` afterwards.
pub geometry: Option<Box<GpuGeometry>>,
pub yaw: f32,
pub tilt: f32,
/// Accent colour `[r, g, b]` (0..255).
pub accent: [u8; 3],
/// Pane size in physical px (the offscreen target size). When `0`, the prepare
/// step falls back to the egui screen-descriptor size.
pub px_w: u32,
pub px_h: u32,
}
impl egui_wgpu::CallbackTrait for LogoPaintCallback {
fn prepare(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
sd: &egui_wgpu::ScreenDescriptor,
encoder: &mut wgpu::CommandEncoder,
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let Some(renderer) = resources.get_mut::<LogoRenderer>() else {
return vec![];
};
if let Some(geo) = &self.geometry {
renderer.upload(device, queue, geo);
}
// Clip space is [-1,1]; a constant scale keeps the [-1,1] design-box logo
// nicely framed in the pane.
let scale = 0.7;
let accent = [self.accent[0] as f32 / 255.0, self.accent[1] as f32 / 255.0, self.accent[2] as f32 / 255.0];
queue.write_buffer(
&renderer.uniform,
0,
bytemuck::bytes_of(&Uniforms { yaw: self.yaw, tilt: self.tilt, scale, _pad0: 0.0, accent, _pad1: 0.0 }),
);
// Ensure the shared offscreen colour+depth target for this pane.
let (w, h) = if self.px_w > 0 && self.px_h > 0 {
(self.px_w, self.px_h)
} else {
(sd.size_in_pixels[0], sd.size_in_pixels[1])
};
renderer.offscreen.ensure(device, w, h);
let (Some(vbuf), Some(ibuf), Some(color_view), Some(depth_view)) = (
&renderer.vbuf,
&renderer.ibuf,
renderer.offscreen.color_view(),
renderer.offscreen.depth_view(),
) else {
return vec![];
};
if renderer.index_count == 0 {
return vec![];
}
// ── the offscreen depth-tested pass: real per-pixel Z-buffer occlusion ──
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("logo3d_offscreen_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
// Transparent clear so the blit composites the logo over egui's
// existing pane background (alpha-blended), matching the old
// straight-into-egui behaviour where the pane bg showed through.
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store }),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&renderer.pipeline);
pass.set_bind_group(0, &renderer.bind, &[]);
pass.set_vertex_buffer(0, vbuf.slice(..));
pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..renderer.index_count, 0, 0..1);
drop(pass);
vec![]
}
fn paint(
&self,
info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
resources: &egui_wgpu::CallbackResources,
) {
let Some(renderer) = resources.get::<LogoRenderer>() else { return };
if renderer.index_count == 0 {
return;
}
// Blit the depth-resolved offscreen colour into egui's pass, scissored to the
// widget rect (the shared L0 helper — the same path map3d uses).
let _scissor = renderer.offscreen.blit(&info, render_pass);
}
}
/// Install a [`LogoRenderer`] into an egui-wgpu `RenderState`'s callback
/// resources (call once at host startup, like facett-map's `install_renderer`).
pub fn install_renderer(render_state: &egui_wgpu::RenderState) {
// The shared install-once writer (LAW #5) — it also registers this lane with
// `facett_core::render::lane`, so an About box can report that a GPU lane exists.
let _ = facett_core::render::gpu::install_renderer(render_state, LogoRenderer::new);
}