// Dreamlet Catalog — GPU-resident scene manifest for the DVR.
//
// All scene objects are represented as GpuDreamlet entries in a persistent
// storage buffer. The CPU uploads the catalog once at scene load. Per-frame
// cost is zero for static scenes (only 96-byte camera uniform).
//
// The catalog serves as input to:
//   - dreamlet_visibility.wgsl (GPU compute cull + compact)
//   - dreamlet_physics.wgsl (GPU compute physics simulation)
//   - pbr_dreamlet.wgsl (fragment: PBR from storage buffer)
//
// This replaces per-object CPU data (GpuObjectData) with a unified
// dreamlet primitive that combines rendering, physics, and lifecycle.

/// GPU-resident dreamlet descriptor (144 bytes, std430).
/// Serves as both render primitive and physics body.
///
/// WGSL alignment: `target_position: vec3<f32>` requires align(16). In WGSL,
/// after `lifetime` at offset 100, next align(16) boundary is offset 112.
/// Rust `[f32; 3]` has align(4) and would start at 104, causing a 8-byte mismatch.
/// `_pad_target` fills the gap. Struct-level alignment pad `_pad_struct` reaches 144.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuDreamlet {
    // Transform (48 bytes)
    pub position: [f32; 3],    // world-space center
    pub scale: f32,            // uniform scale (pixel-scale minimum: 0.001)
    pub velocity: [f32; 3],    // m/s (zero for Static mode)
    pub angular_velocity: f32, // rad/s
    pub rotation: [f32; 4],    // quaternion (x, y, z, w)

    // Material (16 bytes)
    pub base_color: [f32; 4], // RGBA

    // Physics (16 bytes)
    pub roughness: f32,
    pub metallic: f32,
    pub mass: f32,
    pub restitution: f32,

    // State (16 bytes)
    pub state: u32,     // 0=static, 1=particle, 2=converging, 3=settled, 4=dissolving
    pub mesh_id: u32,   // index into mesh descriptor table
    pub parent_id: u32, // authority object ID (for debris tracking)
    pub flags: u32,     // bit 0: active, bit 1: physics_enabled, bit 2: promotable

    // Lifecycle (40 bytes — 8 bytes alignment padding for target_position vec3)
    pub age: f32,
    pub lifetime: f32,
    pub _pad_target: [f32; 2],     // align target_position to WGSL vec3 offset 112
    pub target_position: [f32; 3], // convergence target (WGSL vec3, align 16)
    pub deconstruct_level: u32,    // 0-2 nesting depth for destruction
    pub bounding_radius: f32,      // for visibility culling
    pub skeleton_index: u32,       // index into skeleton instance table (0 = no skeleton)
    pub _pad_struct: [f32; 2],     // pad to 144 bytes (WGSL struct align = 16, ceil(136/16)*16)
}

/// Dreamlet state constants.
pub const DREAMLET_STATE_STATIC: u32 = 0;
pub const DREAMLET_STATE_PARTICLE: u32 = 1;
pub const DREAMLET_STATE_CONVERGING: u32 = 2;
pub const DREAMLET_STATE_SETTLED: u32 = 3;
pub const DREAMLET_STATE_DISSOLVING: u32 = 4;

/// Dreamlet flags.
pub const DREAMLET_FLAG_ACTIVE: u32 = 1 << 0;
pub const DREAMLET_FLAG_PHYSICS: u32 = 1 << 1;
pub const DREAMLET_FLAG_PROMOTABLE: u32 = 1 << 2;
/// DL-11: Emissive light source — boosted in RT GI material upload.
pub const DREAMLET_FLAG_EMISSIVE: u32 = 1 << 3;

/// Maximum deconstruct nesting depth.
pub const MAX_DECONSTRUCT_LEVEL: u32 = 2;

// ── Quantum Bridge: CPU→GPU State Transfer ──────────────────────────

/// GPU-uploadable quantum context (128 bytes, uniform buffer).
/// Carries the pre-computed density matrix observables from CPU quantum simulation
/// to the GPU particle physics and culling shaders.
///
/// Quantum-to-GPU correspondence:
///   populations[5]     = normalized occupancy weights over 5 locomotion modes (ρ_kk diagonal)
///   coherences[20]     = off-diagonal density matrix entries projected into coupling coefficients
///   coherence_magnitude = total off-diagonal magnitude (Σ|ρ_ij| for i<j)
///   purity             = state purity Tr(ρ²) (1.0=pure, 0.2=maximally mixed)
///   decoherence_epsilon = Lindblad dephasing rate per frame
///   measured_mode       = Born measurement outcome (committed mode, analogous to hard attention)
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuQuantumContext {
    /// Normalized mode occupancy weights, sum to 1.0 (ρ_kk diagonal).
    pub populations: [f32; 5], // offset 0, 20 bytes
    /// Total off-diagonal coherence magnitude Σ|ρ_ij| for i<j.
    pub coherence_magnitude: f32, // offset 20
    /// Purity Tr(ρ²): 1.0 = pure state, 0.2 = maximally mixed.
    pub purity: f32, // offset 24
    /// Von Neumann entropy -Σ p_k ln(p_k).
    pub entropy: f32, // offset 28

    /// Off-diagonal density matrix entries as coupling coefficients (Re,Im pairs).
    /// Order: ρ_{01}, ρ_{02}, ρ_{03}, ρ_{04}, ρ_{12}, ρ_{13}, ρ_{14}, ρ_{23}, ρ_{24}, ρ_{34}.
    pub coherences: [f32; 20], // offset 32, 80 bytes

    /// Current dephasing rate (decoherence channel strength).
    pub decoherence_epsilon: f32, // offset 112
    /// Effective temperature 1/(1+coherence_magnitude).
    pub temperature: f32, // offset 116
    /// Accumulated quantum phase time.
    pub phase_time: f32, // offset 120
    /// Born rule measurement result (committed mode: argmax of populations).
    pub measured_mode: u32, // offset 124

    /// SuperBody feature flags (bitfield from SuperBodyFeatureFlags).
    pub feature_flags: u32, // offset 128
    pub free_energy: f32, // offset 132
    pub asymmetry: f32,   // offset 136
    pub _pad_ff_c: u32,   // offset 140, pad to 144
}
// Total: 144 bytes, 144 % 16 = 0 ✓

/// Per-particle interference kernel (256 bytes, storage buffer).
/// Each particle carries its own displacement kernel — the interference shifts
/// that coherent quantum mode pairs contribute to its position.
///
/// 10 mode pairs × 3 axes × 2 (Re, Im) = 60 floats.
/// The GPU applies: x_n += coherence * Σ_{i<j} 2·Re(κ_{n,ij} · ρ_{ij})
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuParticleKernel {
    /// Interference kernel: 10 mode pairs × 3 axes × 2 (re, im) = 60 floats.
    pub kernels: [f32; 60], // 240 bytes
    pub _pad: [f32; 4], // 16 bytes → total 256 bytes (16-aligned)
}

/// GPU-uploadable event cue data (16 bytes, group 1 binding 3).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuEventCueData {
    pub active_cue: u32,
    pub cue_intensity: f32,
    pub cue_timer: f32,
    pub _pad: f32,
}

/// GPU-uploadable matter config (16 bytes, group 1 binding 4).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuMatterConfig {
    pub active_count: u32,
    pub density_budget: f32,
    pub meshlet_policy: u32,
    pub physics_policy: u32,
}

/// GPU-uploadable wave material bias (16 bytes, group 2 binding 4 in pbr_dreamlet.wgsl).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuWaveMaterialBias {
    pub roughness_bias: f32,
    pub emissive_bias: f32,
    pub aniso_bias: f32,
    pub _pad: f32,
}

impl GpuMatterConfig {
    /// Default: all particles active, no gating.
    pub fn all_active(count: u32) -> Self {
        Self {
            active_count: count,
            density_budget: 1.0,
            meshlet_policy: 0,
            physics_policy: 0,
        }
    }
}

impl GpuQuantumContext {
    /// Classical default: mode 0 at 100%, zero coherence, DECOHERE flag.
    pub fn classical() -> Self {
        let mut ctx = <Self as bytemuck::Zeroable>::zeroed();
        ctx.populations[0] = 1.0;
        ctx.purity = 1.0;
        ctx.measured_mode = 0;
        ctx.temperature = 1.0;
        ctx.feature_flags = 0x0002; // DECOHERE
        ctx.free_energy = 0.0;
        ctx.asymmetry = 0.0;
        ctx
    }

    /// Validate invariants before GPU upload. Returns Ok or error description.
    /// On failure, the caller should fall back to `classical()` to prevent
    /// the GPU from receiving an invalid quantum state.
    pub fn validate(&self) -> Result<(), &'static str> {
        // Check for NaN in critical fields.
        if self.coherence_magnitude.is_nan() {
            return Err("coherence_magnitude is NaN");
        }
        if self.purity.is_nan() {
            return Err("purity is NaN");
        }
        if self.entropy.is_nan() {
            return Err("entropy is NaN");
        }

        // Populations must sum to ~1.0 (trace preservation).
        let pop_sum: f32 = self.populations.iter().sum();
        if (pop_sum - 1.0).abs() > 0.01 {
            return Err("populations do not sum to 1.0");
        }

        // Each population must be in [0, 1].
        for &p in &self.populations {
            if p < -0.01 || p > 1.01 {
                return Err("population out of [0, 1]");
            }
            if p.is_nan() {
                return Err("population is NaN");
            }
        }

        // Purity: Tr(ρ²) ∈ [1/5, 1] for a 5-dimensional system.
        if self.purity < 0.19 || self.purity > 1.01 {
            return Err("purity out of range [0.2, 1.0]");
        }

        // Von Neumann entropy: [0, ln5 ≈ 1.609].
        if self.entropy < -0.01 || self.entropy > 1.62 {
            return Err("entropy out of range [0, ln5]");
        }

        Ok(())
    }

    /// Compute BLAKE3 digest of bridge bytes for proof chain attestation.
    pub fn bridge_digest(&self) -> [u8; 32] {
        *blake3::hash(bytemuck::bytes_of(self)).as_bytes()
    }

    /// Compute individual BLAKE3 digests for each named observable field.
    /// Returns 7 digests: [populations, coherence_mag, purity, entropy,
    /// coherences, decoherence_epsilon, measured_mode].
    pub fn observable_digests(&self) -> [[u8; 32]; 7] {
        [
            *blake3::hash(bytemuck::cast_slice(&self.populations)).as_bytes(),
            *blake3::hash(&self.coherence_magnitude.to_le_bytes()).as_bytes(),
            *blake3::hash(&self.purity.to_le_bytes()).as_bytes(),
            *blake3::hash(&self.entropy.to_le_bytes()).as_bytes(),
            *blake3::hash(bytemuck::cast_slice(&self.coherences)).as_bytes(),
            *blake3::hash(&self.decoherence_epsilon.to_le_bytes()).as_bytes(),
            *blake3::hash(&self.measured_mode.to_le_bytes()).as_bytes(),
        ]
    }
}

/// Sealed bridge — immutable after validation.
/// The digest is computed at seal time and can be verified before GPU upload.
pub struct SealedBridge {
    pub context: GpuQuantumContext,
    pub digest: [u8; 32],
    pub frame: u64,
}

impl SealedBridge {
    /// Seal a validated bridge context. Computes BLAKE3 digest.
    pub fn seal(context: GpuQuantumContext, frame: u64) -> Result<Self, &'static str> {
        context.validate()?;
        let digest = context.bridge_digest();
        Ok(Self { context, digest, frame })
    }

    /// Verify that the bridge bytes haven't been mutated since sealing.
    pub fn verify(&self) -> bool {
        self.context.bridge_digest() == self.digest
    }
}

// ── Particle Generation ─────────────────────────────────────────────────

