facett-core 0.1.7

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **Shared offscreen colour+depth target + blit-into-egui helper** (feature
//! `wgpu`) — the CONS-CORE Phase C extraction of the bespoke offscreen plumbing
//! that lived in `facett-map3d`'s `Map3dRenderer` (`ensure_targets` + the blit
//! pipeline + the widget scissor).
//!
//! Why a skin needs this: egui's own render pass is **colour-only** (no depth
//! attachment), so a depth-tested 3D pass cannot draw straight into it — it must
//! render into an **owned offscreen colour + depth32 target**, then **sample (blit)**
//! that colour into egui's pass. This helper owns:
//!
//! - the offscreen colour texture (+ view) and the depth texture (+ view),
//!   (re)created when the pane size changes ([`OffscreenColorDepth::ensure`]);
//! - the blit pipeline + sampler + bind group that sample the colour into egui's
//!   pass ([`OffscreenColorDepth::blit`]);
//! - the **widget scissor** math ([`gpu_scissor_px`] / [`scissor_intersection`] /
//!   [`ScissorPx`]) — the GPU twin of the CPU painter's rect clip, so nothing the 3D
//!   pass paints can escape the widget rect (the clip bug oracle on the GPU lane).
//!
//! The skin keeps its own **mesh pipeline + shader + uniforms** and records its
//! depth-tested pass into [`OffscreenColorDepth::color_view`] /
//! [`OffscreenColorDepth::depth_view`]; this helper provides only the targets + the
//! blit. The depth+colour **formats are fixed** ([`DEPTH_FORMAT`] /
//! [`OFFSCREEN_FORMAT`]) so a skin's mesh pipeline matches the targets it draws into.

use wgpu::TextureFormat;

/// The offscreen depth attachment format — `Depth32Float`, the format the skins'
/// mesh pipelines declare for the `Less` depth test.
pub const DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
/// The offscreen colour attachment format — `Rgba8Unorm`, sampled by the blit.
pub const OFFSCREEN_FORMAT: TextureFormat = TextureFormat::Rgba8Unorm;

/// The blit shader (fullscreen triangle sampling the offscreen colour), byte-for-byte
/// the `blit_vs`/`blit_fs` map3d used. Self-contained so the helper owns its blit.
pub const BLIT_WGSL: &str = r#"
@group(0) @binding(0) var blit_tex: texture_2d<f32>;
@group(0) @binding(1) var blit_smp: sampler;

struct BlitOut {
    @builtin(position) clip: vec4<f32>,
    @location(0) uv: vec2<f32>,
};

@vertex
fn blit_vs(@builtin(vertex_index) vi: u32) -> BlitOut {
    var p = array<vec2<f32>, 3>(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>( 3.0, -1.0),
        vec2<f32>(-1.0,  3.0),
    );
    var out: BlitOut;
    let xy = p[vi];
    out.clip = vec4<f32>(xy, 0.0, 1.0);
    out.uv = vec2<f32>(xy.x * 0.5 + 0.5, 1.0 - (xy.y * 0.5 + 0.5));
    return out;
}

@fragment
fn blit_fs(in: BlitOut) -> @location(0) vec4<f32> {
    return textureSample(blit_tex, blit_smp, in.uv);
}
"#;

/// The shared offscreen colour+depth target + blit pipeline. Construct once
/// ([`OffscreenColorDepth::new`], from the host's `target_format`); each frame call
/// [`ensure`](OffscreenColorDepth::ensure) for the pane size, record the skin's
/// depth-tested pass into the [`color_view`](OffscreenColorDepth::color_view) /
/// [`depth_view`](OffscreenColorDepth::depth_view), then [`blit`](OffscreenColorDepth::blit)
/// into egui's pass.
pub struct OffscreenColorDepth {
    blit_pipeline: wgpu::RenderPipeline,
    blit_bgl: wgpu::BindGroupLayout,
    sampler: wgpu::Sampler,

    color_tex: Option<wgpu::Texture>,
    color_view: Option<wgpu::TextureView>,
    depth_view: Option<wgpu::TextureView>,
    blit_bind: Option<wgpu::BindGroup>,
    size: (u32, u32),
}

impl OffscreenColorDepth {
    /// Build the blit pipeline + sampler for the host's `target_format` (egui's
    /// colour pass format). No targets are allocated until [`ensure`](Self::ensure).
    pub fn new(device: &wgpu::Device, target_format: TextureFormat) -> Self {
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("l0_offscreen_blit"),
            source: wgpu::ShaderSource::Wgsl(BLIT_WGSL.into()),
        });
        let blit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("l0_offscreen_blit_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 blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("l0_offscreen_blit_pipeline"),
            layout: Some(&device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("l0_offscreen_blit_pll"),
                bind_group_layouts: &[Some(&blit_bgl)],
                immediate_size: 0,
            })),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("blit_vs"),
                compilation_options: Default::default(),
                buffers: &[],
            },
            primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("blit_fs"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: target_format,
                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            multiview_mask: None,
            cache: None,
        });
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("l0_offscreen_blit_sampler"),
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            ..Default::default()
        });
        Self {
            blit_pipeline,
            blit_bgl,
            sampler,
            color_tex: None,
            color_view: None,
            depth_view: None,
            blit_bind: None,
            size: (0, 0),
        }
    }

    /// (Re)create the offscreen colour + depth targets for a pane of `w×h` physical
    /// pixels. Idempotent within a size — only reallocates when the pane resizes.
    /// Extracted verbatim from map3d's `ensure_targets`.
    pub fn ensure(&mut self, device: &wgpu::Device, w: u32, h: u32) {
        let w = w.max(1);
        let h = h.max(1);
        if self.size == (w, h) && self.color_view.is_some() {
            return;
        }
        let color = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("l0_offscreen_color"),
            size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: OFFSCREEN_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        let depth = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("l0_offscreen_depth"),
            size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: DEPTH_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        let color_view = color.create_view(&Default::default());
        let depth_view = depth.create_view(&Default::default());
        let blit_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("l0_offscreen_blit_bind"),
            layout: &self.blit_bgl,
            entries: &[
                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&color_view) },
                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
            ],
        });
        self.color_tex = Some(color);
        self.color_view = Some(color_view);
        self.depth_view = Some(depth_view);
        self.blit_bind = Some(blit_bind);
        self.size = (w, h);
    }

    /// The offscreen colour view to use as a skin's render-pass colour attachment.
    /// `None` before the first [`ensure`](Self::ensure).
    pub fn color_view(&self) -> Option<&wgpu::TextureView> {
        self.color_view.as_ref()
    }
    /// The offscreen depth view to use as a skin's depth-stencil attachment.
    pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
        self.depth_view.as_ref()
    }
    /// Whether targets are allocated (an `ensure` has run with a non-zero size).
    pub fn ready(&self) -> bool {
        self.blit_bind.is_some()
    }
    /// The current target size in physical pixels.
    pub fn size(&self) -> (u32, u32) {
        self.size
    }

    /// Blit the offscreen colour into the host's (egui) `render_pass`, **scissored to
    /// the widget rect** (the GPU twin of the CPU painter's `clip_poly_to_rect`).
    /// `info` is the paint callback's `PaintCallbackInfo`; the scissor is the
    /// intersection of its viewport and clip rect, so nothing lands outside the
    /// component. A no-op when the widget is fully clipped away or targets aren't
    /// ready. Returns the scissor it applied (zero-area ⇒ nothing painted) so a test
    /// can assert the clip on data.
    pub fn blit(&self, info: &egui::PaintCallbackInfo, render_pass: &mut wgpu::RenderPass<'static>) -> ScissorPx {
        let Some(blit_bind) = &self.blit_bind else { return ScissorPx { x: 0, y: 0, w: 0, h: 0 } };
        let s = gpu_scissor_px(info);
        if s.w == 0 || s.h == 0 {
            return s; // fully clipped — paint nothing
        }
        render_pass.set_scissor_rect(s.x, s.y, s.w, s.h);
        render_pass.set_pipeline(&self.blit_pipeline);
        render_pass.set_bind_group(0, blit_bind, &[]);
        render_pass.draw(0..3, 0..1);
        s
    }
}

