facett-core 0.1.16

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **The in-GPU pass clock** — device timestamps around a lane's encoded passes,
//! resolved through a small non-blocking readback ring into
//! [`crate::render::lane`]'s per-lane clock stats.
//!
//! # Why this exists
//!
//! korp clocks the CPU legs of a mousewheel→pixels TRACE line itself; the one leg no
//! host can see is what the GPU actually spent executing the lane's passes. wgpu's
//! `TIMESTAMP_QUERY` is the only honest source for that number — a wall clock around
//! `queue.submit` measures driver queuing, not execution.
//!
//! # The honesty rules (LAW 2 shapes, written down)
//!
//! * **Feature-detect, never fake.** A device without `TIMESTAMP_QUERY` (llvmpipe
//!   sometimes, every kittest default harness — its `DeviceDescriptor` requests no
//!   features) gets `last_gpu_pass_us = None` and `gpu_timestamps = Some(false)`.
//!   Never `0`: a zero here is indistinguishable from an infinitely fast GPU, the
//!   identity-value trap.
//! * **Never stall the frame.** The two timestamps are resolved into a buffer and
//!   copied to one of [`CLOCK_RING`] staging slots; the slot is `map_async`'d the
//!   *next* frame (after egui has submitted the encoder) and read the frame after
//!   that. A sample therefore lands ~2–3 frames late, and that is the contract —
//!   `last_gpu_pass_us` is "a recent frame", not "this frame". No `device.poll(Wait)`
//!   anywhere on the live path.
//! * **A zero delta is not a sample.** Two equal timestamps mean the device's
//!   counter did not tick across the window (or a discarded encoder left a stale
//!   slot); reporting it would publish `0 µs` as a measurement. It is skipped, and
//!   the previous real sample stands.
//!
//! # What it costs when idle
//!
//! A lane on a featureless device holds a [`PassClockSlot::Unavailable`] — one enum
//! check per frame. On a device WITH the feature: two `write_timestamp`s, one
//! 16-byte resolve+copy, one `map_async` (its boxed callback is the only per-frame
//! allocation on the active path) and one mutex-guarded stats note per resolved
//! sample.

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use egui_wgpu::wgpu;

/// Staging slots in flight. 3 covers the record → submit → map → read pipeline with
/// one spare, so a slow mapping never blocks arming the next frame (that frame just
/// goes unmeasured — skipping a sample is cheaper than stalling a frame).
const CLOCK_RING: usize = 3;

/// Two u64 timestamps.
const QUERY_BYTES: u64 = 16;

/// The features the clock needs: `TIMESTAMP_QUERY` for the query set + resolve, and
/// `TIMESTAMP_QUERY_INSIDE_ENCODERS` for `CommandEncoder::write_timestamp` — the
/// encoder-level write is what lets ONE clock bracket a lane's whole recorded work
/// (3D: shadow→post, many conditional passes) without threading `timestamp_writes`
/// through every pass descriptor.
pub const PASS_CLOCK_FEATURES: wgpu::Features = wgpu::Features::TIMESTAMP_QUERY
    .union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS);

/// Where one staging slot is in the record→map→read pipeline.
enum SlotState {
    /// Not in flight — may be armed for this frame's copy.
    Free,
    /// A resolve+copy into this slot was recorded; `map_async` next frame (by then
    /// egui has submitted the encoder — mapping before submission is a validation
    /// error waiting to happen).
    Copied,
    /// `map_async` issued; `mapped` flips true when the callback fires.
    Mapping,
}

struct ClockSlot {
    staging: wgpu::Buffer,
    state: SlotState,
    /// Set by the `map_async` callback (fired during `device.poll`); read on the
    /// following frame. `Arc` because the callback outlives the borrow.
    mapped: Arc<AtomicBool>,
}

/// One lane's pass clock. Construct through [`PassClockSlot::get_or_init`] so the
/// feature probe (and its honest `note_gpu_timestamps` verdict) cannot be skipped.
pub struct PassClock {
    /// The lane the samples are filed under (`lane_name()` of the owning renderer).
    lane: &'static str,
    query_set: wgpu::QuerySet,
    /// `QUERY_RESOLVE | COPY_SRC` — the resolve target the per-slot copies read from.
    resolve_buf: wgpu::Buffer,
    /// Nanoseconds per timestamp tick (`Queue::get_timestamp_period`), captured once.
    period_ns: f32,
    slots: [ClockSlot; CLOCK_RING],
    /// The slot this frame's `end` will copy into (`begin` armed it), if any.
    armed: Option<usize>,
}

impl PassClock {
    /// Does this DEVICE carry what the clock needs? Checked against the device, not
    /// the adapter: a device *requested without* the feature must read unsupported
    /// even on a 4090 — that is the test's forced-unavailable arm, and the live
    /// kittest-default-harness shape.
    #[must_use]
    pub fn supported(device: &wgpu::Device) -> bool {
        device.features().contains(PASS_CLOCK_FEATURES)
    }