/// Generate a particle of GpuDreamlets distributed on a sphere via golden ratio spiral.
/// Deterministic — same inputs always produce the same output.
///
/// `center`: world-space center of the particle.
/// `count`: number of particles (64-256 typical).
/// `radius`: cloud radius in meters.
/// `base_color`: RGBA particle color.
/// `offsets`: pre-computed unit sphere offsets from `particle_sphere_offsets()`.
///
/// Clean Compute: called once at spawn. Per-frame updates via `update_particle_positions()`.
pub fn generate_particle(
    center: [f32; 3],
    count: u32,
    radius: f32,
    base_color: [f32; 4],
    offsets: &[[f32; 3]],
) -> Vec<GpuDreamlet> {
    let n = (count as usize).min(offsets.len());
    let mut dreamlets = Vec::with_capacity(n);

    for i in 0..n {
        let off = offsets[i];
        dreamlets.push(GpuDreamlet {
            position: [
                center[0] + off[0] * radius,
                center[1] + off[1] * radius,
                center[2] + off[2] * radius,
            ],
            scale: dreamwell_metaphors::DEFAULT_PARTICLE_SCALE,
            velocity: [0.0; 3],
            angular_velocity: 0.0,
            rotation: [0.0, 0.0, 0.0, 1.0],
            base_color,
            roughness: dreamwell_metaphors::DEFAULT_ROUGHNESS,
            metallic: dreamwell_metaphors::DEFAULT_METALLIC,
            mass: dreamwell_metaphors::DEFAULT_MASS,
            restitution: dreamwell_metaphors::DEFAULT_RESTITUTION,
            state: DREAMLET_STATE_PARTICLE,
            mesh_id: 3, // Sphere Full LOD (9 shapes × 3 LODs: Cube=0/1/2, Sphere=3/4/5)
            parent_id: 0,
            flags: DREAMLET_FLAG_ACTIVE | DREAMLET_FLAG_PHYSICS,
            age: 0.0,
            lifetime: 0.0, // persistent (no expiry)
            _pad_target: [0.0; 2],
            target_position: center,
            deconstruct_level: 0,
            bounding_radius: 0.12,
            skeleton_index: 0,
            _pad_struct: [0.0; 2],
        });
    }

    dreamlets
}

/// Update particle dreamlet positions relative to a new center position.
/// Applies locomotion-driven shape deformation (stretch, radius scale, noise).
///
/// `dreamlets`: mutable slice of existing particle dreamlets.
/// `center`: new world-space center.
/// `velocity`: controller velocity (for stretch direction).
/// `offsets`: pre-computed unit sphere offsets (same as generation).
/// `stretch`: [x, y, z] shape stretch from ParticleShapeParams.
/// `radius`: base cloud radius * radius_scale.
/// `particle_scale`: per-particle visual scale.
/// `noise_amplitude`: organic noise amplitude.
/// `time`: elapsed seconds (for noise phase).
///
/// Clean Compute: O(N) inline math, zero allocation. Caller owns the buffer.
pub fn update_particle_positions(
    dreamlets: &mut [GpuDreamlet],
    center: [f32; 3],
    velocity: [f32; 3],
    offsets: &[[f32; 3]],
    stretch: [f32; 3],
    radius: f32,
    particle_scale: f32,
    noise_amplitude: f32,
    time: f32,
) {
    let speed = (velocity[0] * velocity[0] + velocity[2] * velocity[2]).sqrt();
    // Compute facing direction for stretch alignment
    let (face_x, face_z) = if speed > 0.1 {
        (velocity[0] / speed, velocity[2] / speed)
    } else {
        (0.0, 1.0)
    };

    for (i, dreamlet) in dreamlets.iter_mut().enumerate() {
        if i >= offsets.len() {
            break;
        }
        let off = offsets[i];

        // Rotate offset to align stretch with movement direction
        let aligned_x = off[0] * face_z - off[2] * face_x;
        let aligned_z = off[0] * face_x + off[2] * face_z;

        // Apply stretch and radius
        let sx = aligned_x * stretch[0] * radius;
        let sy = off[1] * stretch[1] * radius;
        let sz = aligned_z * stretch[2] * radius;

        // Simple deterministic noise based on particle index + time
        let phase = (i as f32 * 1.618 + time * 2.0).sin() * noise_amplitude;
        let phase2 = (i as f32 * 2.414 + time * 1.7).cos() * noise_amplitude;

        dreamlet.position = [center[0] + sx + phase, center[1] + sy + phase2, center[2] + sz + phase];
        dreamlet.scale = particle_scale;
        dreamlet.target_position = center;
        dreamlet.velocity = velocity;
    }
}

/// DVR cull parameters (128 bytes, matches dreamlet_visibility.wgsl).
///
/// WGSL std140 layout: `vec3<f32>` has 16-byte alignment, so `_pad_lod`
/// starts at offset 112 (not 100), and the struct rounds up to 128 bytes.
/// The Rust side must match by adding explicit padding.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DvrCullParams {
    pub view_proj: [f32; 16], // 64 bytes  (offset 0)
    pub camera_pos: [f32; 3], // 12 bytes  (offset 64)
    pub dreamlet_count: u32,  //  4 bytes  (offset 76)
    pub screen_width: f32,    //  4 bytes  (offset 80)
    pub screen_height: f32,   //  4 bytes  (offset 84)
    pub lod1_distance: f32,   //  4 bytes  (offset 88)
    pub cull_distance: f32,   //  4 bytes  (offset 92)
    pub lod_scale: f32,       //  4 bytes  (offset 96)
    pub _pad_align: [f32; 3], // 12 bytes  (offset 100 — aligns _pad_lod to 112)
    pub _pad_lod: [f32; 3],   // 12 bytes  (offset 112 — matches WGSL vec3 at align 16)
    pub _pad_end: f32,        //  4 bytes  (offset 124 — struct tail to 128)
}

/// DVR destruction event (24 bytes, CPU → GPU).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DestructionEvent {
    pub parent_id: u32,     // object to destroy
    pub force: f32,         // impulse magnitude
    pub position: [f32; 3], // impact point
    pub _pad: f32,          // align to 24 bytes
}

/// Visible dreamlet count (written by GPU atomic, read by multi_draw_indexed_indirect_count).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DvrDrawCount {
    pub visible_count: u32,
}

/// Scene-wide uniforms for the DVR render pass (matches pbr_dreamlet.wgsl @group(0) @binding(0)).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DvrSceneUniforms {
    pub view_proj: [f32; 16],         // 64 bytes  (offset 0)
    pub camera_pos: [f32; 3],         // 12 bytes  (offset 64)
    pub directional_light_count: u32, //  4 bytes  (offset 76)
    pub point_light_count: u32,       //  4 bytes  (offset 80)
    pub elapsed_time: f32,            //  4 bytes  (offset 84) — animation clock
    pub _pad_align: [u32; 2],         //  8 bytes  (offset 88)
    pub anchor_pos: [f32; 3],         // 12 bytes  (offset 96)
    pub _pad_end: u32,                //  4 bytes  (offset 108) — struct rounds to 112
}

/// GPU-side particle physics parameters (128 bytes, matches particle_physics.wgsl).
/// PBF fluid sim: density constraint, cohesion, viscosity, vorticity, force field, edge fade.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ParticlePhysicsParams {
    pub anchor_position: [f32; 3],      // player center — spring tether target
    pub dt: f32,                        // delta time (seconds)
    pub dreamlet_count: u32,            // particle count
    pub spring_k: f32,                  // spring constant (tether stiffness)
    pub repulsion_strength: f32,        // inter-particle repulsion force scale
    pub damping: f32,                   // velocity damping per frame
    pub ground_y: f32,                  // ground plane Y
    pub cloud_radius: f32,              // max radial distance from anchor
    pub gravity: f32,                   // downward acceleration (low for cloud)
    pub restitution: f32,               // ground bounce coefficient
    pub prev_anchor_position: [f32; 3], // previous frame anchor (GPU derives velocity)
    pub cohesion_strength: f32,         // medium-range attraction
    pub viscosity: f32,                 // XSPH smoothing coefficient
    pub vorticity_epsilon: f32,         // vorticity confinement strength
    pub rheology_blend: f32,            // 0=gas, 0.5=liquid, 1=solid
    pub _pad_rheology: f32,             // alignment
    pub force_field_dir: [f32; 3],      // movement direction (normalized)
    pub force_field_mag: f32,           // movement strength
    pub fade_start: f32,                // edge fade start (meters from anchor)
    pub fade_end: f32,                  // edge fade end (meters from anchor)
    pub particle_base_scale: f32,       // base visual scale
    pub collision_sphere_count: u32,    // scene obstacles (0-8)
    pub time_seconds: f32,              // elapsed time for noise
    pub _pad: [f32; 3],                 // align to 128 bytes
}

/// Build ParticlePhysicsParams from a SuperBodyFrameDescriptor.
/// Centralizes the CPU→GPU packet construction that was previously duplicated
/// inline in runtime/app.rs and editor/play_view.rs.
pub fn build_particle_physics_params(
    descriptor: &dreamwell_attention::SuperBodyFrameDescriptor,
    dt: f32,
    particle_count: u32,
    time_seconds: f32,
) -> ParticlePhysicsParams {
    use dreamwell_metaphors::constants::*;
    ParticlePhysicsParams {
        anchor_position: descriptor.position,
        dt,
        dreamlet_count: particle_count,
        spring_k: DEFAULT_SPRING_K,
        repulsion_strength: DEFAULT_REPULSION_STRENGTH,
        damping: DEFAULT_DAMPING,
        ground_y: 0.0,
        cloud_radius: descriptor.cloud_radius,
        gravity: DEFAULT_GRAVITY,
        restitution: DEFAULT_RESTITUTION,
        prev_anchor_position: descriptor.position,
        cohesion_strength: descriptor.cohesion_strength,
        viscosity: DEFAULT_VISCOSITY,
        vorticity_epsilon: descriptor.vorticity_epsilon,
        rheology_blend: descriptor.rheology_blend,
        _pad_rheology: 0.0,
        force_field_dir: descriptor.force_field_dir,
        force_field_mag: descriptor.force_field_mag,
        fade_start: descriptor.fade_start,
        fade_end: descriptor.fade_end,
        particle_base_scale: descriptor.shape.particle_scale,
        collision_sphere_count: 0,
        time_seconds,
        _pad: [0.0; 3],
    }
}

/// Build ParticlePhysicsParams from a MetaphorOutput.
/// Uses evaluated metaphor physics instead of hard-coded constants.
pub fn build_particle_physics_params_from_metaphor(
    descriptor: &dreamwell_attention::SuperBodyFrameDescriptor,
    metaphor: &dreamwell_metaphors::MetaphorOutput,
    dt: f32,
    particle_count: u32,
    time_seconds: f32,
    prev_anchor: [f32; 3],
) -> ParticlePhysicsParams {
    ParticlePhysicsParams {
        anchor_position: descriptor.position,
        dt,
        dreamlet_count: particle_count,
        spring_k: metaphor.spring_k,
        repulsion_strength: metaphor.repulsion_strength,
        damping: metaphor.damping,
        ground_y: 0.0,
        cloud_radius: metaphor.cloud_radius,
        gravity: metaphor.gravity,
        restitution: dreamwell_metaphors::DEFAULT_RESTITUTION,
        prev_anchor_position: prev_anchor,
        cohesion_strength: metaphor.cohesion_strength,
        viscosity: metaphor.viscosity,
        vorticity_epsilon: metaphor.vorticity_epsilon,
        rheology_blend: metaphor.rheology_blend,
        _pad_rheology: 0.0,
        force_field_dir: descriptor.force_field_dir,
        force_field_mag: descriptor.force_field_mag,
        fade_start: metaphor.fade_start,
        fade_end: metaphor.fade_end,
        particle_base_scale: metaphor.particle_scale,
        collision_sphere_count: 0,
        time_seconds,
        _pad: [0.0; 3],
    }
}

/// GPU-side collision sphere for scene obstacle avoidance (16 bytes).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuCollisionSphere {
    pub center: [f32; 3],
    pub radius: f32,
}

