Skip to main content

dreamwell_runtime/
renderer.rs

1// Scene renderer — draws the game scene to the window surface.
2// Delegates GPU pipeline orchestration to DreamFabric.
3// Render graph: fabric.begin_frame → extract → prepare → queue → fabric.end_frame → present.
4//
5// CODESPEC: No per-frame allocation. Buffers pre-allocated and reused.
6// Pipeline creation is lazy (first frame only) with warmup precompilation at startup.
7// Surface errors handled without panic (reconfigure on Lost/Outdated).
8// GPU timestamps (TIMESTAMP_QUERY) provide gpu_ms when adapter supports them.
9
10use crate::scene::Scene;
11use crate::window::WindowState;
12use dreamwell_engine::material::SceneDreamMode;
13use dreamwell_engine::TopologyLayer;
14use dreamwell_fabric::entanglement::CausalEntanglement;
15use dreamwell_fabric::DreamFabric;
16use dreamwell_fabric::{CausalObserverLaneSender, CausalObserverRecorder, FrameGate, FrameMemory, FrameSeal};
17use dreamwell_gpu::formats::DEPTH_FORMAT;
18use dreamwell_gpu::post::PostProcessConfig;
19
20/// Frame health record — populated after present(), read by app.rs next frame.
21#[derive(Clone, Debug)]
22pub struct FrameContract {
23    pub frame: u64,
24    pub cpu_ms: f32,
25    pub gpu_ms: f32,
26    pub budget_ms: f32,
27    pub budget_exceeded: bool,
28    pub quality_tier: u32,
29    pub particle_count: u32,
30    pub post_process_enabled: bool,
31    pub frame_skipped: bool,
32    pub seal_digest: [u8; 32],
33    pub chain_depth: u64,
34    pub presented: bool,
35}
36
37impl Default for FrameContract {
38    fn default() -> Self {
39        Self {
40            frame: 0,
41            cpu_ms: 0.0,
42            gpu_ms: 0.0,
43            budget_ms: 16.67,
44            budget_exceeded: false,
45            quality_tier: 0,
46            particle_count: 0,
47            post_process_enabled: true,
48            frame_skipped: false,
49            seal_digest: [0u8; 32],
50            chain_depth: 0,
51            presented: true,
52        }
53    }
54}
55
56/// Scene renderer for the runtime. Owns DreamFabric and depth buffer.
57///
58/// Memory safety:
59/// - `depth_texture` / `depth_view` are recreated on resize (old dropped first).
60/// - `fabric` owns GPU scene, observer, meshlet, cull, and particle resources.
61/// - `fabric.matter` is the single owner of all particle systems.
62/// - No Arc/Mutex — single-owner, no thread sharing.
63pub struct SceneRenderer {
64    width: u32,
65    height: u32,
66    depth_texture: Option<wgpu::Texture>,
67    depth_view: Option<wgpu::TextureView>,
68    /// MSAA color resolve texture (created when sample_count > 1).
69    /// Allocation deferred until fabric gains MSAA support.
70    msaa_texture: Option<wgpu::Texture>,
71    msaa_view: Option<wgpu::TextureView>,
72    /// MSAA sample count (1 = disabled, 4 = recommended).
73    msaa_samples: u32,
74    pub fabric: Box<DreamFabric>,
75    total_time: f32,
76    /// CPU frame time in milliseconds from the previous frame (dt * 1000).
77    last_cpu_ms: f32,
78    /// GPU timestamp query set (2 timestamps: start/end). None if adapter lacks TIMESTAMP_QUERY.
79    query_set: Option<wgpu::QuerySet>,
80    /// GPU timestamp resolve buffer (16 bytes = 2 x u64).
81    resolve_buf: Option<wgpu::Buffer>,
82    /// GPU timestamp readback buffer (16 bytes, MAP_READ). Read 1 frame behind.
83    readback_buf: Option<wgpu::Buffer>,
84    /// Last GPU frame time in milliseconds (1-frame lag from readback).
85    last_gpu_ms: f32,
86    /// GPU timestamp period (nanoseconds per tick). 0.0 if timestamps unavailable.
87    timestamp_period: f32,
88    /// FrameGate — GPU budget enforcement and quality scaling.
89    frame_gate: FrameGate,
90    /// FrameSeal — BLAKE3 chain attestation for GPU frames.
91    frame_seal: FrameSeal,
92    /// FrameMemory — 120-frame ring buffer of sealed frame records.
93    frame_memory: FrameMemory,
94    /// CausalObserverRecorder — KPI timing for GPU causal observer hot path.
95    causal_observer_recorder: CausalObserverRecorder,
96    /// CausalObserverLane sender — cold path to QuantumCloud (set by SimulationService).
97    causal_observer_tx: Option<CausalObserverLaneSender>,
98    /// Last bridge seal digest (from decoder).
99    last_bridge_seal: [u8; 32],
100    /// Last frame contract — public API for app layer.
101    pub last_frame_contract: FrameContract,
102    /// Frame counter for seal chain.
103    frame_count: u64,
104}
105
106impl SceneRenderer {
107    pub fn new(ws: &WindowState) -> Self {
108        Self::with_msaa(ws, 1)
109    }
110
111    /// Create renderer with explicit MSAA sample count.
112    pub fn with_msaa(ws: &WindowState, msaa_samples: u32) -> Self {
113        let samples = match msaa_samples {
114            1 | 4 => msaa_samples,
115            _ => {
116                log::warn!("msaa_samples={msaa_samples} not supported, falling back to 1");
117                1
118            }
119        };
120        let fabric = Box::new(DreamFabric::new(&ws.device, ws.surface_config.format, false));
121
122        // GPU timestamp query setup (adapter-dependent).
123        // wgpu 27+ split TIMESTAMP_QUERY into two features:
124        // TIMESTAMP_QUERY (for resolve) and TIMESTAMP_QUERY_INSIDE_ENCODERS (for write_timestamp).
125        // We need both to use encoder.write_timestamp() + resolve.
126        let has_timestamps = ws.device.features().contains(wgpu::Features::TIMESTAMP_QUERY)
127            && ws
128                .device
129                .features()
130                .contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS);
131        let (query_set, resolve_buf, readback_buf, timestamp_period) = if has_timestamps {
132            let qs = ws.device.create_query_set(&wgpu::QuerySetDescriptor {
133                label: Some("gpu_timestamp_queries"),
134                ty: wgpu::QueryType::Timestamp,
135                count: 2,
136            });
137            let resolve = ws.device.create_buffer(&wgpu::BufferDescriptor {
138                label: Some("gpu_timestamp_resolve"),
139                size: 16, // 2 x u64
140                usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
141                mapped_at_creation: false,
142            });
143            let readback = ws.device.create_buffer(&wgpu::BufferDescriptor {
144                label: Some("gpu_timestamp_readback"),
145                size: 16,
146                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
147                mapped_at_creation: false,
148            });
149            let period = ws.queue.get_timestamp_period();
150            log::info!("GPU timestamps enabled (period={period:.2}ns)");
151            (Some(qs), Some(resolve), Some(readback), period)
152        } else {
153            log::info!("GPU timestamps unavailable (adapter lacks TIMESTAMP_QUERY)");
154            (None, None, None, 0.0)
155        };
156
157        let mut renderer = Self {
158            width: ws.surface_config.width,
159            height: ws.surface_config.height,
160            depth_texture: None,
161            depth_view: None,
162            msaa_texture: None,
163            msaa_view: None,
164            msaa_samples: samples,
165            fabric,
166            total_time: 0.0,
167            last_cpu_ms: 0.0,
168            query_set,
169            resolve_buf,
170            readback_buf,
171            last_gpu_ms: 0.0,
172            timestamp_period,
173            frame_gate: FrameGate::default(),
174            frame_seal: FrameSeal::new(),
175            frame_memory: FrameMemory::default(),
176            causal_observer_recorder: CausalObserverRecorder::default(),
177            causal_observer_tx: None,
178            last_bridge_seal: [0u8; 32],
179            last_frame_contract: FrameContract::default(),
180            frame_count: 0,
181        };
182        renderer.create_depth_texture(&ws.device);
183        renderer
184            .fabric
185            .resize(&ws.device, ws.surface_config.width, ws.surface_config.height);
186        renderer
187    }
188
189    pub fn resize(&mut self, width: u32, height: u32, device: &wgpu::Device) {
190        if width == 0 || height == 0 {
191            return;
192        }
193        if self.width == width && self.height == height {
194            return; // No-op if same size.
195        }
196        self.width = width;
197        self.height = height;
198        self.create_depth_texture(device);
199        self.fabric.resize(device, width, height);
200    }
201
202    /// Create/recreate depth and MSAA textures. Old resources dropped before allocation.
203    fn create_depth_texture(&mut self, device: &wgpu::Device) {
204        if self.width == 0 || self.height == 0 {
205            return;
206        }
207        // Drop old resources first to free GPU memory before allocating new.
208        self.depth_view = None;
209        self.depth_texture = None;
210        self.msaa_view = None;
211        self.msaa_texture = None;
212
213        let texture = device.create_texture(&wgpu::TextureDescriptor {
214            label: Some("runtime_depth"),
215            size: wgpu::Extent3d {
216                width: self.width,
217                height: self.height,
218                depth_or_array_layers: 1,
219            },
220            mip_level_count: 1,
221            sample_count: self.msaa_samples,
222            dimension: wgpu::TextureDimension::D2,
223            format: DEPTH_FORMAT,
224            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
225            view_formats: &[],
226        });
227        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
228        self.depth_texture = Some(texture);
229        self.depth_view = Some(view);
230
231        // Allocate MSAA color resolve texture when sample_count > 1.
232        if self.msaa_samples > 1 {
233            let surface_format = wgpu::TextureFormat::Bgra8UnormSrgb;
234            let msaa_tex = device.create_texture(&wgpu::TextureDescriptor {
235                label: Some("runtime_msaa_color"),
236                size: wgpu::Extent3d {
237                    width: self.width,
238                    height: self.height,
239                    depth_or_array_layers: 1,
240                },
241                mip_level_count: 1,
242                sample_count: self.msaa_samples,
243                dimension: wgpu::TextureDimension::D2,
244                format: surface_format,
245                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
246                view_formats: &[],
247            });
248            let msaa_v = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
249            self.msaa_texture = Some(msaa_tex);
250            self.msaa_view = Some(msaa_v);
251        }
252    }
253
254    /// Render the scene. Frame sequence:
255    /// 1. Acquire surface texture (handle Lost/Outdated gracefully)
256    /// 2. DreamFabric begin_frame (observer update, GPU upload)
257    /// 3. Sync GPU scene transforms from engine scene
258    /// 4. Extract → Prepare → Queue (render staging)
259    /// 5. DreamFabric end_frame (cull → DreamMatter simulate → render → picking)
260    /// 6. GPU timestamp resolve + readback
261    /// 7. Submit + present
262    pub fn render(
263        &mut self,
264        ws: &WindowState,
265        scene: &Scene,
266        dt: f32,
267        player_pos: glam::Vec3,
268        active_layer: TopologyLayer,
269    ) {
270        // Record CPU frame time from caller-supplied dt (measured by FrameTimer).
271        self.last_cpu_ms = dt * 1000.0;
272        self.total_time += dt;
273
274        // Read previous frame's GPU timestamp (1-frame lag).
275        self.read_gpu_timestamp(&ws.device);
276
277        // 1. Acquire surface texture.
278        let output = match ws.surface.get_current_texture() {
279            wgpu::CurrentSurfaceTexture::Success(t) => t,
280            wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
281                log::warn!("surface suboptimal — reconfiguring");
282                ws.surface.configure(&ws.device, &ws.surface_config);
283                t
284            }
285            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
286                log::warn!("surface lost/outdated — reconfiguring");
287                ws.surface.configure(&ws.device, &ws.surface_config);
288                return;
289            }
290            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
291                return;
292            }
293            wgpu::CurrentSurfaceTexture::Validation => {
294                log::error!("surface validation error");
295                return;
296            }
297        };
298
299        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
300
301        let mut encoder = ws.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
302            label: Some("runtime_render"),
303        });
304
305        // GPU timestamp: start
306        if let Some(ref qs) = self.query_set {
307            encoder.write_timestamp(qs, 0);
308        }
309
310        // 2. DreamFabric begin_frame
311        let fc = self.fabric.begin_frame(&ws.queue, dt, player_pos, active_layer);
312
313        // 3. Sync GPU scene — re-upload if dirty, otherwise just update transforms.
314        if self.fabric.gpu_scene().is_dirty() {
315            self.fabric.upload_scene(&ws.device, &scene.game_objects);
316        } else {
317            self.fabric.sync_scene_transforms(&scene.game_objects);
318        }
319
320        // 4. Extract → Prepare → Queue (render staging).
321        self.fabric.extract(&scene.game_objects, &fc.gpu_observer, dt);
322        self.fabric.prepare(&scene.game_objects);
323        self.fabric.queue_draw_items(&scene.camera);
324
325        // 5. DreamFabric end_frame — submits compute commands immediately via
326        // queue.submit(), then populates the render encoder. Compute-render ordering
327        // is guaranteed by wgpu's sequential queue submission model.
328        if let Some(dv) = &self.depth_view {
329            self.fabric.end_frame(
330                &ws.device,
331                &ws.queue,
332                &mut encoder,
333                &view,
334                dv,
335                &scene.camera,
336                &fc,
337                self.msaa_view.as_ref(),
338            );
339        } else {
340            // Fallback: depth_view missing — render a clear pass directly to surface
341            // so the window isn't black. This indicates create_depth_texture failed.
342            log::warn!("depth_view is None — rendering fallback clear pass");
343            let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
344                label: Some("fallback_clear"),
345                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
346                    view: &view,
347                    resolve_target: None,
348                    depth_slice: None,
349                    ops: wgpu::Operations {
350                        load: wgpu::LoadOp::Clear(wgpu::Color {
351                            r: 1.0,
352                            g: 0.0,
353                            b: 0.0,
354                            a: 1.0,
355                        }),
356                        store: wgpu::StoreOp::Store,
357                    },
358                })],
359                depth_stencil_attachment: None,
360                timestamp_writes: None,
361                occlusion_query_set: None,
362                multiview_mask: None,
363            });
364        }
365
366        // GPU timestamp: end + resolve + copy to readback
367        if let (Some(ref qs), Some(ref resolve), Some(ref readback)) =
368            (&self.query_set, &self.resolve_buf, &self.readback_buf)
369        {
370            encoder.write_timestamp(qs, 1);
371            encoder.resolve_query_set(qs, 0..2, resolve, 0);
372            encoder.copy_buffer_to_buffer(resolve, 0, readback, 0, 16);
373        }
374
375        // 6. Submit render command buffer. Compute was already submitted by end_frame().
376        let render_cmds = encoder.finish();
377        ws.queue.submit(std::iter::once(render_cmds));
378        output.present();
379
380        // === GPU Causal Observer Pipeline ===
381        self.frame_count += 1;
382
383        // Quality decision (uses previous frame's gpu_ms).
384        let quality = self
385            .frame_gate
386            .evaluate(self.last_gpu_ms, self.fabric.matter().dreammatter_capacity());
387        let quality_tier = self.frame_gate.quality_tier();
388
389        // Seal the frame.
390        let timestamp_ns = std::time::SystemTime::now()
391            .duration_since(std::time::SystemTime::UNIX_EPOCH)
392            .map(|d| d.as_nanos() as u64)
393            .unwrap_or(0);
394        let entry = self.frame_seal.seal_frame(
395            self.frame_count,
396            self.last_gpu_ms,
397            self.last_cpu_ms,
398            quality.particle_count,
399            quality_tier,
400            self.last_bridge_seal,
401            timestamp_ns,
402            true,
403        );
404
405        // Capture to frame memory.
406        let chain_depth = self.frame_seal.chain_depth();
407        self.frame_memory.capture_seal(entry.clone());
408
409        // Record causal observer timing.
410        let dispatch_ns = self.causal_observer_recorder.now_ns();
411        self.causal_observer_recorder.record(
412            self.frame_count,
413            dispatch_ns,
414            self.last_gpu_ms,
415            quality.particle_count,
416            quality_tier,
417            true,
418        );
419
420        // Cold path dispatch.
421        if let Some(ref tx) = self.causal_observer_tx {
422            tx.send_frame(&entry, self.frame_count, chain_depth);
423        }
424
425        // Populate frame contract for app layer.
426        self.last_frame_contract = FrameContract {
427            frame: self.frame_count,
428            cpu_ms: self.last_cpu_ms,
429            gpu_ms: self.last_gpu_ms,
430            budget_ms: 16.67,
431            budget_exceeded: self.last_gpu_ms > 16.67 * 0.85,
432            quality_tier,
433            particle_count: quality.particle_count,
434            post_process_enabled: quality.post_process_enabled,
435            frame_skipped: false,
436            seal_digest: entry.seal_digest,
437            chain_depth,
438            presented: true,
439        };
440    }
441
442    /// Render with CausalEntanglement — production entry point.
443    /// Same pipeline as render() but uses begin_frame_entangled() for
444    /// observer-aware quantum culling via CausalEntanglement.
445    pub fn render_entangled(
446        &mut self,
447        ws: &WindowState,
448        scene: &Scene,
449        dt: f32,
450        entanglement: &CausalEntanglement,
451        active_layer: TopologyLayer,
452    ) {
453        self.last_cpu_ms = dt * 1000.0;
454        self.total_time += dt;
455
456        self.read_gpu_timestamp(&ws.device);
457
458        let output = match ws.surface.get_current_texture() {
459            wgpu::CurrentSurfaceTexture::Success(t) => t,
460            wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
461                log::warn!("surface suboptimal — reconfiguring");
462                ws.surface.configure(&ws.device, &ws.surface_config);
463                t
464            }
465            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
466                log::warn!("surface lost/outdated — reconfiguring");
467                ws.surface.configure(&ws.device, &ws.surface_config);
468                return;
469            }
470            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
471                return;
472            }
473            wgpu::CurrentSurfaceTexture::Validation => {
474                log::error!("surface validation error");
475                return;
476            }
477        };
478
479        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
480
481        let mut encoder = ws.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
482            label: Some("runtime_render_entangled"),
483        });
484
485        if let Some(ref qs) = self.query_set {
486            encoder.write_timestamp(qs, 0);
487        }
488
489        // Production entry: CausalEntanglement → GpuObserverContext
490        let fc = self
491            .fabric
492            .begin_frame_entangled(&ws.queue, dt, entanglement, active_layer);
493
494        if self.fabric.gpu_scene().is_dirty() {
495            self.fabric.upload_scene(&ws.device, &scene.game_objects);
496        } else {
497            self.fabric.sync_scene_transforms(&scene.game_objects);
498        }
499
500        self.fabric.extract(&scene.game_objects, &fc.gpu_observer, dt);
501        self.fabric.prepare(&scene.game_objects);
502        self.fabric.queue_draw_items(&scene.camera);
503
504        if let Some(dv) = &self.depth_view {
505            self.fabric.end_frame(
506                &ws.device,
507                &ws.queue,
508                &mut encoder,
509                &view,
510                dv,
511                &scene.camera,
512                &fc,
513                self.msaa_view.as_ref(),
514            );
515        }
516
517        if let (Some(ref qs), Some(ref resolve), Some(ref readback)) =
518            (&self.query_set, &self.resolve_buf, &self.readback_buf)
519        {
520            encoder.write_timestamp(qs, 1);
521            encoder.resolve_query_set(qs, 0..2, resolve, 0);
522            encoder.copy_buffer_to_buffer(resolve, 0, readback, 0, 16);
523        }
524
525        let render_cmds = encoder.finish();
526        ws.queue.submit(std::iter::once(render_cmds));
527        output.present();
528
529        // === GPU Causal Observer Pipeline ===
530        self.frame_count += 1;
531
532        // Quality decision (uses previous frame's gpu_ms).
533        let quality = self
534            .frame_gate
535            .evaluate(self.last_gpu_ms, self.fabric.matter().dreammatter_capacity());
536        let quality_tier = self.frame_gate.quality_tier();
537
538        // Seal the frame.
539        let timestamp_ns = std::time::SystemTime::now()
540            .duration_since(std::time::SystemTime::UNIX_EPOCH)
541            .map(|d| d.as_nanos() as u64)
542            .unwrap_or(0);
543        let entry = self.frame_seal.seal_frame(
544            self.frame_count,
545            self.last_gpu_ms,
546            self.last_cpu_ms,
547            quality.particle_count,
548            quality_tier,
549            self.last_bridge_seal,
550            timestamp_ns,
551            true,
552        );
553
554        // Capture to frame memory.
555        let chain_depth = self.frame_seal.chain_depth();
556        self.frame_memory.capture_seal(entry.clone());
557
558        // Record causal observer timing.
559        let dispatch_ns = self.causal_observer_recorder.now_ns();
560        self.causal_observer_recorder.record(
561            self.frame_count,
562            dispatch_ns,
563            self.last_gpu_ms,
564            quality.particle_count,
565            quality_tier,
566            true,
567        );
568
569        // Cold path dispatch.
570        if let Some(ref tx) = self.causal_observer_tx {
571            tx.send_frame(&entry, self.frame_count, chain_depth);
572        }
573
574        // Populate frame contract for app layer.
575        self.last_frame_contract = FrameContract {
576            frame: self.frame_count,
577            cpu_ms: self.last_cpu_ms,
578            gpu_ms: self.last_gpu_ms,
579            budget_ms: 16.67,
580            budget_exceeded: self.last_gpu_ms > 16.67 * 0.85,
581            quality_tier,
582            particle_count: quality.particle_count,
583            post_process_enabled: quality.post_process_enabled,
584            frame_skipped: false,
585            seal_digest: entry.seal_digest,
586            chain_depth,
587            presented: true,
588        };
589    }
590
591    /// Read GPU timestamp from the readback buffer (previous frame's data).
592    /// Polls the device to resolve the map_async callback, then reads with 1-frame lag.
593    fn read_gpu_timestamp(&mut self, device: &wgpu::Device) {
594        let Some(ref readback) = self.readback_buf else { return };
595        if self.timestamp_period == 0.0 {
596            return;
597        }
598
599        let slice = readback.slice(..);
600        let (tx, rx) = std::sync::mpsc::sync_channel(1);
601        slice.map_async(wgpu::MapMode::Read, move |result| {
602            let _ = tx.send(result);
603        });
604
605        // Poll device to give the map_async callback a chance to fire.
606        // Maintain::Poll is non-blocking — returns immediately if no work is pending.
607        let _ = device.poll(wgpu::PollType::Poll);
608
609        if let Ok(Ok(())) = rx.try_recv() {
610            let data = slice.get_mapped_range();
611            if data.len() >= 16 {
612                let timestamps: &[u64] = bytemuck::cast_slice(&data[..16]);
613                let start = timestamps[0];
614                let end = timestamps[1];
615                if end > start {
616                    self.last_gpu_ms = (end - start) as f32 * self.timestamp_period / 1_000_000.0;
617                }
618            }
619            drop(data);
620            readback.unmap();
621        }
622    }
623
624    /// Current render dimensions.
625    pub fn dimensions(&self) -> (u32, u32) {
626        (self.width, self.height)
627    }
628
629    /// MSAA sample count.
630    pub fn msaa_samples(&self) -> u32 {
631        self.msaa_samples
632    }
633
634    /// Get the MSAA view for render pass color attachment.
635    /// Returns None when MSAA is disabled (sample_count = 1).
636    pub fn msaa_view(&self) -> Option<&wgpu::TextureView> {
637        self.msaa_view.as_ref()
638    }
639
640    /// Last picked object ID.
641    pub fn last_picked_id(&self) -> u32 {
642        self.fabric.last_picked_id()
643    }
644
645    /// Per-frame stats. `cpu_ms` is the wall-clock frame time from FrameTimer.
646    /// `gpu_ms` is populated from GPU timestamp queries when available (1-frame lag).
647    pub fn last_frame_stats(&self) -> dreamwell_fabric::FrameStats {
648        dreamwell_fabric::FrameStats {
649            cpu_ms: self.last_cpu_ms,
650            gpu_ms: self.last_gpu_ms,
651            visible_meshlets: self.fabric.gpu_scene().object_count() as u32,
652            particle_count: self.fabric.matter().dreammatter_capacity(),
653        }
654    }
655
656    /// Set post-processing configuration.
657    pub fn set_post_process_config(&mut self, config: PostProcessConfig) {
658        self.fabric.set_post_process_config(config);
659    }
660
661    /// Get post-processing configuration.
662    pub fn post_process_config(&self) -> &PostProcessConfig {
663        self.fabric.post_process_config()
664    }
665
666    /// Set the scene rendering mode.
667    pub fn set_scene_dream_mode(&mut self, mode: SceneDreamMode) {
668        self.fabric.set_scene_dream_mode(mode);
669    }
670
671    /// Get the current scene rendering mode.
672    pub fn scene_dream_mode(&self) -> SceneDreamMode {
673        self.fabric.scene_dream_mode()
674    }
675
676    /// Set the causal observer lane sender for cold path dispatch.
677    pub fn set_causal_observer_sender(&mut self, tx: CausalObserverLaneSender) {
678        self.causal_observer_tx = Some(tx);
679    }
680
681    /// Set the last bridge seal digest (from decoder).
682    pub fn set_bridge_seal(&mut self, seal: [u8; 32]) {
683        self.last_bridge_seal = seal;
684    }
685
686    /// Get the frame gate for quality tier inspection.
687    pub fn frame_gate(&self) -> &FrameGate {
688        &self.frame_gate
689    }
690
691    /// Get the frame seal for chain depth inspection.
692    pub fn frame_seal(&self) -> &FrameSeal {
693        &self.frame_seal
694    }
695
696    /// Get the frame memory for replay/debug access.
697    pub fn frame_memory(&self) -> &FrameMemory {
698        &self.frame_memory
699    }
700
701    /// Get the causal observer recorder for KPI timing.
702    pub fn causal_observer_recorder(&self) -> &CausalObserverRecorder {
703        &self.causal_observer_recorder
704    }
705}