    /// Build against a device that [`supported`](Self::supported) said yes to.
    /// Private on purpose — [`PassClockSlot::get_or_init`] is the one constructor
    /// path, so the probe verdict always reaches the lane registry.
    fn new(device: &wgpu::Device, queue: &wgpu::Queue, lane: &'static str) -> Self {
        let query_set = device.create_query_set(&wgpu::QuerySetDescriptor {
            label: Some("lane_pass_clock"),
            ty: wgpu::QueryType::Timestamp,
            count: 2,
        });
        let resolve_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("lane_pass_clock_resolve"),
            size: QUERY_BYTES,
            usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
            mapped_at_creation: false,
        });
        let slots = std::array::from_fn(|_| ClockSlot {
            staging: device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("lane_pass_clock_staging"),
                size: QUERY_BYTES,
                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
                mapped_at_creation: false,
            }),
            state: SlotState::Free,
            mapped: Arc::new(AtomicBool::new(false)),
        });
        Self { lane, query_set, resolve_buf, period_ns: queue.get_timestamp_period(), slots, armed: None }
    }

    /// **Start of the lane's encoded work.** Advances the ring (reads any sample
    /// whose mapping completed → `note_gpu_pass`; issues `map_async` for last
    /// frame's copy), then writes the opening timestamp if a free slot exists.
    ///
    /// The `device.poll(Poll)` is non-blocking — it only pumps callbacks that are
    /// already done. Returns whether this frame is being measured (a full ring says
    /// no, and the frame simply goes unmeasured).
    pub fn begin(&mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder) -> bool {
        let _ = device.poll(wgpu::PollType::Poll);
        for slot in &mut self.slots {
            match slot.state {
                SlotState::Mapping if slot.mapped.load(Ordering::Acquire) => {
                    {
                        let data = slot.staging.slice(..).get_mapped_range();
                        let ts: &[u64] = bytemuck::cast_slice(&data);
                        let delta_ns =
                            (ts[1].saturating_sub(ts[0]) as f64) * f64::from(self.period_ns);
                        // Ceiling to µs so a real sub-microsecond pass reads 1, not 0
                        // — and a zero DELTA (counter never ticked / stale slot from a
                        // discarded encoder) is not published at all; see module doc.
                        let us = (delta_ns / 1000.0).ceil() as u64;
                        if us > 0 {
                            crate::render::lane::note_gpu_pass(self.lane, us);
                        }
                    }
                    slot.staging.unmap();
                    slot.state = SlotState::Free;
                }
                SlotState::Copied => {
                    let flag = Arc::clone(&slot.mapped);
                    flag.store(false, Ordering::Release);
                    slot.staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
                        if r.is_ok() {
                            flag.store(true, Ordering::Release);
                        }
                    });
                    slot.state = SlotState::Mapping;
                }
                _ => {}
            }
        }
        self.armed = self.slots.iter().position(|s| matches!(s.state, SlotState::Free));
        if self.armed.is_some() {
            encoder.write_timestamp(&self.query_set, 0);
        }
        self.armed.is_some()
    }

    /// **End of the lane's encoded work**: closing timestamp, resolve, copy into the
    /// armed slot. No-op when [`begin`](Self::begin) found no free slot (or was never
    /// called this frame) — an early-returned `prepare` leaves nothing dangling.
    pub fn end(&mut self, encoder: &mut wgpu::CommandEncoder) {
        let Some(i) = self.armed.take() else { return };
        encoder.write_timestamp(&self.query_set, 1);
        encoder.resolve_query_set(&self.query_set, 0..2, &self.resolve_buf, 0);
        encoder.copy_buffer_to_buffer(&self.resolve_buf, 0, &self.slots[i].staging, 0, QUERY_BYTES);
        self.slots[i].state = SlotState::Copied;
    }
}

/// **A lane's lazily-probed clock slot** — the field a renderer embeds.
///
/// Lazy because a renderer is constructed from a device alone
/// (`install_renderer`'s `make(&device, format)`) while the clock also needs the
/// queue's timestamp period; the first `prepare` (which has both) probes once. The
/// probe's verdict is written to the lane registry either way, so "unavailable" is a
/// recorded fact, not an absence.
#[derive(Default)]
pub enum PassClockSlot {
    /// No `prepare` has run yet — the lane registry still says `None` for this lane.
    #[default]
    Untried,
    /// Probed: the device lacks [`PASS_CLOCK_FEATURES`]. One enum check per frame
    /// from here on — the "~zero overhead when nobody can read" arm.
    Unavailable,
    /// Probed: timestamps are real and this clock is writing them.
    Ready(PassClock),
}

impl PassClockSlot {
    /// The clock, probing on first use. `None` = this device cannot measure (already
    /// recorded as `gpu_timestamps = Some(false)` for `lane`).
    pub fn get_or_init(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        lane: &'static str,
    ) -> Option<&mut PassClock> {
        if matches!(self, PassClockSlot::Untried) {
            *self = if PassClock::supported(device) {
                crate::render::lane::note_gpu_timestamps(lane, true);
                PassClockSlot::Ready(PassClock::new(device, queue, lane))
            } else {
                crate::render::lane::note_gpu_timestamps(lane, false);
                PassClockSlot::Unavailable
            };
        }
        self.ready_mut()
    }

    /// The clock if the probe said yes — for the `end` half of a bracket, where
    /// re-probing would be wrong (state was decided at `begin`).
    pub fn ready_mut(&mut self) -> Option<&mut PassClock> {
        match self {
            PassClockSlot::Ready(c) => Some(c),
            _ => None,
        }
    }
}