/// Dreamlet catalog — manages GPU-resident dreamlet storage and DVR render pipeline.
pub struct DreamletCatalog {
    /// Persistent dreamlet storage buffer.
    pub dreamlet_buffer: Option<wgpu::Buffer>,
    /// Visible dreamlet indirect draw args (GPU-written).
    pub indirect_buffer: Option<wgpu::Buffer>,
    /// Visible count buffer (GPU atomic, read by multi_draw_indexed_indirect_count).
    pub count_buffer: Option<wgpu::Buffer>,
    /// Cull params uniform.
    pub cull_params_buffer: Option<wgpu::Buffer>,
    /// Destruction event buffer (CPU → GPU, small).
    pub destruction_buffer: Option<wgpu::Buffer>,
    /// Cull compute pipeline.
    pub cull_pipeline: Option<wgpu::ComputePipeline>,
    /// Physics compute pipeline.
    pub physics_pipeline: Option<wgpu::ComputePipeline>,
    /// Cull bind group layout.
    pub cull_bgl: Option<wgpu::BindGroupLayout>,
    /// Physics bind group layout.
    pub physics_bgl: Option<wgpu::BindGroupLayout>,
    /// Cached cull bind group (invalidated on buffer reallocation).
    cached_cull_bg: Option<wgpu::BindGroup>,
    /// Cached physics bind group (invalidated on buffer reallocation).
    cached_physics_bg: Option<wgpu::BindGroup>,

    // ── DVR Render Pipeline (pbr_dreamlet.wgsl) ────────────────────
    /// PBR dreamlet render pipeline (replaces render_pbr for GpuDriven mode).
    pub render_pipeline: Option<wgpu::RenderPipeline>,
    /// Scene uniform buffer for DVR render pass (view_proj + camera_pos + light counts).
    pub scene_uniform_buffer: Option<wgpu::Buffer>,
    /// Group 0 bind group layout (scene uniforms + lights + dreamlet storage).
    pub render_bgl_scene: Option<wgpu::BindGroupLayout>,
    /// Group 1 bind group layout (IBL textures).
    pub render_bgl_ibl: Option<wgpu::BindGroupLayout>,
    /// Cached group 0 bind group.
    pub render_bg_scene: Option<wgpu::BindGroup>,
    /// Cached group 1 bind group (IBL).
    pub render_bg_ibl: Option<wgpu::BindGroup>,
    /// Cached group 2 bind group (RT outputs — disabled by default).
    pub render_bg_rt: Option<wgpu::BindGroup>,
    /// Whether the render pipeline is compiled.
    pub render_pipeline_ready: bool,
    /// The texture format the render pipeline was compiled for.
    pub render_pipeline_format: Option<wgpu::TextureFormat>,
    /// Merged mesh buffer (all shapes × all LODs, for DVR cull + rendering).
    pub merged_mesh: Option<crate::gpu_driven::MergedMeshBuffer>,

    // ── MicroDreamlet support (64B, 2× density) ───────────────────
    /// Micro cull compute pipeline (micro_dreamlet_cull.wgsl).
    pub micro_cull_pipeline: Option<wgpu::ComputePipeline>,
    /// Micro cull bind group layout.
    pub micro_cull_bgl: Option<wgpu::BindGroupLayout>,
    /// Cached micro cull bind group.
    cached_micro_cull_bg: Option<wgpu::BindGroup>,
    /// Micro PBR render pipeline (pbr_micro_dreamlet.wgsl).
    pub micro_render_pipeline: Option<wgpu::RenderPipeline>,
    /// Whether micro render pipeline is compiled.
    pub micro_render_pipeline_ready: bool,
    /// Whether current buffer data is MicroDreamlet (64B) or GpuDreamlet (128B).
    pub micro_mode: bool,

    // ── Particle Physics (PBF fluid sim) ─────────────────────────────
    /// Particle physics compute pipeline (particle_physics.wgsl).
    pub particle_physics_pipeline: Option<wgpu::ComputePipeline>,
    /// Particle physics params uniform buffer (128 bytes).
    pub particle_params_buffer: Option<wgpu::Buffer>,
    /// Collision sphere storage buffer (8 × 16 = 128 bytes).
    pub collision_sphere_buffer: Option<wgpu::Buffer>,
    /// Particle physics bind group layout.
    pub particle_physics_bgl: Option<wgpu::BindGroupLayout>,
    /// Cached particle physics bind group.
    cached_particle_physics_bg: Option<wgpu::BindGroup>,
    /// Whether the particle physics pipeline is compiled.
    pub particle_physics_ready: bool,
    /// Whether the initial dreamlet upload has been done (GPU owns positions after).
    pub particle_initial_upload_done: bool,

    // ── Quantum Bridge (CPU→GPU density matrix observables) ──────────
    /// Quantum context uniform buffer (128 bytes).
    pub quantum_context_buf: Option<wgpu::Buffer>,
    /// Per-particle interference kernel storage buffer (256B × particle_count).
    pub interference_kernel_buf: Option<wgpu::Buffer>,
    /// Quantum bind group layout (group 1 in particle_physics.wgsl).
    pub quantum_bgl: Option<wgpu::BindGroupLayout>,
    /// Cached quantum bind group (group 1 for particle physics dispatch).
    cached_quantum_bg: Option<wgpu::BindGroup>,
    /// Whether quantum buffers are initialized.
    pub quantum_bridge_ready: bool,

    // ── WaveParams (metaphor state for GPU decode) ──────────────────
    /// WaveParams uniform buffer (64 bytes, group 1 binding 2 in particle_physics.wgsl).
    pub wave_params_buf: Option<wgpu::Buffer>,
    /// Whether wave params buffer is allocated.
    pub wave_params_ready: bool,

    // ── EventCue + MatterConfig (group 1 bindings 3-4) ──────────────
    /// EventCue uniform buffer (16 bytes, group 1 binding 3).
    pub event_cue_buf: Option<wgpu::Buffer>,
    /// MatterConfig uniform buffer (16 bytes, group 1 binding 4).
    pub matter_config_buf: Option<wgpu::Buffer>,
    /// WaveMaterialBias uniform buffer (16 bytes, group 2 binding 4 in pbr_dreamlet.wgsl).
    pub wave_material_buf: Option<wgpu::Buffer>,

    /// Current dreamlet count.
    pub count: u32,
    /// Maximum capacity.
    pub capacity: u32,
    /// Whether the catalog is initialized.
    pub initialized: bool,
    /// VRAM budget tracker.
    pub vram: crate::vram::VramTracker,
}

impl Default for DreamletCatalog {
    fn default() -> Self {
        Self {
            dreamlet_buffer: None,
            indirect_buffer: None,
            count_buffer: None,
            cull_params_buffer: None,
            destruction_buffer: None,
            cull_pipeline: None,
            physics_pipeline: None,
            cull_bgl: None,
            physics_bgl: None,
            cached_cull_bg: None,
            cached_physics_bg: None,
            render_pipeline: None,
            scene_uniform_buffer: None,
            render_bgl_scene: None,
            render_bgl_ibl: None,
            render_bg_scene: None,
            render_bg_ibl: None,
            render_bg_rt: None,
            render_pipeline_ready: false,
            render_pipeline_format: None,
            merged_mesh: None,
            micro_cull_pipeline: None,
            micro_cull_bgl: None,
            cached_micro_cull_bg: None,
            micro_render_pipeline: None,
            micro_render_pipeline_ready: false,
            micro_mode: false,
            particle_physics_pipeline: None,
            particle_params_buffer: None,
            collision_sphere_buffer: None,
            particle_physics_bgl: None,
            cached_particle_physics_bg: None,
            particle_physics_ready: false,
            particle_initial_upload_done: false,
            quantum_context_buf: None,
            interference_kernel_buf: None,
            quantum_bgl: None,
            cached_quantum_bg: None,
            wave_params_buf: None,
            wave_params_ready: false,
            event_cue_buf: None,
            matter_config_buf: None,
            wave_material_buf: None,
            quantum_bridge_ready: false,
            count: 0,
            capacity: 0,
            initialized: false,
            vram: crate::vram::VramTracker::default(),
        }
    }
}

