facett-core 0.3.0

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
//! **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 — `Rgba16Float`, an **HDR** target so a
/// skin can write linear radiance **above 1.0** (emissive windows, sun glints) that
/// a post stack (bright-pass → bloom → ACES tonemap, see `facett-map3d`'s post
/// passes) reads before it is tonemapped down to the 8-bit egui surface. The blit
/// (or a skin's tonemap pass) samples it as a filterable float texture.
pub const OFFSCREEN_FORMAT: TextureFormat = TextureFormat::Rgba16Float;

/// 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>,
    /// A SECOND view of the depth texture, for SAMPLING it (the water/SSR/depth-fog
    /// passes) — separate from the attachment view so a pass can read the scene
    /// depth a previous pass wrote.
    depth_sample_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), compositing the offscreen with straight
    /// [`BlendState::ALPHA_BLENDING`](wgpu::BlendState::ALPHA_BLENDING) — right for a
    /// skin whose pass writes an **opaque** scene (`alpha = 1`), which is every 3D
    /// skin. No targets are allocated until [`ensure`](Self::ensure).
    ///
    /// A pass that writes **premultiplied** colour (an additive glow overlay, where
    /// `alpha` is coverage rather than 1) must not go through this one: alpha-blending
    /// premultiplied colour multiplies by alpha a second time, so the falloff comes
    /// out as `a²` and a faint particle halo vanishes. Use
    /// [`new_with_blend`](Self::new_with_blend) with
    /// `PREMULTIPLIED_ALPHA_BLENDING` for those.
    pub fn new(device: &wgpu::Device, target_format: TextureFormat) -> Self {
        Self::new_with_blend(device, target_format, wgpu::BlendState::ALPHA_BLENDING)
    }

    /// [`new`](Self::new) with an explicit blit blend mode — the ONE pipeline
    /// construction, parameterised rather than copied (LAW 5). See `new`'s note for
    /// which mode a pass needs.
    pub fn new_with_blend(device: &wgpu::Device, target_format: TextureFormat, blend: wgpu::BlendState) -> 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: crate::render::gpu::msaa_state(),
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("blit_fs"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: target_format,
                    blend: Some(blend),
                    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,
            depth_sample_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`.
    ///
    /// Each side is clamped to the device's `max_texture_dimension_2d`
    /// ([`super::texture_extent`]): a pane wider than the limit is rendered into the
    /// clamped target and the blit stretches it over the pane — never a panic.
    pub fn ensure(&mut self, device: &wgpu::Device, w: u32, h: u32) {
        let (w, h) = super::texture_extent(device, w, h);
        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: crate::render::gpu::NO_MSAA_SAMPLES,
            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: crate::render::gpu::NO_MSAA_SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: DEPTH_FORMAT,
            // TEXTURE_BINDING so a skin's water/SSR/depth-fog pass can SAMPLE the
            // scene depth (the reflective-water depth-tint reads the bottom's depth
            // under each water fragment). RENDER_ATTACHMENT for the depth test.
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        let color_view = color.create_view(&Default::default());
        let depth_view = depth.create_view(&Default::default());
        let depth_sample_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.depth_sample_view = Some(depth_sample_view);
        self.blit_bind = Some(blit_bind);
        self.size = (w, h);
    }

    /// A sampleable view of the offscreen **depth** texture (`Depth32Float`,
    /// `texture_depth_2d` in WGSL) — for a skin's water / SSR / depth-fog pass to
    /// read the scene depth a previous pass wrote. `None` before the first
    /// [`ensure`](Self::ensure).
    pub fn depth_sample_view(&self) -> Option<&wgpu::TextureView> {
        self.depth_sample_view.as_ref()
    }

    /// 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.
    /// **A pane wider than the device's texture limit never panics** (the loki crash,
    /// 2026-10-16: 10 604 px > 8 192): the targets are clamped to the limit, the blit
    /// scales them over the pane. RED before the clamp — wgpu's validation panic.
    #[test]
    fn a_pane_wider_than_the_texture_limit_is_clamped_not_a_panic() {
        assert_eq!(super::super::clamp_extent(8192, 10_604, 900), (8192, 900));
        assert_eq!(super::super::clamp_extent(8192, 0, 0), (1, 1));
        use crate::render::gputurn::probe;
        let Some((device, _queue, _turn)) = probe::open("l0-offscreen-limit", probe::OnSoftware::Refuse, probe::downlevel).map(|p| p.split()) else {
            eprintln!("[offscreen] no GPU adapter — skipping the device leg");
            return;
        };
        let max = device.limits().max_texture_dimension_2d;
        let mut off = OffscreenColorDepth::new(&device, TextureFormat::Bgra8Unorm);
        off.ensure(&device, max + 2412, 600);
        assert_eq!(off.size(), (max, 600), "clamped to the device limit");
        assert!(off.ready() && off.color_view().is_some());
        off.ensure(&device, 300, max * 2);
        assert_eq!(off.size(), (300, max));
    }

    #[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::Rgba16Float);
        assert!(BLIT_WGSL.contains("blit_vs") && BLIT_WGSL.contains("blit_fs"));
    }

    /// INJECT-ASSERT (water prereq): the depth texture is created with BOTH
    /// RENDER_ATTACHMENT (the depth test) and TEXTURE_BINDING (so the water /
    /// depth-fog pass can SAMPLE the scene depth). A unit check on the usage flags
    /// the GPU-less matrix can read — the actual sampling is proven in the wgpu
    /// water blob oracle.
    #[test]
    fn depth_target_is_render_attachment_and_sampleable() {
        let usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
        assert!(usage.contains(wgpu::TextureUsages::RENDER_ATTACHMENT), "depth is a render target");
        assert!(usage.contains(wgpu::TextureUsages::TEXTURE_BINDING), "depth is sampleable for water");
    }
}