Skip to main content

dreamwell_runtime/
play.rs

1// SimulationService — the DreamRealityContainer.
2//
3// Dual-purpose: Studio→Play preview AND Published .exe builds.
4// Manages the full play lifecycle: Braid mount → reality check → scene instantiation
5// → CausalComputeKernel init → GPU sync → stitch → first frame confirmation → frame loop → shutdown.
6//
7// Input is NEVER enabled before the 7-step initialization completes.
8// The Seer must be Happy.
9//
10// Uses CausalComputeKernel + SDK Decoherence instead of ParticleController.
11// Every input is dispatched through Oracle as a SacredWeave with BLAKE3 attestation.
12
13use dreamwell_attention::encoder::CausalEngineEncoder;
14use dreamwell_attention::{CausalComputeKernel, InputPacket, KernelResult};
15use dreamwell_engine::game_object::GameObjectScene;
16use dreamwell_engine::input::particle::particle_sphere_offsets;
17use dreamwell_engine::physics::simulation::SuperpositionObserver;
18use dreamwell_fabric::causal_observer_lane::{causal_observer_lane_channel, CausalObserverLaneReceiver};
19use dreamwell_gates::{DreamGate, GateConfig};
20use dreamwell_gpu::quantum_bridge::QuantumBridge;
21use dreamwell_sdk::{
22    decohere::{Decoherence, DecoherenceConfig, DecoherenceInputMode},
23    ledger_lane_channel,
24    loom_budgeter::LoomBudgeter,
25    loom_limiter::LoomLimiter,
26    LedgerLaneReceiver, Oracle, OracleConfig,
27};
28
29/// Simulation mode discriminator.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SimulationMode {
32    /// Studio→Play preview (editor-hosted).
33    Preview,
34    /// Published standalone build.
35    Published,
36}
37
38/// Runtime readiness signal — mirrors editor RuntimeReadiness.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum RuntimeReadiness {
41    Pending,
42    Syncing,
43    Ready,
44    Failed,
45}
46
47/// A journal entry for the simulation session.
48#[derive(Debug, Clone)]
49pub struct SimulationJournalEntry {
50    pub frame: u64,
51    pub kind: String,
52    pub detail: String,
53}
54
55/// SimulationService — the DreamRealityContainer.
56pub struct SimulationService {
57    /// Active scene objects.
58    pub scene: GameObjectScene,
59    /// CausalComputeKernel — synced from encoder for collision/POI checks.
60    pub kernel: Option<CausalComputeKernel>,
61    /// CausalEngineEncoder — authoritative encode boundary (wraps SuperBodyController → Kernel).
62    pub encoder: Option<CausalEngineEncoder>,
63    /// QuantumBridge — formal CPU→GPU transfer with DreamMemory + SacredSeal.
64    pub bridge: Option<QuantumBridge>,
65    /// Last bridge packet for decoder path (app.rs consumes this).
66    pub last_bridge_packet: Option<dreamwell_gpu::quantum_bridge::BridgePacket>,
67    /// SDK Decoherence facade (input → Weave → Oracle).
68    pub decoherence: Option<Decoherence>,
69    /// Oracle — SacredPath validation + BLAKE3 attestation router.
70    pub oracle: Option<Oracle>,
71    /// Spatial decoherence zones from scene tags.
72    pub decoherence_fields: Vec<dreamwell_quantum::DecoherenceField>,
73    /// Last kernel tick result for rendering.
74    pub last_result: Option<KernelResult>,
75    /// Last SuperBody frame descriptor for GPU upload (backward compat).
76    pub last_descriptor: Option<dreamwell_attention::SuperBodyFrameDescriptor>,
77    /// Player entity ID (CanonicalObserverObject).
78    pub player_object_id: u64,
79    /// Pre-computed sphere offsets for particle particles.
80    pub particle_offsets: Vec<[f32; 3]>,
81    /// Observer context for quantum culling.
82    pub observer: SuperpositionObserver,
83    /// Session journal.
84    pub journal: Vec<SimulationJournalEntry>,
85    /// Console messages (for UI display).
86    pub console: Vec<String>,
87    /// Simulation mode.
88    pub mode: SimulationMode,
89    /// Readiness signal — input is gated on this.
90    pub readiness: RuntimeReadiness,
91    /// Current frame index.
92    pub frame: u64,
93    /// Total ticks processed.
94    pub tick: u64,
95    /// Quantum cull radius in meters.
96    pub quantum_cull_radius: f32,
97    /// Accumulated elapsed time in seconds.
98    pub elapsed_time: f32,
99    /// DreamGate — high-throughput Weave validation and dispatch.
100    pub gate: Option<DreamGate>,
101    /// LoomLimiter — per-frame mutation rate limiting.
102    pub limiter: Option<LoomLimiter>,
103    /// LoomBudgeter — hardware-derived resource budgets.
104    pub budgeter: Option<LoomBudgeter>,
105    /// LedgerLane receiver for cold-path draining.
106    pub ledger_rx: Option<LedgerLaneReceiver>,
107    /// CausalObserverLane receiver for GPU frame cold-path draining.
108    pub causal_observer_rx: Option<CausalObserverLaneReceiver>,
109    /// CausalObserverLane sender (stored here for wiring to renderer).
110    pub causal_observer_tx: Option<dreamwell_fabric::CausalObserverLaneSender>,
111    /// Active topology layer index (0-9).
112    pub active_zone_index: usize,
113    /// Metaphor profile for the bridge evaluator.
114    pub metaphor_profile: dreamwell_metaphors::MetaphorProfile,
115    /// Active particle count (decremented by POI siphon, regenerated over time).
116    pub active_particle_count: f32,
117    /// Form coherence target: 1.0=Cohere (fibonacci), 0.0=Wave (sinusoidal). TAB cycles.
118    pub coherence_target: f32,
119    /// Current coherence — smooth-lerped toward target (0.5s transition).
120    pub coherence_current: f32,
121    /// Gather mode active (Ctrl held).
122    pub gather_active: bool,
123    /// Primary action emit strength [0,1].
124    pub emit_strength: f32,
125    /// Universe connection: snapshot receiver (from QuantumServer).
126    pub universe_snapshot_rx: Option<crossbeam_channel::Receiver<dreamwell_universe::FieldSnapshot>>,
127    /// Universe connection: update sender (to QuantumServer).
128    pub universe_update_tx: Option<crossbeam_channel::Sender<dreamwell_universe::ClientUpdate>>,
129    /// Universe client ID.
130    pub universe_client_id: u64,
131    /// Whether universe is connected.
132    pub universe_connected: bool,
133}
134
135impl SimulationService {
136    /// Default quantum cull radius: 9.14m (30ft).
137    pub const DEFAULT_QUANTUM_CULL_RADIUS: f32 = 9.14;
138
139    /// Create a new simulation service.
140    pub fn new(scene_name: &str, mode: SimulationMode) -> Self {
141        Self {
142            scene: GameObjectScene::new(scene_name.into()),
143            kernel: None,
144            encoder: None,
145            bridge: None,
146            last_bridge_packet: None,
147            decoherence: None,
148            oracle: None,
149            decoherence_fields: Vec::new(),
150            last_result: None,
151            last_descriptor: None,
152            player_object_id: 0,
153            particle_offsets: Vec::new(),
154            observer: SuperpositionObserver::new([0.0; 3], Self::DEFAULT_QUANTUM_CULL_RADIUS),
155            journal: Vec::new(),
156            console: Vec::new(),
157            mode,
158            readiness: RuntimeReadiness::Pending,
159            frame: 0,
160            tick: 0,
161            quantum_cull_radius: Self::DEFAULT_QUANTUM_CULL_RADIUS,
162            elapsed_time: 0.0,
163            gate: None,
164            limiter: None,
165            budgeter: None,
166            ledger_rx: None,
167            causal_observer_rx: None,
168            causal_observer_tx: None,
169            active_zone_index: 9,
170            metaphor_profile: dreamwell_metaphors::MetaphorProfile::wave(),
171            active_particle_count: dreamwell_metaphors::DEFAULT_PARTICLE_COUNT as f32,
172            coherence_target: 1.0,
173            coherence_current: 1.0,
174            gather_active: false,
175            emit_strength: 0.0,
176            universe_snapshot_rx: None,
177            universe_update_tx: None,
178            universe_client_id: 0,
179            universe_connected: false,
180        }
181    }
182
183    /// Whether input is enabled. Requires encoder (authoritative pipeline).
184    pub fn input_enabled(&self) -> bool {
185        self.readiness == RuntimeReadiness::Ready && self.encoder.is_some()
186    }
187
188    /// Connect to a universe server (spawns background kernel thread).
189    /// Returns the shutdown sender so the caller can stop the universe.
190    pub fn connect_universe(&mut self) -> crossbeam_channel::Sender<()> {
191        let config = dreamwell_universe::UniverseConfig::default();
192        let (shutdown_tx, shutdown_rx) = crossbeam_channel::bounded(1);
193
194        // Create the field and connection protocol on the main thread,
195        // then move the kernel to a background thread.
196        let mut field = dreamwell_universe::QuantumField::new(&config);
197        let mut connections = dreamwell_universe::ConnectionProtocol::new(config.max_clients);
198
199        // Connect this client on layer 9 (Point — default play layer).
200        let (client_id, snapshot_rx, update_tx) = connections
201            .connect(&mut field, 9)
202            .expect("Failed to connect to universe");
203
204        self.universe_snapshot_rx = Some(snapshot_rx);
205        self.universe_update_tx = Some(update_tx);
206        self.universe_client_id = client_id;
207        self.universe_connected = true;
208
209        log::info!("Connected to universe on layer 9 (client_id={})", client_id);
210
211        // Spawn the universe kernel in a background thread.
212        let config_clone = config.clone();
213        std::thread::Builder::new()
214            .name("dreamwell-universe".into())
215            .spawn(move || {
216                let mut kernel = dreamwell_universe::UniverseKernel {
217                    config: config_clone.clone(),
218                    field,
219                    connections,
220                    metrics: dreamwell_universe::metrics::UniverseMetrics::new(),
221                    shutdown_rx,
222                };
223                // Expose the fields that were moved
224                kernel.run();
225            })
226            .expect("Failed to spawn universe thread");
227
228        shutdown_tx
229    }
230
231    /// Set form coherence target. 1.0=Cohere, 0.0=Wave.
232    pub fn set_coherence_target(&mut self, t: f32) {
233        self.coherence_target = t.clamp(0.0, 1.0);
234    }
235
236    /// Set gather mode.
237    pub fn set_gather(&mut self, active: bool) {
238        self.gather_active = active;
239    }
240
241    /// Set primary action emit strength.
242    pub fn set_emit_strength(&mut self, s: f32) {
243        self.emit_strength = s.clamp(0.0, 1.0);
244    }
245
246    /// Initialize the simulation from a scene and spawn position.
247    /// Runs the 7-step initialization sequence with CausalComputeKernel + SDK Decoherence.
248    ///
249    /// Returns Ok(()) if all steps pass, Err with failure description.
250    pub fn initialize(
251        &mut self,
252        scene: GameObjectScene,
253        spawn_pos: [f32; 3],
254        particle_count: u32,
255    ) -> Result<(), String> {
256        // Step 1: Mount Braid
257        self.scene = scene;
258        self.journal.push(SimulationJournalEntry {
259            frame: 0,
260            kind: "mount_braid".into(),
261            detail: format!("scene: {}, objects: {}", self.scene.name, self.scene.objects.len()),
262        });
263        self.console.push(format!(
264            "[Info] Braid mounted from Chronoshift checkpoint (tick: {}).",
265            self.tick,
266        ));
267
268        // Step 2: Reality check
269        if self.scene.objects.is_empty() {
270            self.readiness = RuntimeReadiness::Failed;
271            return Err("reality_check: empty scene".into());
272        }
273        self.console.push("[Info] Reality check: all phases passed.".into());
274
275        // Step 3: Scene instantiation
276        let obj_count = self.scene.objects.len();
277        self.console
278            .push(format!("[Info] Scene instantiated: {obj_count} objects."));
279
280        // Step 4: CausalComputeKernel + SuperBodyController + Encoder + Bridge + SDK Decoherence init
281        let kernel = CausalComputeKernel::new(spawn_pos, particle_count);
282        self.kernel = Some(kernel);
283        self.encoder = Some(CausalEngineEncoder::new(spawn_pos, particle_count, 42));
284        let mut bridge = QuantumBridge::new(particle_count);
285        bridge.set_metaphor_profile(self.metaphor_profile.clone());
286        self.bridge = Some(bridge);
287
288        let decoherence = Decoherence::with_config(DecoherenceConfig {
289            input_mode: DecoherenceInputMode::Particle,
290            ..DecoherenceConfig::default()
291        });
292        self.decoherence = Some(decoherence);
293
294        let mut oracle = Oracle::new(OracleConfig::default());
295        let (tx, rx) = ledger_lane_channel();
296        oracle.connect_ledger(tx);
297        oracle.begin_frame(0);
298        self.oracle = Some(oracle);
299        self.ledger_rx = Some(rx);
300
301        // CausalObserverLane channel for GPU cold path.
302        let (causal_observer_tx, causal_observer_rx) = causal_observer_lane_channel();
303        self.causal_observer_tx = Some(causal_observer_tx);
304        self.causal_observer_rx = Some(causal_observer_rx);
305
306        // DreamGate pipeline: Budgeter → Limiter → Gate.
307        let hw = dreamwell_sdk::loom_budgeter::HardwareProfile::mid_range();
308        let budgeter = LoomBudgeter::from_hardware(hw);
309        let mut limiter = LoomLimiter::default();
310        budgeter.apply_to_limiter(&mut limiter);
311        let gate = DreamGate::new(GateConfig::from_services(&limiter, &budgeter));
312        self.gate = Some(gate);
313        self.limiter = Some(limiter);
314        self.budgeter = Some(budgeter);
315
316        self.particle_offsets = particle_sphere_offsets(particle_count);
317
318        // Find player object ID and starting topology layer.
319        self.player_object_id = self
320            .scene
321            .objects
322            .iter()
323            .find(|o| {
324                o.property_tags
325                    .iter()
326                    .any(|t| t == "isInputReceiver" || t == "isPlayer")
327            })
328            .map(|o| o.id)
329            .unwrap_or(1);
330
331        let starting_layer = self
332            .scene
333            .objects
334            .iter()
335            .filter(|o| o.property_tags.iter().any(|t| t == "isInputReceiver"))
336            .flat_map(|o| o.property_tags.iter())
337            .find_map(|t| {
338                t.strip_prefix("isStartingOnTopologyLayer")
339                    .and_then(|v| v.parse::<u8>().ok())
340            })
341            .unwrap_or(9);
342        if let Some(ref mut k) = self.kernel {
343            k.active_topology_layer = starting_layer;
344        }
345        if let Some(ref mut enc) = self.encoder {
346            enc.kernel_mut().active_topology_layer = starting_layer;
347        }
348        self.active_zone_index = starting_layer as usize;
349
350        self.observer.position = spawn_pos;
351        self.observer.active_radius = self.quantum_cull_radius;
352        self.console.push(format!(
353            "[Info] CausalComputeKernel mounted at [{:.1}, {:.1}, {:.1}] ({particle_count} particles).",
354            spawn_pos[0], spawn_pos[1], spawn_pos[2],
355        ));
356        self.journal.push(SimulationJournalEntry {
357            frame: 0,
358            kind: "avatar_init".into(),
359            detail: format!("particles: {particle_count}, pos: {spawn_pos:?}, sdk: active"),
360        });
361
362        // Step 5: GPU sync (headless: no-op)
363        self.console.push("[Info] GPU resources synced.".into());
364
365        // Step 6: Stitch
366        if self.encoder.is_none() || self.scene.objects.is_empty() {
367            self.readiness = RuntimeReadiness::Failed;
368            return Err("stitch: render graph validation failed".into());
369        }
370        self.readiness = RuntimeReadiness::Syncing;
371        self.console
372            .push("[Info] Stitch passed: render graph validated.".into());
373
374        // Step 7: First frame confirmation
375        self.readiness = RuntimeReadiness::Ready;
376        self.console
377            .push("[Info] Seer is happy, reality rendered without concern.".into());
378        self.console
379            .push("[Info] Input controls attached to CausalComputeKernel.".into());
380        self.console.push(format!(
381            "[Info] Quantum pipeline: {} particles, density matrix 5×5, adaptive Hamiltonian (Model 1).",
382            particle_count,
383        ));
384        self.console
385            .push("[Info] Couplings will adapt to movement patterns via parameter shift gradient.".into());
386        log::info!(
387            "Quantum locomotion active: {} particles, F(12)={}, adaptive Hamiltonian learning enabled",
388            particle_count,
389            particle_count == 144,
390        );
391
392        // Step 7c: Connect to universe (persistent quantum field server).
393        // Spawns the CausalSimulationKernel in a background thread at φ-scaled 1618Hz.
394        let _universe_shutdown = self.connect_universe();
395        self.console
396            .push("[Info] Universe connected: persistent quantum field at 1618Hz (φ-scaled).".into());
397
398        // Step 7b: Dispatch session begin weave through SDK pipeline.
399        if let (Some(ref decoherence), Some(ref mut gate)) = (&self.decoherence, &mut self.gate) {
400            let weave = decoherence.build_session_begin_weave(0);
401            gate.submit(weave, 100);
402        }
403
404        Ok(())
405    }
406
407    /// Process one simulation tick via single-encoder authority pipeline.
408    ///
409    /// Round trip (THE_BRAIDED_PATH):
410    /// (1) Build Input Weave → submit to DreamGate
411    /// (2) Encoder tick (single authority: Encoder → SuperBody → Kernel → AttentionSolver)
412    /// (3) Build Observation + Locomotion Weaves → submit to DreamGate
413    /// (4) Gate dispatch: spindle → SacredPath → scene
414    /// (5) Bridge: encode → GPU-ready BridgePacket (DreamMemory + SacredSeal)
415    ///
416    /// Returns the KernelResult, or None if input is not enabled.
417    pub fn tick(&mut self, packet: &InputPacket) -> Option<KernelResult> {
418        if !self.input_enabled() {
419            return None;
420        }
421
422        self.frame += 1;
423        self.tick += 1;
424        self.elapsed_time += packet.dt;
425
426        // Exponential coherence lerp toward target. Rate 6.0 ≈ 0.4s to 95% settle.
427        // Frame-rate independent: identical at 30/60/144fps.
428        let coherence_rate = 6.0_f32;
429        let coherence_t = 1.0 - (-coherence_rate * packet.dt).exp();
430        self.coherence_current += (self.coherence_target - self.coherence_current) * coherence_t;
431
432        let gate = self.gate.as_mut()?;
433        let tick = self.tick;
434
435        gate.begin_frame(tick);
436        if let Some(ref mut limiter) = self.limiter {
437            limiter.begin_frame(tick);
438        }
439
440        // (1) Build Input Weave → submit to DreamGate
441        if let Some(ref mut decoherence) = self.decoherence {
442            let actions = packet.actions_as_u8();
443            let movement = packet.movement;
444            let look = [packet.camera_yaw, 0.0];
445            let input_weave = decoherence.build_input_weave(tick, actions, movement, look, 0.0);
446            gate.submit(input_weave, 100);
447        }
448
449        // (2) SINGLE encoder tick — authoritative quantum evolution.
450        // Patch authoritative state onto packet before encoding:
451        //   grounded:         from encoder kernel (previous frame's physics result)
452        //   timestamp:        from elapsed_time (just incremented above)
453        //   coherence_target: from coherence_current (just lerped above — NOT the raw target)
454        let encoder = self.encoder.as_mut()?;
455        let mut enc_packet = packet.clone();
456        enc_packet.grounded = encoder.kernel().grounded;
457        enc_packet.timestamp = self.elapsed_time as f64;
458        enc_packet.coherence_target = self.coherence_current;
459        let encode_result = encoder.encode_frame(&enc_packet, &self.decoherence_fields);
460
461        // ── Model 1: Adaptive Hamiltonian (online gradient learning) ──
462        // Every other frame, compute parameter shift gradient of free energy w.r.t.
463        // Hamiltonian couplings and update them. Adapts locomotion feel to the
464        // player's movement patterns in real time. Cost: ~14µs (0.085% budget).
465        if self.frame % 2 == 0 {
466            let kernel = encoder.kernel_mut();
467            let bias = kernel.quantum.hamiltonian.bias;
468            let couplings = kernel.quantum.hamiltonian.couplings;
469            // Reconstruct input bias from kernel movement state.
470            let moving = kernel.velocity[0].abs() > 0.01 || kernel.velocity[2].abs() > 0.01;
471            let sprinting = enc_packet.sprint && moving;
472            let input_bias = [
473                if !moving { 2.0 } else { 0.0 },
474                if moving && !sprinting { 2.0 } else { 0.0 },
475                if enc_packet.jump { 3.0 } else { 0.0 },
476                if kernel.landing_timer > 0.0 { 2.0 } else { 0.0 },
477                if sprinting { 2.0 } else { 0.0 },
478            ];
479            let shift = std::f32::consts::FRAC_PI_2;
480            let lr = 0.005_f32;
481
482            let mut new_couplings = couplings;
483            for k in 0..5 {
484                let mut c_plus = couplings;
485                c_plus[k] = (c_plus[k] + shift).min(2.0);
486                let f_plus = kernel.quantum.rho.free_energy(&bias, &input_bias, &c_plus);
487
488                let mut c_minus = couplings;
489                c_minus[k] = (c_minus[k] - shift).max(0.1);
490                let f_minus = kernel.quantum.rho.free_energy(&bias, &input_bias, &c_minus);
491
492                let grad = (f_plus - f_minus) / 2.0;
493                // Descend — minimize free energy for stability (not maximize).
494                new_couplings[k] = (new_couplings[k] - lr * grad).clamp(0.1, 2.0);
495            }
496            kernel.quantum.hamiltonian.couplings = new_couplings;
497
498            if self.frame % 120 == 0 {
499                log::info!(
500                    "Adaptive Hamiltonian: couplings=[{:.3}, {:.3}, {:.3}, {:.3}, {:.3}] F={:.4}",
501                    new_couplings[0],
502                    new_couplings[1],
503                    new_couplings[2],
504                    new_couplings[3],
505                    new_couplings[4],
506                    kernel.quantum.rho.free_energy(&bias, &input_bias, &new_couplings),
507                );
508            }
509        }
510
511        // Extract authoritative result from encoder (single source of truth).
512        // (Universe coupling happens below, after mutable encoder borrow is released.)
513        let pos = encode_result.kernel_result.position;
514
515        // Sync standalone kernel for collision/POI checks.
516        if let Some(ref mut k) = self.kernel {
517            k.position = pos;
518            k.velocity = encoder.kernel().velocity;
519            k.grounded = encoder.kernel().grounded;
520            k.facing_yaw = encoder.kernel().facing_yaw;
521        }
522
523        // (3) Observation + Locomotion Weaves → submit to DreamGate
524        if let Some(ref mut decoherence) = self.decoherence {
525            let facing_yaw = encoder.kernel().facing_yaw;
526            let obs_weave = decoherence.build_observe_weave(tick, pos, facing_yaw);
527            gate.submit(obs_weave, 100);
528
529            // Locomotion Weave if mode changed
530            if let Some(ref prev) = self.last_result {
531                if encode_result.kernel_result.locomotion_mode != prev.locomotion_mode {
532                    let loco_state = match encode_result.kernel_result.locomotion_mode {
533                        0 => dreamwell_sdk::decohere::LocomotionState::Idle,
534                        1 => dreamwell_sdk::decohere::LocomotionState::Walking,
535                        2 => dreamwell_sdk::decohere::LocomotionState::Running,
536                        3 => dreamwell_sdk::decohere::LocomotionState::Jumping,
537                        4 => dreamwell_sdk::decohere::LocomotionState::Falling,
538                        _ => dreamwell_sdk::decohere::LocomotionState::Idle,
539                    };
540                    let loco_weave = decoherence.build_locomotion_weave(tick, loco_state);
541                    gate.submit(loco_weave, 90);
542                }
543            }
544
545            // Topology transition if layer changed
546            if encode_result.kernel_result.layer_changed {
547                self.active_zone_index = encode_result.kernel_result.topology_layer as usize;
548            }
549        }
550
551        // (4) Gate dispatch: spindle → SacredPath → scene
552        if let Some(ref mut oracle) = self.oracle {
553            let (_accepted, _rejected, _mutations) = gate.dispatch_through_oracle(oracle, &mut self.scene);
554            oracle.end_frame();
555            oracle.begin_frame(self.tick);
556        }
557
558        // (4b) Drain GPU causal observer lane → forward to Oracle's ledger for QuantumCloud archival.
559        if let Some(ref causal_observer_rx) = self.causal_observer_rx {
560            let gpu_events = causal_observer_rx.drain();
561            if !gpu_events.is_empty() {
562                log::trace!("causal_observer_lane: drained {} GPU frame events", gpu_events.len());
563            }
564        }
565
566        // (5) Bridge: encode → GPU-ready packets (validates + seals + captures to DreamMemory).
567        // Evaluate TagInfluenceFrame from scene POIs before bridging.
568        let tag_frame = self.evaluate_tag_influences(pos);
569        if let Some(ref mut bridge) = self.bridge {
570            bridge.set_tag_frame(tag_frame);
571            let (bp, _validation) = bridge.bridge(&encode_result, packet.dt, self.elapsed_time);
572            self.last_bridge_packet = Some(bp);
573        }
574
575        // (5b) Particle siphon + regeneration based on POI proximity.
576        self.update_particle_siphon(pos, packet.dt);
577
578        // Update observer + store lightweight snapshot for next-frame locomotion comparison.
579        // Only the locomotion_mode is consumed (line 381). Avoid cloning String/Vec per frame.
580        self.observer.position = pos;
581        let result = encode_result.kernel_result;
582
583        self.last_result = Some(KernelResult {
584            position: result.position,
585            locomotion_mode: result.locomotion_mode,
586            form: result.form,
587            coherence: result.coherence,
588            populations: result.populations,
589            entropy: result.entropy,
590            purity: result.purity,
591            active_clip: String::new(),
592            moved: result.moved,
593            coalesce_events: Vec::new(),
594            topology_layer: result.topology_layer,
595            layer_changed: false,
596        });
597
598        // ── Universe coupling (Blaed's Algorithm: client ↔ server field) ──
599        if self.universe_connected {
600            // Send our state to the universe server.
601            if let Some(ref tx) = self.universe_update_tx {
602                if let Some(ref enc) = self.encoder {
603                    let k = enc.kernel();
604                    let update = dreamwell_universe::ClientUpdate {
605                        client_id: self.universe_client_id,
606                        populations: k.quantum.rho.populations(),
607                        coherences: k.quantum.rho.off_diagonal_pairs(),
608                        position: k.position,
609                        layer: self.active_zone_index as u8,
610                        frame: self.frame,
611                    };
612                    let _ = tx.try_send(update);
613                }
614            }
615            // Receive field snapshot and apply coupling.
616            if let Some(ref rx) = self.universe_snapshot_rx {
617                if let Ok(snapshot) = rx.try_recv() {
618                    if let Some(ref mut enc) = self.encoder {
619                        let layer = self.active_zone_index;
620                        let channel = &snapshot.channels[layer];
621                        let coupling_eps = 0.03 * packet.dt;
622                        let field_coh = channel.coherence_magnitude;
623                        let rho = &mut enc.kernel_mut().quantum.rho;
624                        for i in 0..5 {
625                            for j in 0..5 {
626                                if i != j {
627                                    let retain = 1.0 - coupling_eps * (1.0 - field_coh);
628                                    rho.entries[i * 5 + j] = rho.entries[i * 5 + j].scale(retain.max(0.0));
629                                }
630                            }
631                        }
632                        if self.frame % 300 == 0 {
633                            log::info!(
634                                "Universe sync: tick={} field_coh={:.4} field_F={:.4}",
635                                snapshot.tick,
636                                field_coh,
637                                channel.free_energy,
638                            );
639                        }
640                    }
641                }
642            }
643        }
644
645        Some(result)
646    }
647
648    /// Current authoritative position: encoder first, kernel fallback.
649    fn observer_position(&self) -> Option<[f32; 3]> {
650        self.encoder
651            .as_ref()
652            .map(|e| e.kernel().position)
653            .or_else(|| self.kernel.as_ref().map(|k| k.position))
654    }
655
656    /// Check for collisions with isWall/isCollider objects within threshold.
657    pub fn check_collisions(&self, threshold: f32) -> Vec<(u64, f32)> {
658        let pos = match self.observer_position() {
659            Some(p) => p,
660            None => return Vec::new(),
661        };
662
663        self.scene
664            .objects
665            .iter()
666            .filter_map(|o| {
667                let is_collider = o.property_tags.iter().any(|t| t == "isWall" || t == "isCollider");
668                if !is_collider {
669                    return None;
670                }
671                let dx = o.transform.position[0] - pos[0];
672                let dy = o.transform.position[1] - pos[1];
673                let dz = o.transform.position[2] - pos[2];
674                let dist = (dx * dx + dy * dy + dz * dz).sqrt();
675                if dist <= threshold {
676                    Some((o.id, dist))
677                } else {
678                    None
679                }
680            })
681            .collect()
682    }
683
684    /// Check POI proximity. Returns Vec of (object_id, distance).
685    pub fn check_poi_proximity(&self, radius: f32) -> Vec<(u64, f32)> {
686        let pos = match self.observer_position() {
687            Some(p) => p,
688            None => return Vec::new(),
689        };
690
691        self.scene
692            .objects
693            .iter()
694            .filter_map(|o| {
695                let is_poi = o.property_tags.iter().any(|t| t == "poi" || t == "isInteractable");
696                if !is_poi {
697                    return None;
698                }
699                let dx = o.transform.position[0] - pos[0];
700                let dy = o.transform.position[1] - pos[1];
701                let dz = o.transform.position[2] - pos[2];
702                let dist = (dx * dx + dy * dy + dz * dz).sqrt();
703                if dist <= radius {
704                    Some((o.id, dist))
705                } else {
706                    None
707                }
708            })
709            .collect()
710    }
711
712    /// Check POI proximity and dispatch interaction weaves for nearby POIs.
713    pub fn tick_poi_interactions(&mut self) {
714        let pois = self.check_poi_proximity(5.0);
715        if pois.is_empty() {
716            return;
717        }
718
719        let gate = match &mut self.gate {
720            Some(g) => g,
721            None => return,
722        };
723        let decoherence = match &self.decoherence {
724            Some(d) => d,
725            None => return,
726        };
727        let tick = self.tick;
728
729        for (poi_id, dist) in &pois {
730            if *dist < 2.0 {
731                let weave = decoherence.build_interaction_weave(tick, *poi_id);
732                gate.submit(weave, 80);
733            }
734        }
735    }
736
737    /// Apply collision pushback from wall/collider objects.
738    pub fn tick_collisions(&mut self) {
739        let collisions = self.check_collisions(2.0);
740        if collisions.is_empty() {
741            return;
742        }
743
744        // Try encoder kernel first, fall back to direct kernel.
745        let kernel = if let Some(ref mut enc) = self.encoder {
746            enc.kernel_mut()
747        } else if let Some(ref mut k) = self.kernel {
748            k
749        } else {
750            return;
751        };
752        for (obj_id, dist) in &collisions {
753            if let Some(obj) = self.scene.find(*obj_id) {
754                if *dist < 0.01 {
755                    continue;
756                }
757                let push_str = (1.0 - dist / 2.0).max(0.0) * 0.5;
758                let dx = kernel.position[0] - obj.transform.position[0];
759                let dz = kernel.position[2] - obj.transform.position[2];
760                let len = (dx * dx + dz * dz).sqrt().max(0.001);
761                kernel.position[0] += (dx / len) * push_str;
762                kernel.position[2] += (dz / len) * push_str;
763            }
764        }
765    }
766
767    /// Evaluate TagInfluenceFrame from scene POI objects near the observer.
768    /// Scans for `isPointOfInterest` tagged objects within quantum_cull_radius.
769    fn evaluate_tag_influences(&self, pos: [f32; 3]) -> dreamwell_metaphors::TagInfluenceFrame {
770        let mut frame = dreamwell_metaphors::TagInfluenceFrame::empty();
771        let aoi_radius = self.quantum_cull_radius;
772
773        for obj in &self.scene.objects {
774            let is_poi = obj
775                .property_tags
776                .iter()
777                .any(|t| t == "isPointOfInterest" || t == "poi" || t == "isInteractable");
778            if !is_poi {
779                continue;
780            }
781
782            let dx = obj.transform.position[0] - pos[0];
783            let dy = obj.transform.position[1] - pos[1];
784            let dz = obj.transform.position[2] - pos[2];
785            let dist = (dx * dx + dy * dy + dz * dz).sqrt();
786            if dist > aoi_radius {
787                continue;
788            }
789
790            // Coupling strength: inverse-distance falloff, 1.0 at contact, 0.0 at radius edge.
791            let coupling = (1.0 - dist / aoi_radius).max(0.0);
792            let siphon_eligible = obj.property_tags.iter().any(|t| t == "isSiphon" || t == "isFractal");
793
794            frame.poi_couplings.push(dreamwell_metaphors::PoiCoupling {
795                poi_id: obj.id,
796                distance: dist,
797                coupling,
798                siphon_eligible,
799            });
800        }
801
802        // Zone biases from topology transition tags.
803        for obj in &self.scene.objects {
804            let is_zone = obj
805                .property_tags
806                .iter()
807                .any(|t| t.starts_with("isTopologyTransition") || t == "isDecoherenceZone");
808            if !is_zone {
809                continue;
810            }
811
812            let dx = obj.transform.position[0] - pos[0];
813            let dz = obj.transform.position[2] - pos[2];
814            let dist = (dx * dx + dz * dz).sqrt();
815            if dist > aoi_radius {
816                continue;
817            }
818
819            let proximity = (1.0 - dist / aoi_radius).max(0.0);
820            frame.zone_biases.push(dreamwell_metaphors::ZoneBias {
821                zone_id: obj.id,
822                morph_bias: proximity * 0.3,
823                coherence_bias: -proximity * 0.2, // zones reduce coherence
824            });
825        }
826
827        frame
828    }
829
830    /// Update particle siphon + regeneration based on POI proximity.
831    /// Siphon: nearby siphon-eligible POIs reduce active_particle_count.
832    /// Regeneration: particle count slowly restores to default when not siphoned.
833    fn update_particle_siphon(&mut self, pos: [f32; 3], dt: f32) {
834        let default_count = dreamwell_metaphors::DEFAULT_PARTICLE_COUNT as f32;
835        let siphon_rate = 30.0; // particles per second per coupling unit
836        let regen_rate = 10.0; // particles per second when below default
837
838        let mut total_siphon = 0.0f32;
839        for obj in &self.scene.objects {
840            let is_siphon = obj.property_tags.iter().any(|t| t == "isSiphon" || t == "isFractal");
841            if !is_siphon {
842                continue;
843            }
844
845            let dx = obj.transform.position[0] - pos[0];
846            let dy = obj.transform.position[1] - pos[1];
847            let dz = obj.transform.position[2] - pos[2];
848            let dist = (dx * dx + dy * dy + dz * dz).sqrt();
849            let coupling = (1.0 - dist / self.quantum_cull_radius).max(0.0);
850            if coupling > 0.1 {
851                total_siphon += coupling;
852            }
853        }
854
855        if total_siphon > 0.0 {
856            self.active_particle_count -= siphon_rate * total_siphon * dt;
857        } else if self.active_particle_count < default_count {
858            self.active_particle_count += regen_rate * dt;
859        }
860
861        // Clamp to valid range.
862        self.active_particle_count = self.active_particle_count.clamp(16.0, default_count);
863    }
864
865    /// Set the active metaphor profile and propagate to the bridge evaluator.
866    pub fn set_metaphor_profile(&mut self, profile: dreamwell_metaphors::MetaphorProfile) {
867        self.metaphor_profile = profile.clone();
868        if let Some(ref mut bridge) = self.bridge {
869            bridge.set_metaphor_profile(profile);
870        }
871    }
872
873    /// Cycle to the next metaphor profile (Wave → Fibonacci → Stream → Wave).
874    /// Returns the name of the newly active profile.
875    pub fn cycle_metaphor(&mut self) -> &'static str {
876        let new = dreamwell_metaphors::cycle_next(self.metaphor_profile.name);
877        let name = new.name;
878        self.set_metaphor_profile(new);
879        name
880    }
881
882    /// End the simulation session.
883    pub fn end_session(&mut self) {
884        // Dispatch session end weave before teardown.
885        if let (Some(ref decoherence), Some(ref mut oracle), Some(ref mut gate)) =
886            (&self.decoherence, &mut self.oracle, &mut self.gate)
887        {
888            let weave = decoherence.build_session_end_weave(self.tick);
889            gate.submit(weave, 100);
890            let _ = gate.dispatch_through_oracle(oracle, &mut self.scene);
891        }
892
893        self.readiness = RuntimeReadiness::Pending;
894        self.kernel = None;
895        self.encoder = None;
896        self.bridge = None;
897        self.last_bridge_packet = None;
898        self.decoherence = None;
899        self.oracle = None;
900        self.causal_observer_rx = None;
901        self.causal_observer_tx = None;
902        self.decoherence_fields.clear();
903        self.last_result = None;
904        self.last_descriptor = None;
905        self.console.push("[Info] Client closing.. Chronoshift success.".into());
906        self.console
907            .push("[Info] Reality checked and loaded. Editor session restored.".into());
908        self.journal.push(SimulationJournalEntry {
909            frame: self.frame,
910            kind: "end_session".into(),
911            detail: "play".into(),
912        });
913    }
914}
915
916// ── Tests ─────────────────────────────────────────────────────────────
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921
922    fn test_scene() -> GameObjectScene {
923        let mut s = GameObjectScene::new("test".into());
924        s.spawn("Avatar Start".into()).unwrap();
925        s.objects[0].property_tags.push("isPlayer".into());
926        s.objects[0].property_tags.push("isInputReceiver".into());
927        s.objects[0].transform.position = [0.0, 0.5, 0.0];
928        s.spawn("Ground".into()).unwrap();
929        s.objects[1].property_tags.push("isStatic".into());
930        s.objects[1].property_tags.push("isGround".into());
931        s
932    }
933
934    #[test]
935    fn simulation_new() {
936        let sim = SimulationService::new("test", SimulationMode::Preview);
937        assert_eq!(sim.readiness, RuntimeReadiness::Pending);
938        assert!(!sim.input_enabled());
939    }
940
941    #[test]
942    fn simulation_initialize_7_steps() {
943        let mut sim = SimulationService::new("test", SimulationMode::Preview);
944        let scene = test_scene();
945        let result = sim.initialize(scene, [0.0, 0.5, 0.0], 128);
946        assert!(result.is_ok(), "init should succeed: {:?}", result.err());
947        assert_eq!(sim.readiness, RuntimeReadiness::Ready);
948        assert!(sim.input_enabled());
949        assert!(sim.kernel.is_some());
950        assert!(sim.encoder.is_some());
951        assert!(sim.bridge.is_some());
952        assert!(sim.decoherence.is_some());
953        assert!(sim.oracle.is_some());
954        assert!(sim.console.iter().any(|m| m.contains("Seer is happy")));
955        assert!(sim.console.iter().any(|m| m.contains("CausalComputeKernel")));
956    }
957
958    #[test]
959    fn simulation_input_gated_before_ready() {
960        let mut sim = SimulationService::new("test", SimulationMode::Preview);
961        let packet = InputPacket {
962            movement: [0.0, 1.0],
963            ..InputPacket::idle(1.0 / 60.0, 0)
964        };
965        let result = sim.tick(&packet);
966        assert!(result.is_none(), "input should be gated before init");
967    }
968
969    #[test]
970    fn simulation_tick_moves_player() {
971        let mut sim = SimulationService::new("test", SimulationMode::Preview);
972        sim.initialize(test_scene(), [0.0, 0.5, 0.0], 128).unwrap();
973        let packet = InputPacket {
974            movement: [0.0, 1.0],
975            ..InputPacket::idle(1.0 / 60.0, 1)
976        };
977        let result = sim.tick(&packet);
978        assert!(result.is_some());
979        let r = result.unwrap();
980        assert!(r.position != [0.0, 0.5, 0.0], "player should move after forward input");
981    }
982
983    #[test]
984    fn simulation_collision_detection() {
985        let mut sim = SimulationService::new("test", SimulationMode::Preview);
986        let mut scene = test_scene();
987        scene.spawn("Wall".into()).unwrap();
988        let wall_idx = scene.objects.len() - 1;
989        scene.objects[wall_idx].property_tags.push("isWall".into());
990        scene.objects[wall_idx].property_tags.push("isCollider".into());
991        scene.objects[wall_idx].transform.position = [0.5, 0.5, 0.0];
992        sim.initialize(scene, [0.0, 0.5, 0.0], 128).unwrap();
993
994        let collisions = sim.check_collisions(1.0);
995        assert!(!collisions.is_empty());
996    }
997
998    #[test]
999    fn simulation_poi_proximity() {
1000        let mut sim = SimulationService::new("test", SimulationMode::Preview);
1001        let mut scene = test_scene();
1002        scene.spawn("POI".into()).unwrap();
1003        let poi_idx = scene.objects.len() - 1;
1004        scene.objects[poi_idx].property_tags.push("isInteractable".into());
1005        scene.objects[poi_idx].transform.position = [0.0, 0.5, -1.0];
1006        sim.initialize(scene, [0.0, 0.5, 0.0], 128).unwrap();
1007
1008        let pois = sim.check_poi_proximity(2.0);
1009        assert!(!pois.is_empty());
1010    }
1011
1012    #[test]
1013    fn simulation_end_session() {
1014        let mut sim = SimulationService::new("test", SimulationMode::Preview);
1015        sim.initialize(test_scene(), [0.0, 0.5, 0.0], 128).unwrap();
1016        sim.end_session();
1017        assert_eq!(sim.readiness, RuntimeReadiness::Pending);
1018        assert!(!sim.input_enabled());
1019        assert!(sim.kernel.is_none());
1020        assert!(sim.encoder.is_none());
1021        assert!(sim.bridge.is_none());
1022        assert!(sim.decoherence.is_none());
1023        assert!(sim.oracle.is_none());
1024        assert!(sim.console.iter().any(|m| m.contains("Chronoshift success")));
1025    }
1026
1027    #[test]
1028    fn simulation_empty_scene_fails() {
1029        let mut sim = SimulationService::new("test", SimulationMode::Preview);
1030        let scene = GameObjectScene::new("empty".into());
1031        let result = sim.initialize(scene, [0.0; 3], 128);
1032        assert!(result.is_err());
1033        assert_eq!(sim.readiness, RuntimeReadiness::Failed);
1034    }
1035
1036    #[test]
1037    fn simulation_mode_variants() {
1038        assert_ne!(SimulationMode::Preview, SimulationMode::Published);
1039    }
1040
1041    #[test]
1042    fn cycle_metaphor_changes_profile() {
1043        let mut sim = SimulationService::new("test", SimulationMode::Preview);
1044        sim.initialize(test_scene(), [0.0, 0.5, 0.0], 128).unwrap();
1045        assert_eq!(sim.metaphor_profile.name, "wave");
1046        let name = sim.cycle_metaphor();
1047        assert_eq!(name, "keynote_fibonacci");
1048        assert_eq!(sim.metaphor_profile.name, "keynote_fibonacci");
1049        let name = sim.cycle_metaphor();
1050        assert_eq!(name, "cohered_stream");
1051        let name = sim.cycle_metaphor();
1052        assert_eq!(name, "wave");
1053    }
1054
1055    #[test]
1056    fn default_profile_is_wave() {
1057        let sim = SimulationService::new("test", SimulationMode::Preview);
1058        assert_eq!(sim.metaphor_profile.name, "wave");
1059    }
1060
1061    #[test]
1062    fn simulation_quantum_cull_radius() {
1063        let sim = SimulationService::new("test", SimulationMode::Preview);
1064        assert!((sim.quantum_cull_radius - SimulationService::DEFAULT_QUANTUM_CULL_RADIUS).abs() < 0.01);
1065    }
1066
1067    #[test]
1068    fn simulation_observer_position_updates() {
1069        let mut sim = SimulationService::new("test", SimulationMode::Preview);
1070        sim.initialize(test_scene(), [0.0, 0.5, 0.0], 128).unwrap();
1071        let packet = InputPacket {
1072            movement: [0.0, 1.0],
1073            ..InputPacket::idle(1.0, 1)
1074        };
1075        sim.tick(&packet);
1076        assert!(
1077            sim.observer.position != [0.0, 0.5, 0.0],
1078            "observer should follow kernel position"
1079        );
1080    }
1081
1082    #[test]
1083    fn simulation_journal_records_lifecycle() {
1084        let mut sim = SimulationService::new("test", SimulationMode::Preview);
1085        sim.initialize(test_scene(), [0.0, 0.5, 0.0], 128).unwrap();
1086        assert!(sim.journal.iter().any(|e| e.kind == "mount_braid"));
1087        assert!(sim.journal.iter().any(|e| e.kind == "avatar_init"));
1088        sim.end_session();
1089        assert!(sim.journal.iter().any(|e| e.kind == "end_session"));
1090    }
1091}