impl DreamletCatalog {
    /// Initialize the catalog with capacity for `max_dreamlets`.
    /// Capacity is clamped to fit within maxStorageBufferBindingSize.
    pub fn init(&mut self, device: &wgpu::Device, max_dreamlets: u32) {
        let dreamlet_size = std::mem::size_of::<GpuDreamlet>() as u64;
        let indirect_size = 20u64; // DrawIndexedIndirectArgs

        // Clamp to storage buffer limit. Also cap indirect buffer (20B per dreamlet).
        let max_storage = device.limits().max_storage_buffer_binding_size as u64;
        let max_by_dreamlet_buf = (max_storage / dreamlet_size) as u32;
        let max_by_indirect_buf = (max_storage / indirect_size) as u32;
        let max_dreamlets = max_dreamlets.min(max_by_dreamlet_buf).min(max_by_indirect_buf);

        if max_dreamlets == 0 {
            log::error!(
                "DVR: storage buffer too small for any dreamlets (max_storage={})",
                max_storage
            );
            return;
        }

        // Also clamp to VRAM budget (reserve 500 MB for non-dreamlet allocations)
        let reserved = 500 * 1024 * 1024u64; // 500 MB for HDR, depth, bloom, IBL, etc.
        let available_for_dreamlets = self.vram.remaining().saturating_sub(reserved);
        let max_by_budget = (available_for_dreamlets / (dreamlet_size + indirect_size)) as u32;
        let max_dreamlets = max_dreamlets.min(max_by_budget);

        if max_dreamlets == 0 {
            log::error!("DVR: insufficient VRAM budget for dreamlets");
            return;
        }

        let dreamlet_buf_size = (max_dreamlets as u64) * dreamlet_size;
        let indirect_buf_size = (max_dreamlets as u64) * indirect_size;

        log::info!(
            "DVR: capacity {} dreamlets ({:.1} MB storage + {:.1} MB indirect)",
            max_dreamlets,
            dreamlet_buf_size as f64 / (1024.0 * 1024.0),
            indirect_buf_size as f64 / (1024.0 * 1024.0)
        );

        // Track VRAM for all buffer allocations
        let count_buf_size = 4u64;
        let cull_params_size = std::mem::size_of::<DvrCullParams>() as u64;
        let destruction_buf_size = 256 * std::mem::size_of::<DestructionEvent>() as u64;
        self.vram.record(dreamlet_buf_size);
        self.vram.record(indirect_buf_size);
        self.vram.record(count_buf_size);
        self.vram.record(cull_params_size);
        self.vram.record(destruction_buf_size);

        // Dreamlet storage buffer
        self.dreamlet_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("dvr_dreamlet_catalog"),
            size: dreamlet_buf_size,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
            mapped_at_creation: false,
        }));

        // Indirect draw args (one per dreamlet, GPU-written)
        self.indirect_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("dvr_indirect_draws"),
            size: indirect_buf_size,
            usage: wgpu::BufferUsages::STORAGE
                | wgpu::BufferUsages::INDIRECT
                | wgpu::BufferUsages::COPY_DST
                | wgpu::BufferUsages::COPY_SRC,
            mapped_at_creation: false,
        }));

        // Visible count (atomic u32, read by multi_draw_indexed_indirect_count)
        self.count_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("dvr_visible_count"),
            size: 4,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::INDIRECT | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));

        // Cull params uniform
        self.cull_params_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("dvr_cull_params"),
            size: std::mem::size_of::<DvrCullParams>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));

        // Destruction events (small, CPU → GPU)
        self.destruction_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("dvr_destruction_events"),
            size: 256 * std::mem::size_of::<DestructionEvent>() as u64,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));

        // Cull compute pipeline
        let cull_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("dvr_cull_bgl"),
            entries: &[
                // binding 0: cull params (uniform)
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 1: dreamlets (storage, read)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 2: mesh descriptors (storage, read)
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 3: indirect draw args (storage, write)
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: false },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 4: visible count (storage, write — atomic)
                wgpu::BindGroupLayoutEntry {
                    binding: 4,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: false },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        let cull_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("dreamlet_visibility.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/dreamlet_visibility.wgsl").into()),
        });

        let cull_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("dvr_cull_layout"),
            bind_group_layouts: &[Some(&cull_bgl)],
            immediate_size: 0,
        });

        self.cull_pipeline = Some(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("dvr_cull_compute"),
            layout: Some(&cull_layout),
            module: &cull_shader,
            entry_point: Some("cull_main"),
            compilation_options: Default::default(),
            cache: None,
        }));

        // Physics compute pipeline
        let physics_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("dvr_physics_bgl"),
            entries: &[
                // binding 0: params (uniform)
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 1: dreamlets (storage, read-write)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: false },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 2: destruction events (storage, read)
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        let physics_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("dreamlet_physics.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/dreamlet_physics.wgsl").into()),
        });

        let physics_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("dvr_physics_layout"),
            bind_group_layouts: &[Some(&physics_bgl)],
            immediate_size: 0,
        });

        self.physics_pipeline = Some(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("dvr_physics_compute"),
            layout: Some(&physics_layout),
            module: &physics_shader,
            entry_point: Some("physics_main"),
            compilation_options: Default::default(),
            cache: None,
        }));

        self.cull_bgl = Some(cull_bgl);
        self.physics_bgl = Some(physics_bgl);
        // Build merged mesh buffer (all 9 shapes × 3 LODs = 27 descriptors).
        // Used by both the cull shader (descriptor_buffer) and render pass (VB/IB).
        self.merged_mesh = Some(crate::gpu_driven::MergedMeshBuffer::build_primitive(device));

        self.capacity = max_dreamlets;
        self.initialized = true;

        // Build cached bind groups (zero allocation per frame after this point)
        self.ensure_cull_bind_group(device);
        self.ensure_physics_bind_group(device);

        log::info!(
            "DVR catalog initialized: {} max dreamlets, {} MB VRAM",
            max_dreamlets,
            (max_dreamlets as u64 * dreamlet_size) / (1024 * 1024)
        );
    }

    /// Recompile the DVR render pipeline if the target format has changed.
    /// Returns true if recompilation occurred.
    pub fn ensure_render_pipeline_format(&mut self, device: &wgpu::Device, target_format: wgpu::TextureFormat) -> bool {
        if self.render_pipeline_format == Some(target_format) {
            return false;
        }
        // Format mismatch — invalidate and recompile.
        self.render_pipeline_ready = false;
        self.render_pipeline_format = None;
        self.render_pipeline = None;
        self.render_bgl_scene = None;
        self.render_bgl_ibl = None;
        self.render_bg_scene = None;
        self.render_bg_ibl = None;
        self.micro_render_pipeline_ready = false;
        self.micro_render_pipeline = None;
        self.init_render_pipeline(device, target_format);
        true
    }

    /// Compile the DVR render pipeline (pbr_dreamlet.wgsl).
    /// Call once after init(). Requires target_format for the render pipeline color target.
    pub fn init_render_pipeline(&mut self, device: &wgpu::Device, target_format: wgpu::TextureFormat) {
        if self.render_pipeline_ready {
            return;
        }

        // Group 0: scene uniforms + directional lights + point lights + dreamlet storage
        let render_bgl_scene = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("dvr_render_bgl_scene"),
            entries: &[
                // @binding(0) SceneUniforms (uniform)
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // @binding(1) DirectionalLight[4] (uniform)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // @binding(2) PointLight[64] (uniform)
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // @binding(3) Dreamlet[] (storage, read)
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        // Group 1: IBL textures
        let render_bgl_ibl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("dvr_render_bgl_ibl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::Cube,
                        multisampled: false,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::Cube,
                        multisampled: false,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    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: 3,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
            ],
        });

        use wgpu::util::DeviceExt;

        // Group 2: RT outputs + quantum PBR + wave material bias
        let render_bgl_rt = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("dvr_render_bgl_rt"),
            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::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 4,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        // Create RT fallback bind group (disabled config, 1x1 black textures, zeroed quantum/material)
        let rt_fallback_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("dvr_rt_fallback"),
            size: wgpu::Extent3d {
                width: 1,
                height: 1,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8Unorm,
            usage: wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        let rt_fallback_view = rt_fallback_tex.create_view(&wgpu::TextureViewDescriptor::default());
        let rt_config_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("dvr_rt_config"),
            contents: &[0u8; 16],
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
        });
        // Fallback quantum PBR context (zeroed storage buffer).
        // If quantum_context_buf is available, share it; otherwise create a throwaway fallback.
        let quantum_pbr_fallback = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("dvr_quantum_pbr_fallback"),
            contents: &[0u8; std::mem::size_of::<GpuQuantumContext>()],
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
        });
        let quantum_pbr_buf = self.quantum_context_buf.as_ref().unwrap_or(&quantum_pbr_fallback);
        // Fallback wave material bias (zeroed = no bias).
        let wave_material_fallback = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("dvr_wave_material_fallback"),
            contents: &[0u8; 16],
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
        });
        self.render_bg_rt = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dvr_rt_fallback_bg"),
            layout: &render_bgl_rt,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&rt_fallback_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(&rt_fallback_view),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: rt_config_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: quantum_pbr_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: wave_material_fallback.as_entire_binding(),
                },
            ],
        }));
        self.wave_material_buf = Some(wave_material_fallback);

        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("dvr_render_layout"),
            bind_group_layouts: &[Some(&render_bgl_scene), Some(&render_bgl_ibl), Some(&render_bgl_rt)],
            immediate_size: 0,
        });

        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("pbr_dreamlet.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/pbr_dreamlet.wgsl").into()),
        });

        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("dvr_pbr_dreamlet"),
            layout: Some(&pipeline_layout),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("vs_main"),
                compilation_options: Default::default(),
                // PrimitiveVertex: pos(12) + normal(12) + color(16) = 40 bytes
                buffers: &[wgpu::VertexBufferLayout {
                    array_stride: 40,
                    step_mode: wgpu::VertexStepMode::Vertex,
                    attributes: &wgpu::vertex_attr_array![
                        0 => Float32x3, // position
                        1 => Float32x3, // normal
                        2 => Float32x4, // color
                    ],
                }],
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("fs_main"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: target_format,
                    blend: Some(wgpu::BlendState {
                        color: wgpu::BlendComponent {
                            src_factor: wgpu::BlendFactor::SrcAlpha,
                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
                            operation: wgpu::BlendOperation::Add,
                        },
                        alpha: wgpu::BlendComponent::OVER,
                    }),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: Some(wgpu::Face::Back),
                ..Default::default()
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: crate::formats::DEPTH_FORMAT,
                depth_write_enabled: Some(true),
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });

        // Scene uniform buffer
        let scene_uniform_size = std::mem::size_of::<DvrSceneUniforms>() as u64;
        self.scene_uniform_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("dvr_scene_uniforms"),
            size: scene_uniform_size,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));
        self.vram.record(scene_uniform_size);

        self.render_pipeline = Some(pipeline);
        self.render_bgl_scene = Some(render_bgl_scene);
        self.render_bgl_ibl = Some(render_bgl_ibl);
        self.render_pipeline_ready = true;
        self.render_pipeline_format = Some(target_format);

        log::info!(
            "DVR render pipeline compiled (pbr_dreamlet.wgsl, target: {:?})",
            target_format
        );
    }

    /// Create render bind groups. Call after init_render_pipeline() and after
    /// scene lights and IBL are available.
    pub fn create_render_bind_groups(
        &mut self,
        device: &wgpu::Device,
        directional_light_buf: &wgpu::Buffer,
        point_light_buf: &wgpu::Buffer,
        ibl: &crate::ibl::IblPrecompute,
    ) {
        if self.render_bg_scene.is_some() {
            return;
        } // already created
        let Some(ref bgl_scene) = self.render_bgl_scene else {
            return;
        };
        let Some(ref bgl_ibl) = self.render_bgl_ibl else { return };
        let Some(ref dreamlet_buf) = self.dreamlet_buffer else {
            return;
        };
        let Some(ref scene_buf) = self.scene_uniform_buffer else {
            return;
        };

        // Group 0: scene uniforms + lights + dreamlet storage
        self.render_bg_scene = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dvr_render_bg_scene"),
            layout: bgl_scene,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: scene_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: directional_light_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: point_light_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: dreamlet_buf.as_entire_binding(),
                },
            ],
        }));

        // Group 1: IBL
        let Some(irradiance) = ibl.irradiance_view() else {
            return;
        };
        let Some(specular) = ibl.specular_view() else { return };
        let Some(brdf_lut) = ibl.brdf_lut_view() else { return };

        let ibl_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("dvr_ibl_sampler"),
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::MipmapFilterMode::Linear,
            ..Default::default()
        });

        self.render_bg_ibl = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dvr_render_bg_ibl"),
            layout: bgl_ibl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(irradiance),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(specular),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(brdf_lut),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::Sampler(&ibl_sampler),
                },
            ],
        }));

        log::info!("DVR render bind groups created");
    }

    /// Create render bind groups from explicit texture views (avoids borrow conflicts).
    pub fn create_render_bind_groups_from_views(
        &mut self,
        device: &wgpu::Device,
        directional_light_buf: &wgpu::Buffer,
        point_light_buf: &wgpu::Buffer,
        irradiance_view: &wgpu::TextureView,
        specular_view: &wgpu::TextureView,
        brdf_lut_view: &wgpu::TextureView,
    ) {
        if self.render_bg_scene.is_some() {
            return;
        }
        let Some(ref bgl_scene) = self.render_bgl_scene else {
            return;
        };
        let Some(ref bgl_ibl) = self.render_bgl_ibl else { return };
        let Some(ref dreamlet_buf) = self.dreamlet_buffer else {
            return;
        };
        let Some(ref scene_buf) = self.scene_uniform_buffer else {
            return;
        };

        self.render_bg_scene = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dvr_render_bg_scene"),
            layout: bgl_scene,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: scene_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: directional_light_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: point_light_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: dreamlet_buf.as_entire_binding(),
                },
            ],
        }));

        let ibl_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("dvr_ibl_sampler"),
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::MipmapFilterMode::Linear,
            ..Default::default()
        });

        self.render_bg_ibl = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dvr_render_bg_ibl"),
            layout: bgl_ibl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(irradiance_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(specular_view),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(brdf_lut_view),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::Sampler(&ibl_sampler),
                },
            ],
        }));

        log::info!("DVR render bind groups created (from views)");
    }

    /// Upload scene uniforms for the DVR render pass. Call each frame before render().
    pub fn upload_scene_uniforms(
        &self,
        queue: &wgpu::Queue,
        camera: &crate::camera::Camera,
        directional_count: u32,
        point_light_count: u32,
    ) {
        // Default anchor to camera center (lookat target ≈ player position).
        self.upload_scene_uniforms_with_anchor(
            queue,
            camera,
            directional_count,
            point_light_count,
            camera.center.to_array(),
        );
    }

    pub fn upload_scene_uniforms_with_anchor(
        &self,
        queue: &wgpu::Queue,
        camera: &crate::camera::Camera,
        directional_count: u32,
        point_light_count: u32,
        anchor_pos: [f32; 3],
    ) {
        self.upload_scene_uniforms_full(queue, camera, directional_count, point_light_count, anchor_pos, 0.0);
    }

    pub fn upload_scene_uniforms_full(
        &self,
        queue: &wgpu::Queue,
        camera: &crate::camera::Camera,
        directional_count: u32,
        point_light_count: u32,
        anchor_pos: [f32; 3],
        elapsed_time: f32,
    ) {
        let Some(ref buf) = self.scene_uniform_buffer else {
            return;
        };
        let vp = (camera.projection_matrix * camera.view_matrix).to_cols_array();
        let uniforms = DvrSceneUniforms {
            view_proj: vp,
            camera_pos: camera.position.to_array(),
            point_light_count,
            directional_light_count: directional_count,
            elapsed_time,
            _pad_align: [0; 2],
            anchor_pos,
            _pad_end: 0,
        };
        queue.write_buffer(buf, 0, bytemuck::bytes_of(&uniforms));
    }

    /// Render visible dreamlets using the DVR pipeline.
    /// This REPLACES render_pbr() for RenderPath::GpuDriven.
    /// The indirect buffer was filled by dispatch_cull.
    /// Uses the catalog's own merged mesh buffer (built at init).
    pub fn render_dvr(&self, pass: &mut wgpu::RenderPass<'_>) {
        if !self.initialized || !self.render_pipeline_ready || self.count == 0 {
            return;
        }
        let Some(ref pipeline) = self.render_pipeline else {
            return;
        };
        let Some(ref bg_scene) = self.render_bg_scene else {
            return;
        };
        let Some(ref bg_ibl) = self.render_bg_ibl else { return };
        let Some(ref indirect_buf) = self.indirect_buffer else {
            return;
        };
        let Some(ref merged) = self.merged_mesh else { return };

        pass.set_pipeline(pipeline);
        pass.set_bind_group(0, bg_scene, &[]);
        pass.set_bind_group(1, bg_ibl, &[]);
        // RT outputs (@group(2)) — always bound with fallback disabled config
        if let Some(ref bg_rt) = self.render_bg_rt {
            pass.set_bind_group(2, bg_rt, &[]);
        }
        pass.set_vertex_buffer(0, merged.vertex_buffer.slice(..));
        pass.set_index_buffer(merged.index_buffer.slice(..), wgpu::IndexFormat::Uint32);

        // GPU draws ALL dreamlets. Culled ones have instance_count=0.
        pass.multi_draw_indexed_indirect(indirect_buf, 0, self.count);
    }

    /// Render dreamlets using direct draw_indexed calls — no MULTI_DRAW_INDIRECT required.
    /// Each dreamlet gets an individual draw call using the sphere mesh descriptor.
    /// Suitable for small counts (particle ≤256) where per-call overhead is negligible.
    /// Bypasses the indirect buffer and cull shader — all active dreamlets are drawn.
    pub fn render_dvr_direct(&self, pass: &mut wgpu::RenderPass<'_>) {
        if !self.initialized || !self.render_pipeline_ready || self.count == 0 {
            return;
        }
        let Some(ref pipeline) = self.render_pipeline else {
            return;
        };
        let Some(ref bg_scene) = self.render_bg_scene else {
            return;
        };
        let Some(ref bg_ibl) = self.render_bg_ibl else { return };
        let Some(ref merged) = self.merged_mesh else { return };

        pass.set_pipeline(pipeline);
        pass.set_bind_group(0, bg_scene, &[]);
        pass.set_bind_group(1, bg_ibl, &[]);
        if let Some(ref bg_rt) = self.render_bg_rt {
            pass.set_bind_group(2, bg_rt, &[]);
        }
        pass.set_vertex_buffer(0, merged.vertex_buffer.slice(..));
        pass.set_index_buffer(merged.index_buffer.slice(..), wgpu::IndexFormat::Uint32);

        // Look up sphere mesh descriptor (mesh_id=3 = Sphere Full LOD).
        let sphere_desc_idx = 3usize; // Cube=0,1,2  Sphere=3,4,5
        if sphere_desc_idx >= merged.descriptors.len() {
            log::warn!(
                "DVR: sphere descriptor {} out of range ({})",
                sphere_desc_idx,
                merged.descriptors.len()
            );
            return;
        }
        let desc = &merged.descriptors[sphere_desc_idx];

        log::trace!(
            "DVR direct: {} dreamlets (sphere: first_idx={} idx_count={} base_vtx={} descs={})",
            self.count,
            desc.first_index,
            desc.index_count,
            desc.base_vertex,
            merged.descriptors.len(),
        );

        // Issue one draw_indexed per dreamlet. instance_index = dreamlet index
        // so the vertex shader reads dreamlets[instance_index] for transform.
        for i in 0..self.count {
            pass.draw_indexed(
                desc.first_index..desc.first_index + desc.index_count,
                desc.base_vertex,
                i..i + 1,
            );
        }
    }

    /// Upload dreamlet data to the GPU. Call once at scene load or when count changes.
    /// Clamps to capacity — excess dreamlets are silently dropped.
    pub fn upload(&mut self, queue: &wgpu::Queue, dreamlets: &[GpuDreamlet]) {
        let Some(ref buf) = self.dreamlet_buffer else { return };
        if dreamlets.is_empty() {
            return;
        }

        let safe_count = (dreamlets.len() as u32).min(self.capacity);
        if safe_count < dreamlets.len() as u32 {
            log::warn!(
                "DVR: clamping {} dreamlets to capacity {} (budget exceeded)",
                dreamlets.len(),
                self.capacity
            );
        }

        queue.write_buffer(buf, 0, bytemuck::cast_slice(&dreamlets[..safe_count as usize]));
        self.count = safe_count;
        self.micro_mode = false;
    }

    /// Append a batch of new dreamlets at the current count offset.
    /// CPU cost: O(K) where K = batch.len(). Does not touch existing data.
    pub fn upload_batch(&mut self, queue: &wgpu::Queue, batch: &[GpuDreamlet]) {
        let Some(ref buf) = self.dreamlet_buffer else { return };
        if batch.is_empty() {
            return;
        }

        let offset = self.count as u64 * std::mem::size_of::<GpuDreamlet>() as u64;
        let new_total = self.count + batch.len() as u32;

        if new_total > self.capacity {
            let safe = (self.capacity - self.count) as usize;
            if safe == 0 {
                return;
            }
            log::warn!("DVR: batch clamped to {} (capacity {})", safe, self.capacity);
            queue.write_buffer(buf, offset, bytemuck::cast_slice(&batch[..safe]));
            self.count = self.capacity;
        } else {
            queue.write_buffer(buf, offset, bytemuck::cast_slice(batch));
            self.count = new_total;
        }
        self.micro_mode = false;
    }

    /// Dispatch visibility cull (Phase B). GPU builds indirect draw args.
    /// CPU cost: 96-byte uniform write + 1 compute dispatch.
    /// Build and cache the cull bind group. Call after init() or after buffer reallocation.
    pub fn ensure_cull_bind_group(&mut self, device: &wgpu::Device) {
        if self.cached_cull_bg.is_some() {
            return;
        }
        let Some(ref cull_bgl) = self.cull_bgl else { return };
        let Some(ref dreamlet_buf) = self.dreamlet_buffer else {
            return;
        };
        let Some(ref indirect_buf) = self.indirect_buffer else {
            return;
        };
        let Some(ref count_buf) = self.count_buffer else { return };
        let Some(ref params_buf) = self.cull_params_buffer else {
            return;
        };
        let Some(ref merged) = self.merged_mesh else { return };

        self.cached_cull_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dvr_cull_bg"),
            layout: cull_bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: params_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: dreamlet_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: merged.descriptor_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: indirect_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: count_buf.as_entire_binding(),
                },
            ],
        }));
    }

    /// Build and cache the physics bind group.
    pub fn ensure_physics_bind_group(&mut self, device: &wgpu::Device) {
        if self.cached_physics_bg.is_some() {
            return;
        }
        let Some(ref physics_bgl) = self.physics_bgl else {
            return;
        };
        let Some(ref dreamlet_buf) = self.dreamlet_buffer else {
            return;
        };
        let Some(ref params_buf) = self.cull_params_buffer else {
            return;
        };
        let Some(ref destruction_buf) = self.destruction_buffer else {
            return;
        };

        self.cached_physics_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dvr_physics_bg"),
            layout: physics_bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: params_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: dreamlet_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: destruction_buf.as_entire_binding(),
                },
            ],
        }));
    }

    /// Dispatch visibility cull. Zero CPU per-object work.
    /// Per-frame cost: 100 bytes written (96B params + 4B counter reset) + 1 compute dispatch.
    /// Bind group is cached — no per-frame allocation.
    pub fn dispatch_cull(
        &self,
        _device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        camera: &crate::camera::Camera,
        screen_width: f32,
        screen_height: f32,
        observer_decohere_radius: f32,
        lod_scale: f32,
    ) {
        if !self.initialized || self.count == 0 {
            return;
        }
        let Some(ref cull_pipe) = self.cull_pipeline else {
            return;
        };
        let Some(ref count_buf) = self.count_buffer else { return };
        let Some(ref params_buf) = self.cull_params_buffer else {
            return;
        };
        let Some(ref bg) = self.cached_cull_bg else { return };

        // Reset visible count to 0 (4 bytes)
        queue.write_buffer(count_buf, 0, bytemuck::bytes_of(&0u32));

        // Upload cull params (112 bytes — the ONLY per-frame CPU data write)
        let vp = (camera.projection_matrix * camera.view_matrix).to_cols_array();
        let params = DvrCullParams {
            view_proj: vp,
            camera_pos: camera.position.to_array(),
            dreamlet_count: self.count,
            screen_width,
            screen_height,
            lod1_distance: observer_decohere_radius * 0.618,
            cull_distance: observer_decohere_radius * 1.618,
            lod_scale,
            _pad_align: [0.0; 3],
            _pad_lod: [0.0; 3],
            _pad_end: 0.0,
        };
        queue.write_buffer(params_buf, 0, bytemuck::bytes_of(&params));

        // Dispatch cull compute — cached bind group, zero allocation
        let workgroups = self.count.div_ceil(64);
        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
            label: Some("dvr_cull"),
            timestamp_writes: None,
        });
        pass.set_pipeline(cull_pipe);
        pass.set_bind_group(0, bg, &[]);
        pass.dispatch_workgroups(workgroups, 1, 1);
    }

    /// Dispatch physics simulation. Zero CPU per-object work.
    /// Cached bind group, zero allocation per frame.
    pub fn dispatch_physics(
        &self,
        _device: &wgpu::Device,
        _queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        _dt: f32,
    ) {
        if !self.initialized || self.count == 0 {
            return;
        }
        let Some(ref physics_pipe) = self.physics_pipeline else {
            return;
        };
        let Some(ref bg) = self.cached_physics_bg else { return };

        let workgroups = self.count.div_ceil(64);
        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
            label: Some("dvr_physics"),
            timestamp_writes: None,
        });
        pass.set_pipeline(physics_pipe);
        pass.set_bind_group(0, bg, &[]);
        pass.dispatch_workgroups(workgroups, 1, 1);
    }

    // ── Particle Physics (GPU-driven spring tether + inter-particle repulsion) ──

    /// Initialize the particle physics compute pipeline. Call once after init().
    /// Creates params buffer (128 bytes), collision sphere buffer (128 bytes),
    /// bind group layout (3 bindings), compute pipeline, and cached bind group.
    pub fn init_particle_physics(&mut self, device: &wgpu::Device) {
        use wgpu::util::DeviceExt;
        if self.particle_physics_ready {
            return;
        }
        let Some(ref dreamlet_buf) = self.dreamlet_buffer else {
            return;
        };

        // Params buffer (128 bytes)
        let params_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("particle_physics_params"),
            size: std::mem::size_of::<ParticlePhysicsParams>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        self.vram.record(std::mem::size_of::<ParticlePhysicsParams>() as u64);

        // Collision sphere buffer (8 × 16 = 128 bytes)
        let collision_buf_size = 8 * std::mem::size_of::<GpuCollisionSphere>() as u64;
        let collision_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("particle_collision_spheres"),
            size: collision_buf_size,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        self.vram.record(collision_buf_size);

        // Bind group layout: [params uniform, dreamlets storage rw, collision spheres storage ro]
        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("particle_physics_bgl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: false },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        // Group 1: Quantum + Metaphor [5 bindings: quantum_context, kernels, wave_params, event_cue, matter_config]
        // Storage (not uniform) for quantum: QuantumContext contains [f32; N] fields.
        // Uniform buffers require std140 layout (array stride=16), but Rust [f32; N] has stride=4.
        // Storage buffers use std430 (stride=4), matching the Rust repr(C) layout.
        let quantum_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("particle_quantum_bgl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 4,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        // Create fallback quantum buffers (zeroed = classical, coherence_magnitude=0 → skip).
        let fallback_ctx_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("quantum_context_fallback"),
            size: std::mem::size_of::<GpuQuantumContext>() as u64,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let fallback_kernel_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("interference_kernels_fallback"),
            size: std::mem::size_of::<GpuParticleKernel>() as u64, // 1 entry minimum
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        // Fallback WaveParams (zeroed = neutral biases).
        let fallback_wave_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("wave_params_fallback"),
            size: 64, // padded for uniform alignment
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        self.vram.record(64);

        // Fallback EventCue (zeroed = no active cue).
        let fallback_event_cue_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("event_cue_fallback"),
            size: 16,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        self.vram.record(16);

        // Fallback MatterConfig (active_count=0xFFFFFFFF = all active, rest zeroed).
        // MatterConfigData layout: [active_count: u32, density_budget: f32, meshlet_policy: u32, physics_policy: u32]
        let matter_default: [u32; 4] = [u32::MAX, 0, 0, 0];
        let fallback_matter_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("matter_config_fallback"),
            contents: bytemuck::cast_slice(&matter_default),
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
        });
        self.vram.record(16);

        let quantum_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("particle_quantum_bg_fallback"),
            layout: &quantum_bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: fallback_ctx_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: fallback_kernel_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: fallback_wave_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: fallback_event_cue_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: fallback_matter_buf.as_entire_binding(),
                },
            ],
        });

        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("particle_physics.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/particle_physics.wgsl").into()),
        });

        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("particle_physics_layout"),
            bind_group_layouts: &[Some(&bgl), Some(&quantum_bgl)],
            immediate_size: 0,
        });

        let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("particle_physics_compute"),
            layout: Some(&layout),
            module: &shader,
            entry_point: Some("particle_main"),
            compilation_options: Default::default(),
            cache: None,
        });

        // Cached bind group (3 bindings)
        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("particle_physics_bg"),
            layout: &bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: params_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: dreamlet_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: collision_buf.as_entire_binding(),
                },
            ],
        });

        self.particle_params_buffer = Some(params_buf);
        self.collision_sphere_buffer = Some(collision_buf);
        self.particle_physics_bgl = Some(bgl);
        self.quantum_bgl = Some(quantum_bgl);
        self.cached_quantum_bg = Some(quantum_bg);
        self.particle_physics_pipeline = Some(pipeline);
        self.cached_particle_physics_bg = Some(bg);
        self.particle_physics_ready = true;

        // Store fallback quantum buffers for later replacement.
        self.quantum_context_buf = Some(fallback_ctx_buf);
        self.interference_kernel_buf = Some(fallback_kernel_buf);
        self.wave_params_buf = Some(fallback_wave_buf);
        self.wave_params_ready = true;
        self.event_cue_buf = Some(fallback_event_cue_buf);
        self.matter_config_buf = Some(fallback_matter_buf);

        log::info!("Particle physics pipeline compiled (PBF fluid sim, 14 phases + quantum bridge + metaphor)");
    }

    // ── Quantum Bridge ──────────────────────────────────────────────

    /// Initialize quantum bridge buffers. Call once after init_particle_physics().
    /// Replaces fallback zero buffers with real-sized quantum context + kernel storage.
    /// Rebuilds the quantum bind group (group 1) with real buffers.
    pub fn init_quantum_bridge(&mut self, device: &wgpu::Device, particle_count: u32) {
        if self.quantum_bridge_ready {
            return;
        }
        let Some(ref quantum_bgl) = self.quantum_bgl else {
            return;
        };

        // Replace fallback buffers with properly sized ones.
        let ctx_size = std::mem::size_of::<GpuQuantumContext>() as u64;
        let ctx_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("quantum_context"),
            size: ctx_size,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        self.vram.record(ctx_size);

        let kernel_count = particle_count.max(1);
        let kernel_size = (kernel_count as u64) * std::mem::size_of::<GpuParticleKernel>() as u64;
        let kernel_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("interference_kernels"),
            size: kernel_size,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        self.vram.record(kernel_size);

        // Rebuild quantum bind group with real quantum buffers + existing metaphor buffers.
        let wave_buf = self
            .wave_params_buf
            .as_ref()
            .expect("wave_params_buf must exist after init_particle_physics");
        let event_buf = self
            .event_cue_buf
            .as_ref()
            .expect("event_cue_buf must exist after init_particle_physics");
        let matter_buf = self
            .matter_config_buf
            .as_ref()
            .expect("matter_config_buf must exist after init_particle_physics");
        self.cached_quantum_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("particle_quantum_bg"),
            layout: quantum_bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: ctx_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: kernel_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wave_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: event_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: matter_buf.as_entire_binding(),
                },
            ],
        }));

        self.quantum_context_buf = Some(ctx_buf);
        self.interference_kernel_buf = Some(kernel_buf);
        self.quantum_bridge_ready = true;

        log::info!(
            "Quantum bridge initialized: context {}B, kernels {} × {}B = {:.1} KB",
            ctx_size,
            particle_count,
            std::mem::size_of::<GpuParticleKernel>(),
            kernel_size as f64 / 1024.0,
        );
    }

    /// Upload quantum context to GPU. Call each frame after CPU kernel tick.
    pub fn upload_quantum_context(&self, queue: &wgpu::Queue, ctx: &GpuQuantumContext) {
        if let Some(ref buf) = self.quantum_context_buf {
            queue.write_buffer(buf, 0, bytemuck::bytes_of(ctx));
        }
    }

    /// Upload interference kernels to GPU. Call once at scene load.
    pub fn upload_interference_kernels(&self, queue: &wgpu::Queue, kernels: &[GpuParticleKernel]) {
        if let Some(ref buf) = self.interference_kernel_buf {
            if !kernels.is_empty() {
                queue.write_buffer(buf, 0, bytemuck::cast_slice(kernels));
            }
        }
    }

    /// Initialize wave params buffer (56 bytes uniform).
    /// Usually already created by init_particle_physics(). This is a no-op if already ready.
    pub fn init_wave_params(&mut self, device: &wgpu::Device) {
        if self.wave_params_ready {
            return;
        }
        let size = std::mem::size_of::<dreamwell_metaphors::WaveParams>() as u64;
        let buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("wave_params"),
            size: size.max(64), // pad to 64 for uniform alignment
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        self.vram.record(size.max(64));
        self.wave_params_buf = Some(buf);
        self.wave_params_ready = true;
        log::info!("WaveParams buffer initialized: {}B", size);
    }

    /// Upload wave params to GPU. Call each frame after metaphor evaluation.
    /// CPU cost: 56 bytes written.
    pub fn upload_wave_params(&self, queue: &wgpu::Queue, params: &dreamwell_metaphors::WaveParams) {
        if let Some(ref buf) = self.wave_params_buf {
            queue.write_buffer(buf, 0, bytemuck::bytes_of(params));
        }
    }

    /// Upload event cue data to GPU. Call each frame when a cue is active.
    /// CPU cost: 16 bytes written.
    pub fn upload_event_cue(&self, queue: &wgpu::Queue, data: &GpuEventCueData) {
        if let Some(ref buf) = self.event_cue_buf {
            queue.write_buffer(buf, 0, bytemuck::bytes_of(data));
        }
    }

    /// Upload matter config to GPU. Call each frame after tier evaluation.
    /// CPU cost: 16 bytes written.
    pub fn upload_matter_config(&self, queue: &wgpu::Queue, data: &GpuMatterConfig) {
        if let Some(ref buf) = self.matter_config_buf {
            queue.write_buffer(buf, 0, bytemuck::bytes_of(data));
        }
    }

    /// Upload wave material bias to GPU. Call each frame after metaphor evaluation.
    /// CPU cost: 16 bytes written.
    pub fn upload_wave_material(&self, queue: &wgpu::Queue, data: &GpuWaveMaterialBias) {
        if let Some(ref buf) = self.wave_material_buf {
            queue.write_buffer(buf, 0, bytemuck::bytes_of(data));
        }
    }

    /// Upload particle physics params. Call each frame before dispatch_particle_physics().
    /// CPU cost: 128 bytes written.
    pub fn upload_particle_params(&self, queue: &wgpu::Queue, params: &ParticlePhysicsParams) {
        let Some(ref buf) = self.particle_params_buffer else {
            return;
        };
        queue.write_buffer(buf, 0, bytemuck::bytes_of(params));
    }

    /// Upload collision spheres for scene obstacle avoidance (up to 8).
    /// CPU cost: up to 128 bytes written.
    pub fn upload_collision_spheres(&self, queue: &wgpu::Queue, spheres: &[GpuCollisionSphere]) {
        let Some(ref buf) = self.collision_sphere_buffer else {
            return;
        };
        let n = spheres.len().min(8);
        if n > 0 {
            queue.write_buffer(buf, 0, bytemuck::cast_slice(&spheres[..n]));
        }
    }

    /// Dispatch particle physics compute. Updates dreamlet positions in-place on GPU.
    /// Must be called BEFORE dispatch_cull() so cull reads updated positions.
    /// CPU cost: 1 compute dispatch. GPU cost: O(N²) pair checks, trivial for N=128.
    /// Group 0: particle params + dreamlets + collision spheres.
    /// Group 1: quantum context + interference kernels (fallback zeros when not active).
    pub fn dispatch_particle_physics(&self, encoder: &mut wgpu::CommandEncoder) {
        if !self.particle_physics_ready || self.count == 0 {
            return;
        }
        let Some(ref pipe) = self.particle_physics_pipeline else {
            return;
        };
        let Some(ref bg) = self.cached_particle_physics_bg else {
            return;
        };
        let Some(ref qbg) = self.cached_quantum_bg else { return };

        let workgroups = self.count.div_ceil(64);
        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
            label: Some("particle_physics"),
            timestamp_writes: None,
        });
        pass.set_pipeline(pipe);
        pass.set_bind_group(0, bg, &[]);
        pass.set_bind_group(1, qbg, &[]);
        pass.dispatch_workgroups(workgroups, 1, 1);
    }

    /// Render visible dreamlets using multi_draw_indexed_indirect.
    /// The indirect buffer was filled by dispatch_cull.
    pub fn render(
        &self,
        pass: &mut wgpu::RenderPass<'_>,
        merged_vertex_buffer: &wgpu::Buffer,
        merged_index_buffer: &wgpu::Buffer,
    ) {
        if !self.initialized || self.count == 0 {
            return;
        }
        let Some(ref indirect_buf) = self.indirect_buffer else {
            return;
        };

        pass.set_vertex_buffer(0, merged_vertex_buffer.slice(..));
        pass.set_index_buffer(merged_index_buffer.slice(..), wgpu::IndexFormat::Uint32);

        // multi_draw_indexed_indirect: GPU processes all entries.
        // Culled dreamlets have instance_count=0 (zero-cost skip).
        pass.multi_draw_indexed_indirect(indirect_buf, 0, self.count);
    }

    // ── MicroDreamlet support (64-byte compact, 2× density) ───────

    /// Compile micro cull + render pipelines (micro_dreamlet_cull.wgsl, pbr_micro_dreamlet.wgsl).
    /// Call after init() + init_render_pipeline(). Requires render_bgl_scene for pipeline layout reuse.
    pub fn init_micro_pipelines(&mut self, device: &wgpu::Device, target_format: wgpu::TextureFormat) {
        if self.micro_render_pipeline_ready {
            return;
        }

        // ── Micro Cull Pipeline (micro_dreamlet_cull.wgsl) ──
        // 4 bindings: params(uniform), dreamlets(storage,read), mesh_descs(storage,read), indirect(storage,rw)
        let micro_cull_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("micro_cull_bgl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: false },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        let micro_cull_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("micro_dreamlet_cull.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/micro_dreamlet_cull.wgsl").into()),
        });

        let micro_cull_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("micro_cull_layout"),
            bind_group_layouts: &[Some(&micro_cull_bgl)],
            immediate_size: 0,
        });

        self.micro_cull_pipeline = Some(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("micro_cull_compute"),
            layout: Some(&micro_cull_layout),
            module: &micro_cull_shader,
            entry_point: Some("cull_main"),
            compilation_options: Default::default(),
            cache: None,
        }));

        self.micro_cull_bgl = Some(micro_cull_bgl);

        // ── Micro Render Pipeline (pbr_micro_dreamlet.wgsl) ──
        // Reuses render_bgl_scene for @group(0) — same layout (scene+lights+dreamlets).
        // No @group(1) — micro PBR uses flat ambient fallback (no IBL).
        let render_bgl = self
            .render_bgl_scene
            .as_ref()
            .expect("init_render_pipeline() must be called before init_micro_pipelines()");

        let micro_render_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("micro_render_layout"),
            bind_group_layouts: &[Some(render_bgl)],
            immediate_size: 0,
        });

        let micro_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("pbr_micro_dreamlet.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/pbr_micro_dreamlet.wgsl").into()),
        });

        self.micro_render_pipeline = Some(device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("micro_pbr_dreamlet"),
            layout: Some(&micro_render_layout),
            vertex: wgpu::VertexState {
                module: &micro_shader,
                entry_point: Some("vs_main"),
                compilation_options: Default::default(),
                // PrimitiveVertex: pos(12) + normal(12) + color(16) = 40 bytes
                buffers: &[wgpu::VertexBufferLayout {
                    array_stride: 40,
                    step_mode: wgpu::VertexStepMode::Vertex,
                    attributes: &wgpu::vertex_attr_array![
                        0 => Float32x3, // position
                        1 => Float32x3, // normal
                        2 => Float32x4, // color
                    ],
                }],
            },
            fragment: Some(wgpu::FragmentState {
                module: &micro_shader,
                entry_point: Some("fs_main"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: target_format,
                    blend: Some(wgpu::BlendState {
                        color: wgpu::BlendComponent {
                            src_factor: wgpu::BlendFactor::SrcAlpha,
                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
                            operation: wgpu::BlendOperation::Add,
                        },
                        alpha: wgpu::BlendComponent::OVER,
                    }),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: Some(wgpu::Face::Back),
                ..Default::default()
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: crate::formats::DEPTH_FORMAT,
                depth_write_enabled: Some(true),
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        }));

        self.micro_render_pipeline_ready = true;
        self.ensure_micro_cull_bind_group(device);

        log::info!("DVR micro pipelines compiled (micro_dreamlet_cull.wgsl + pbr_micro_dreamlet.wgsl)");
    }

    /// Build and cache the micro cull bind group.
    pub fn ensure_micro_cull_bind_group(&mut self, device: &wgpu::Device) {
        if self.cached_micro_cull_bg.is_some() {
            return;
        }
        let Some(ref bgl) = self.micro_cull_bgl else { return };
        let Some(ref dreamlet_buf) = self.dreamlet_buffer else {
            return;
        };
        let Some(ref indirect_buf) = self.indirect_buffer else {
            return;
        };
        let Some(ref params_buf) = self.cull_params_buffer else {
            return;
        };
        let Some(ref merged) = self.merged_mesh else { return };

        self.cached_micro_cull_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("micro_cull_bg"),
            layout: bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: params_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: dreamlet_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: merged.descriptor_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: indirect_buf.as_entire_binding(),
                },
            ],
        }));
    }

    /// Dispatch micro-dreamlet visibility cull. GPU reads 64B per entry (vs 128B standard).
    /// Per-frame cost: 64-byte uniform write + 1 compute dispatch.
    pub fn dispatch_micro_cull(
        &self,
        _device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        camera: &crate::camera::Camera,
        _screen_width: f32,
        _screen_height: f32,
    ) {
        if !self.initialized || self.count == 0 {
            return;
        }
        let Some(ref cull_pipe) = self.micro_cull_pipeline else {
            return;
        };
        let Some(ref params_buf) = self.cull_params_buffer else {
            return;
        };
        let Some(ref bg) = self.cached_micro_cull_bg else {
            return;
        };

        // Upload MicroCullParams (64 bytes — just view_proj)
        let vp = (camera.projection_matrix * camera.view_matrix).to_cols_array();
        let params = crate::micro_dreamlet::MicroCullParams { view_proj: vp };
        queue.write_buffer(params_buf, 0, bytemuck::bytes_of(&params));

        let workgroups = self.count.div_ceil(64);
        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
            label: Some("micro_cull"),
            timestamp_writes: None,
        });
        pass.set_pipeline(cull_pipe);
        pass.set_bind_group(0, bg, &[]);
        pass.dispatch_workgroups(workgroups, 1, 1);
    }

    /// Render visible micro-dreamlets using the micro PBR pipeline.
    /// Uses the same render_bg_scene bind group (shared BGL).
    pub fn render_micro_dvr(&self, pass: &mut wgpu::RenderPass<'_>) {
        if !self.initialized || !self.micro_render_pipeline_ready || self.count == 0 {
            return;
        }
        let Some(ref pipeline) = self.micro_render_pipeline else {
            return;
        };
        let Some(ref bg_scene) = self.render_bg_scene else {
            return;
        };
        let Some(ref indirect_buf) = self.indirect_buffer else {
            return;
        };
        let Some(ref merged) = self.merged_mesh else { return };

        pass.set_pipeline(pipeline);
        pass.set_bind_group(0, bg_scene, &[]);
        pass.set_vertex_buffer(0, merged.vertex_buffer.slice(..));
        pass.set_index_buffer(merged.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
        pass.multi_draw_indexed_indirect(indirect_buf, 0, self.count);
    }

    /// Append a batch of micro-dreamlets at the current count offset.
    /// CPU cost: O(K) where K = batch.len(). Does not touch existing data.
    pub fn upload_micro_batch(&mut self, queue: &wgpu::Queue, batch: &[crate::micro_dreamlet::MicroDreamlet]) {
        let Some(ref buf) = self.dreamlet_buffer else { return };
        if batch.is_empty() {
            return;
        }

        let micro_size = std::mem::size_of::<crate::micro_dreamlet::MicroDreamlet>() as u64;
        let offset = self.count as u64 * micro_size;
        let new_total = self.count + batch.len() as u32;

        if new_total > self.capacity {
            let safe = (self.capacity - self.count) as usize;
            if safe == 0 {
                return;
            }
            log::warn!("Micro: batch clamped to {} (capacity {})", safe, self.capacity);
            queue.write_buffer(buf, offset, bytemuck::cast_slice(&batch[..safe]));
            self.count = self.capacity;
        } else {
            queue.write_buffer(buf, offset, bytemuck::cast_slice(batch));
            self.count = new_total;
        }
        self.micro_mode = true;
    }

    /// Upload micro-dreamlets to the dreamlet buffer at 64 bytes per entry.
    /// Capped at capacity (not 2×) because the indirect buffer has capacity entries.
    /// The micro cull shader reads 64B per entry matching the MicroDreamlet layout.
    pub fn upload_micro(&mut self, queue: &wgpu::Queue, micro_dreamlets: &[crate::micro_dreamlet::MicroDreamlet]) {
        let Some(ref buf) = self.dreamlet_buffer else { return };
        if micro_dreamlets.is_empty() {
            return;
        }
        let safe = (micro_dreamlets.len() as u32).min(self.capacity);
        queue.write_buffer(buf, 0, bytemuck::cast_slice(&micro_dreamlets[..safe as usize]));
        self.count = safe;
        self.micro_mode = true;
    }

    /// Upload destruction events. Call when authority declares object destruction.
    pub fn upload_destruction_events(&self, queue: &wgpu::Queue, events: &[DestructionEvent]) {
        let Some(ref buf) = self.destruction_buffer else { return };
        if events.is_empty() {
            return;
        }
        let max = 256.min(events.len());
        queue.write_buffer(buf, 0, bytemuck::cast_slice(&events[..max]));
    }

    pub fn destroy(&mut self) {
        // Free GPU buffers and reverse VRAM tracking
        if let Some(b) = self.dreamlet_buffer.take() {
            self.vram.free(b.size());
            b.destroy();
        }
        if let Some(b) = self.indirect_buffer.take() {
            self.vram.free(b.size());
            b.destroy();
        }
        if let Some(b) = self.count_buffer.take() {
            self.vram.free(b.size());
            b.destroy();
        }
        if let Some(b) = self.cull_params_buffer.take() {
            self.vram.free(b.size());
            b.destroy();
        }
        if let Some(b) = self.destruction_buffer.take() {
            self.vram.free(b.size());
            b.destroy();
        }
        if let Some(b) = self.scene_uniform_buffer.take() {
            self.vram.free(b.size());
            b.destroy();
        }
        if let Some(m) = self.merged_mesh.take() {
            m.destroy();
        }
        // Drop pipelines and layouts explicitly
        self.cull_pipeline = None;
        self.physics_pipeline = None;
        self.render_pipeline = None;
        self.micro_cull_pipeline = None;
        self.micro_render_pipeline = None;
        self.cull_bgl = None;
        self.physics_bgl = None;
        self.render_bgl_scene = None;
        self.render_bgl_ibl = None;
        self.micro_cull_bgl = None;
        // Drop cached bind groups
        self.cached_cull_bg = None;
        self.cached_physics_bg = None;
        self.cached_micro_cull_bg = None;
        self.render_bg_scene = None;
        self.render_bg_ibl = None;
        // Reset state
        self.render_pipeline_ready = false;
        self.render_pipeline_format = None;
        self.micro_render_pipeline_ready = false;
        self.micro_mode = false;
        self.count = 0;
        self.capacity = 0;
        self.initialized = false;
    }
}

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

    #[test]
    fn dvr_scene_uniforms_size() {
        assert_eq!(std::mem::size_of::<DvrSceneUniforms>(), 112);
    }

    #[test]
    fn gpu_dreamlet_size() {
        assert_eq!(std::mem::size_of::<GpuDreamlet>(), 144);
    }

    #[test]
    fn dvr_cull_params_size() {
        assert_eq!(std::mem::size_of::<DvrCullParams>(), 128);
    }

    #[test]
    fn destruction_event_size() {
        assert_eq!(std::mem::size_of::<DestructionEvent>(), 24);
    }

    #[test]
    fn dvr_draw_count_size() {
        assert_eq!(std::mem::size_of::<DvrDrawCount>(), 4);
    }

    #[test]
    fn dreamlet_flags() {
        assert_eq!(DREAMLET_FLAG_ACTIVE, 1);
        assert_eq!(DREAMLET_FLAG_PHYSICS, 2);
        assert_eq!(DREAMLET_FLAG_PROMOTABLE, 4);
    }

    #[test]
    fn dreamlet_states() {
        assert_eq!(DREAMLET_STATE_STATIC, 0);
        assert_eq!(DREAMLET_STATE_DISSOLVING, 4);
    }

    /// Generate simple unit-sphere offsets for testing (no engine dependency).
    fn test_sphere_offsets(count: usize) -> Vec<[f32; 3]> {
        let golden_ratio = (1.0 + 5.0_f32.sqrt()) / 2.0;
        (0..count)
            .map(|i| {
                let theta = 2.0 * std::f32::consts::PI * i as f32 / golden_ratio;
                let phi = (1.0 - 2.0 * (i as f32 + 0.5) / count as f32).acos();
                [phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos()]
            })
            .collect()
    }

    #[test]
    fn generate_particle_correct_count() {
        let offsets = test_sphere_offsets(128);
        let dreamlets = generate_particle([0.0, 1.0, 0.0], 128, 1.0, [0.4, 0.6, 1.0, 0.85], &offsets);
        assert_eq!(dreamlets.len(), 128);
    }

    #[test]
    fn generate_particle_positions_near_center() {
        let offsets = test_sphere_offsets(64);
        let center = [5.0, 3.0, -2.0];
        let radius = 1.5;
        let dreamlets = generate_particle(center, 64, radius, [1.0; 4], &offsets);
        for d in &dreamlets {
            let dx = d.position[0] - center[0];
            let dy = d.position[1] - center[1];
            let dz = d.position[2] - center[2];
            let dist = (dx * dx + dy * dy + dz * dz).sqrt();
            assert!(dist <= radius * 1.1, "dreamlet at dist {dist} exceeds radius {radius}");
        }
    }

    #[test]
    fn generate_particle_material_applied() {
        let offsets = test_sphere_offsets(4);
        let color = [0.1, 0.2, 0.3, 0.9];
        let dreamlets = generate_particle([0.0; 3], 4, 1.0, color, &offsets);
        for d in &dreamlets {
            assert_eq!(d.base_color, color);
            assert_eq!(d.state, DREAMLET_STATE_PARTICLE);
            assert!(d.flags & DREAMLET_FLAG_ACTIVE != 0);
        }
    }

    #[test]
    fn update_particle_positions_moves_toward_center() {
        let offsets = test_sphere_offsets(16);
        let mut dreamlets = generate_particle([0.0; 3], 16, 1.0, [1.0; 4], &offsets);
        let new_center = [10.0, 5.0, -3.0];
        update_particle_positions(
            &mut dreamlets,
            new_center,
            [1.0, 0.0, 0.0],
            &offsets,
            [1.0, 1.0, 1.0],
            1.0,
            0.05,
            0.0,
            0.0,
        );
        for d in &dreamlets {
            let dx = d.position[0] - new_center[0];
            let dy = d.position[1] - new_center[1];
            let dz = d.position[2] - new_center[2];
            let dist = (dx * dx + dy * dy + dz * dz).sqrt();
            assert!(dist < 5.0, "dreamlet too far from new center: {dist}");
        }
    }

    #[test]
    fn update_particle_positions_stretch_with_velocity() {
        let offsets = test_sphere_offsets(8);
        let mut dreamlets = generate_particle([0.0; 3], 8, 1.0, [1.0; 4], &offsets);
        let velocity = [5.0, 0.0, 0.0];
        let stretch = [1.5, 0.8, 0.8];
        update_particle_positions(
            &mut dreamlets,
            [0.0; 3],
            velocity,
            &offsets,
            stretch,
            1.0,
            0.05,
            0.0,
            0.0,
        );
        for d in &dreamlets {
            assert_eq!(d.target_position, [0.0, 0.0, 0.0]);
            assert_eq!(d.velocity, velocity);
        }
    }

    #[test]
    fn generate_particle_zero_count() {
        let dreamlets = generate_particle([0.0; 3], 0, 1.0, [1.0; 4], &[]);
        assert!(dreamlets.is_empty());
    }

    #[test]
    fn gpu_dreamlet_repr_c() {
        // Verify Pod/Zeroable compliance (compile-time via bytemuck).
        let d = GpuDreamlet {
            position: [0.0; 3],
            scale: 1.0,
            velocity: [0.0; 3],
            angular_velocity: 0.0,
            rotation: [0.0, 0.0, 0.0, 1.0],
            base_color: [1.0; 4],
            roughness: 0.5,
            metallic: 0.0,
            mass: 1.0,
            restitution: 0.3,
            state: 0,
            mesh_id: 0,
            parent_id: 0,
            flags: 0,
            age: 0.0,
            lifetime: 0.0,
            _pad_target: [0.0; 2],
            target_position: [0.0; 3],
            deconstruct_level: 0,
            bounding_radius: 1.0,
            skeleton_index: 0,
            _pad_struct: [0.0; 2],
        };
        let bytes: &[u8] = bytemuck::bytes_of(&d);
        assert_eq!(bytes.len(), 144);
    }

    #[test]
    fn gpu_quantum_context_size() {
        assert_eq!(std::mem::size_of::<GpuQuantumContext>(), 144);
    }

    #[test]
    fn gpu_quantum_context_alignment() {
        assert_eq!(std::mem::size_of::<GpuQuantumContext>() % 16, 0);
    }

    #[test]
    fn gpu_quantum_context_classical_default() {
        let ctx = GpuQuantumContext::classical();
        assert_eq!(ctx.populations[0], 1.0);
        assert_eq!(ctx.populations[1], 0.0);
        assert_eq!(ctx.coherence_magnitude, 0.0);
        assert_eq!(ctx.purity, 1.0);
        assert_eq!(ctx.measured_mode, 0);
    }

    #[test]
    fn gpu_particle_kernel_size() {
        assert_eq!(std::mem::size_of::<GpuParticleKernel>(), 256);
    }

    #[test]
    fn gpu_particle_kernel_alignment() {
        assert_eq!(std::mem::size_of::<GpuParticleKernel>() % 16, 0);
    }

    #[test]
    fn gpu_quantum_context_bytemuck_roundtrip() {
        let ctx = GpuQuantumContext::classical();
        let bytes = bytemuck::bytes_of(&ctx);
        assert_eq!(bytes.len(), 144);
        let roundtrip: &GpuQuantumContext = bytemuck::from_bytes(bytes);
        assert_eq!(roundtrip.populations, ctx.populations);
        assert_eq!(roundtrip.purity, ctx.purity);
    }

    #[test]
    fn gpu_quantum_context_validate_classical() {
        let ctx = GpuQuantumContext::classical();
        assert!(ctx.validate().is_ok());
    }

    #[test]
    fn gpu_quantum_context_validate_bad_trace() {
        let mut ctx = GpuQuantumContext::classical();
        ctx.populations = [0.5, 0.5, 0.5, 0.0, 0.0]; // sum = 1.5
        assert!(ctx.validate().is_err());
    }

    #[test]
    fn gpu_quantum_context_validate_nan() {
        let mut ctx = GpuQuantumContext::classical();
        ctx.coherence_magnitude = f32::NAN;
        assert!(ctx.validate().is_err());
    }

    #[test]
    fn gpu_quantum_context_validate_purity_out_of_range() {
        let mut ctx = GpuQuantumContext::classical();
        ctx.purity = 0.1; // below 0.19
        assert!(ctx.validate().is_err());
    }

    /// Decisive coherence sensitivity test (QUANTUM.md S7):
    ///   x_n(B(ρ)) ≠ x_n(B(Δ(ρ)))
    /// Constructs a superposition state, builds the GPU bridge, computes
    /// interference displacement, then dephases and verifies it becomes zero.
    #[test]
    fn gpu_bridge_coherence_sensitivity() {
        // 1. Superposition |ψ⟩ = (|0⟩ + |1⟩)/√2 → ρ with non-zero ρ_{01}
        //    populations: [0.5, 0.5, 0, 0, 0]
        //    coherences: Re(ρ_{01})=0.5, Im(ρ_{01})=0.0, rest zero
        let mut coherences_super = [0.0f32; 20];
        coherences_super[0] = 0.5; // Re(ρ_{01})
        coherences_super[1] = 0.0; // Im(ρ_{01})

        let ctx_super = GpuQuantumContext {
            populations: [0.5, 0.5, 0.0, 0.0, 0.0],
            coherence_magnitude: 0.5,                 // |ρ_{01}| = 0.5
            purity: 1.0,                              // pure state
            entropy: 2.0 * 0.5 * (0.5f32.ln()).abs(), // = ln(2) ≈ 0.693
            coherences: coherences_super,
            decoherence_epsilon: 0.0,
            temperature: 1.0 / (1.0 + 0.5),
            phase_time: 0.0,
            measured_mode: 0,
            feature_flags: 0x0001, // COHERE
            free_energy: 0.0,
            asymmetry: 0.0,
            _pad_ff_c: 0,
        };
        assert!(ctx_super.validate().is_ok());

        // 2. Build a test kernel for particle 0 with nonzero (0,1) pair
        let mut kernel_data = [0.0f32; 60];
        // Pair 0 = (0,1), axis X: Re=0.3, Im=0.1
        kernel_data[0] = 0.3; // pair 0, axis 0, Re
        kernel_data[1] = 0.1; // pair 0, axis 0, Im
                              // Pair 0 = (0,1), axis Y: Re=0.2, Im=0.0
        kernel_data[2] = 0.2; // pair 0, axis 1, Re
        kernel_data[3] = 0.0; // pair 0, axis 1, Im

        let kernel = GpuParticleKernel {
            kernels: kernel_data,
            _pad: [0.0; 4],
        };

        // 3. Compute interference displacement using the GPU formula:
        //    x_n += coherence_magnitude * Σ_{pair} 2·Re(κ_{pair} · ρ_{pair})
        let compute_displacement = |ctx: &GpuQuantumContext, k: &GpuParticleKernel| -> [f32; 3] {
            let mut disp = [0.0f32; 3];
            if ctx.coherence_magnitude > 0.001 {
                for pair in 0..10 {
                    let re_rho = ctx.coherences[pair * 2];
                    let im_rho = ctx.coherences[pair * 2 + 1];
                    for axis in 0..3 {
                        let re_k = k.kernels[pair * 6 + axis * 2];
                        let im_k = k.kernels[pair * 6 + axis * 2 + 1];
                        // 2·Re(κ·ρ) = 2·(Re_k·Re_ρ − Im_k·Im_ρ)
                        disp[axis] += 2.0 * (re_k * re_rho - im_k * im_rho);
                    }
                }
                for axis in 0..3 {
                    disp[axis] *= ctx.coherence_magnitude;
                }
            }
            disp
        };

        // 4. Superposition displacement should be non-zero
        let disp_super = compute_displacement(&ctx_super, &kernel);
        let mag_super =
            (disp_super[0] * disp_super[0] + disp_super[1] * disp_super[1] + disp_super[2] * disp_super[2]).sqrt();
        assert!(
            mag_super > 0.01,
            "superposition displacement must be non-zero, got {mag_super}"
        );

        // 5. Dephase: ρ → Δ(ρ), zero all off-diagonals
        let ctx_dephased = GpuQuantumContext {
            populations: [0.5, 0.5, 0.0, 0.0, 0.0],
            coherence_magnitude: 0.0, // fully dephased
            purity: 0.5,              // Tr(Δ(ρ)²) = 0.5² + 0.5² = 0.5
            entropy: 2.0 * 0.5 * (0.5f32.ln()).abs(),
            coherences: [0.0; 20], // all off-diagonals zeroed
            decoherence_epsilon: 0.0,
            temperature: 1.0,
            phase_time: 0.0,
            measured_mode: 0,
            feature_flags: 0x0002, // DECOHERE
            free_energy: 0.0,
            asymmetry: 0.0,
            _pad_ff_c: 0,
        };

        // 6. Dephased displacement should be zero (classical limit)
        let disp_dephased = compute_displacement(&ctx_dephased, &kernel);
        let mag_dephased = (disp_dephased[0] * disp_dephased[0]
            + disp_dephased[1] * disp_dephased[1]
            + disp_dephased[2] * disp_dephased[2])
            .sqrt();
        assert!(
            mag_dephased < 1e-6,
            "dephased displacement must be zero, got {mag_dephased}"
        );

        // Decisive: x_n(B(ρ)) ≠ x_n(B(Δ(ρ))) — coherence is simulation, not metaphor.
        assert!(
            (mag_super - mag_dephased).abs() > 0.01,
            "bridge must be coherence-sensitive: super={mag_super}, dephased={mag_dephased}"
        );
    }

    #[test]
    fn generate_particle_scale_matches_bounding_radius() {
        let offsets = test_sphere_offsets(8);
        let dreamlets = generate_particle([0.0; 3], 8, 1.0, [1.0; 4], &offsets);
        for d in &dreamlets {
            // Particle scale uses DEFAULT_PARTICLE_SCALE (0.045).
            // Bounding radius is a conservative cull sphere (0.12) — intentionally larger
            // than visual scale to prevent popping during physics displacement.
            assert_eq!(
                d.scale,
                dreamwell_metaphors::DEFAULT_PARTICLE_SCALE,
                "particle particle scale should match DEFAULT_PARTICLE_SCALE"
            );
            assert_eq!(d.bounding_radius, 0.12, "bounding radius should be 0.12");
        }
    }

    #[test]
    fn dvr_direct_threshold() {
        // render_dvr_direct is used for count <= 256.
        // This test validates the threshold logic matches the expected particle size.
        let particle_count = 256u32;
        assert!(particle_count <= 256, "particle should use direct render path");
        let large_count = 257u32;
        assert!(large_count > 256, "large counts should use indirect render path");
    }

    #[test]
    fn build_particle_physics_params_from_descriptor() {
        use dreamwell_attention::*;

        let mut sbc = SuperBodyController::new([0.0, 0.5, 0.0], 128, 42);
        let input = InputPacket {
            movement: [0.0, 1.0],
            sprint: true,
            ..InputPacket::idle(1.0 / 60.0, 0)
        };
        let (_, desc) = sbc.tick(&input, &[]);
        let params = build_particle_physics_params(&desc, 1.0 / 60.0, 128, 1.0);

        assert_eq!(params.dreamlet_count, 128);
        assert_eq!(params.anchor_position, desc.position);
        assert!((params.cloud_radius - desc.cloud_radius).abs() < 1e-6);
        assert!((params.cohesion_strength - desc.cohesion_strength).abs() < 1e-6);
        assert!((params.vorticity_epsilon - desc.vorticity_epsilon).abs() < 1e-6);
        assert!((params.rheology_blend - desc.rheology_blend).abs() < 1e-6);
        assert!((params.particle_base_scale - desc.shape.particle_scale).abs() < 1e-6);
    }
}