/// A physical-pixel scissor rectangle (x, y top-left + w×h), the wgpu
/// `set_scissor_rect` argument. Moved from `facett-map3d::gpu` so the GPU rect-clip
/// math is shared + unit-testable without a device.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScissorPx {
    pub x: u32,
    pub y: u32,
    pub w: u32,
    pub h: u32,
}

/// Compute the GPU pass scissor (in physical pixels) for a paint callback: the
/// **intersection of the widget viewport and the clip rect**, clamped to the
/// framebuffer — the GPU twin of the CPU painter's `clip_poly_to_rect`. Pins the 3D
/// pass to the widget's own rectangle so nothing it draws can land outside the
/// component, regardless of the surrounding egui pass's clip rect.
pub fn gpu_scissor_px(info: &egui::PaintCallbackInfo) -> ScissorPx {
    let vp = info.viewport_in_pixels();
    let cl = info.clip_rect_in_pixels();
    scissor_intersection(
        (vp.left_px, vp.top_px, vp.width_px, vp.height_px),
        (cl.left_px, cl.top_px, cl.width_px, cl.height_px),
        info.screen_size_px,
    )
}

/// Intersect two `(left, top, width, height)` pixel rects and clamp to the
/// framebuffer `screen` size. Plain ints (no egui types) so the rect math is testable
/// with zero setup. Returns a zero-area rect when the two do not overlap.
pub fn scissor_intersection(
    viewport: (i32, i32, i32, i32),
    clip: (i32, i32, i32, i32),
    screen: [u32; 2],
) -> ScissorPx {
    let (vx, vy, vw, vh) = viewport;
    let (cx, cy, cw, ch) = clip;
    let left = vx.max(cx).max(0);
    let top = vy.max(cy).max(0);
    let right = (vx + vw).min(cx + cw).min(screen[0] as i32);
    let bottom = (vy + vh).min(cy + ch).min(screen[1] as i32);
    let w = (right - left).max(0) as u32;
    let h = (bottom - top).max(0) as u32;
    ScissorPx { x: left.max(0) as u32, y: top.max(0) as u32, w, h }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// INJECT-ASSERT (clip oracle, moved math): a widget fully inside the screen with
    /// a clip equal to it scissors to exactly that rect.
    #[test]
    fn scissor_equals_widget_when_clip_covers_it() {
        let s = scissor_intersection((50, 40, 200, 150), (50, 40, 200, 150), [640, 480]);
        assert_eq!(s, ScissorPx { x: 50, y: 40, w: 200, h: 150 });
    }

    /// INJECT-ASSERT (the bug oracle): when the surrounding clip is the WHOLE PANEL,
    /// the scissor is still pinned to the widget viewport — no ink outside the rect.
    #[test]
    fn scissor_pinned_to_widget_not_panel_clip() {
        let s = scissor_intersection((300, 200, 120, 90), (0, 0, 640, 480), [640, 480]);
        assert_eq!(s, ScissorPx { x: 300, y: 200, w: 120, h: 90 });
        assert!(s.x + s.w <= 420 && s.y + s.h <= 290, "no ink outside the widget rect");
    }

    /// INJECT-ASSERT: a partial overlap scissors to the intersection, clamped to the
    /// framebuffer.
    #[test]
    fn scissor_intersection_clamped() {
        let s = scissor_intersection((500, 400, 300, 300), (550, 420, 300, 300), [640, 480]);
        assert_eq!(s, ScissorPx { x: 550, y: 420, w: 90, h: 60 });
        assert!(s.x + s.w <= 640 && s.y + s.h <= 480);
    }

    /// INJECT-ASSERT: disjoint clip → zero-area scissor (paint nothing).
    #[test]
    fn scissor_zero_area_when_disjoint() {
        let s = scissor_intersection((0, 0, 100, 100), (300, 300, 50, 50), [640, 480]);
        assert_eq!(s.w, 0);
    }

    /// INJECT-ASSERT: `gpu_scissor_px` reads a real `PaintCallbackInfo` (panel-wide
    /// clip) and confines to the widget viewport — the exact value `blit` feeds
    /// `set_scissor_rect`.
    #[test]
    fn gpu_scissor_px_from_callback_info() {
        let info = egui::PaintCallbackInfo {
            viewport: egui::Rect::from_min_size(egui::pos2(40.0, 30.0), egui::vec2(200.0, 150.0)),
            clip_rect: egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(800.0, 600.0)),
            pixels_per_point: 1.0,
            screen_size_px: [800, 600],
        };
        let s = gpu_scissor_px(&info);
        assert_eq!(s, ScissorPx { x: 40, y: 30, w: 200, h: 150 });
    }

    /// INJECT-ASSERT: the offscreen colour+depth formats match what a depth-tested
    /// skin pipeline declares (so the skin's mesh pass attaches to these targets).
    #[test]
    fn offscreen_formats_are_the_depth_tested_pair() {
        assert_eq!(DEPTH_FORMAT, TextureFormat::Depth32Float);
        assert_eq!(OFFSCREEN_FORMAT, TextureFormat::Rgba8Unorm);
        assert!(BLIT_WGSL.contains("blit_vs") && BLIT_WGSL.contains("blit_fs"));
    }
}