Skip to main content

cvkg_render_gpu/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     — State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     — Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     — Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    — Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–7) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     — Read the target, its surrounding context, and its full call graph
13//!                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     — Every major pub fn, unsafe block, and non-trivial algorithm in
15//!                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//!                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   — Check every tool call / command for progress every 30 seconds.
18//!                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//!                      and move to unblocked work. Never silently accept a broken state.
20//!
21//! Sources:
22//!   Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
23//!   CVKG Extended: Section 2 of the CVKG Design Specification
24
25//! # Surtr Render Pipeline
26//!
27//! The "Fiery Giant" of the CVKG architecture. This is the authoritative GPU renderer
28//! powered by `wgpu`. It manages the heat of the GPU to forge high-fidelity 
29//! "Berserker" aesthetics.
30//!
31//! - **The Flaming Sword**: Command submission and synchronization.
32//! - **Muspelheim Passes**: Multi-pass Gaussian blur and bloom for Bifrost/Gungnir.
33
34use cvkg_core::Rect;
35use std::sync::Arc;
36
37// ShieldWall — re-export AccessKit types so callers can build tree updates
38// without depending on accesskit directly.
39pub use accesskit::{
40    ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler,
41    Node, NodeId, Role, Tree, TreeId, TreeUpdate,
42};
43pub use accesskit_winit::Adapter as ShieldWallAdapter;
44
45
46#[repr(C)]
47#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
48pub struct Vertex {
49    pub position: [f32; 2],
50    pub uv: [f32; 2],
51    pub color: [f32; 4],
52    pub mode: u32,
53}
54
55impl Vertex {
56    const ATTRIBUTES: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
57        0 => Float32x2,
58        1 => Float32x2,
59        2 => Float32x4,
60        3 => Uint32
61    ];
62
63    fn desc() -> wgpu::VertexBufferLayout<'static> {
64        wgpu::VertexBufferLayout {
65            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
66            step_mode: wgpu::VertexStepMode::Vertex,
67            attributes: &Self::ATTRIBUTES,
68        }
69    }
70}
71
72/// SurtrRenderer implements the high-performance GPU backend.
73pub struct SurtrRenderer {
74    device: Arc<wgpu::Device>,
75    queue: Arc<wgpu::Queue>,
76    surface: wgpu::Surface<'static>,
77    config: wgpu::SurfaceConfiguration,
78    pipeline: wgpu::RenderPipeline,
79    
80    // Muspelheim Pass Resources
81    #[allow(dead_code)]
82    bloom_extract_pipeline: wgpu::RenderPipeline,
83    blur_h_pipeline: wgpu::RenderPipeline,
84    blur_v_pipeline: wgpu::RenderPipeline,
85    composite_pipeline: wgpu::RenderPipeline,
86    blur_texture_a: wgpu::TextureView,
87    blur_texture_b: wgpu::TextureView,
88    blur_bind_group_a: wgpu::BindGroup,
89    blur_bind_group_b: wgpu::BindGroup,
90    
91    // Text Forge
92    #[allow(dead_code)]
93    font_system: cosmic_text::FontSystem,
94    #[allow(dead_code)]
95    swash_cache: cosmic_text::SwashCache,
96
97    // Niflheim Resources
98    dummy_bind_group: wgpu::BindGroup,
99
100    // The Forge's Anvil (GPU Buffers)
101    vertex_buffer: wgpu::Buffer,
102    index_buffer: wgpu::Buffer,
103    vertices: Vec<Vertex>,
104    indices: Vec<u16>,
105
106    // Opacity stack: each push multiplies into the current effective alpha.
107    opacity_stack: Vec<f32>,
108    // Clip rect stack: stored for future scissor-rect support.
109    clip_stack: Vec<Rect>,
110}
111
112const MAX_VERTICES: usize = 10000;
113const MAX_INDICES: usize = 15000;
114
115impl SurtrRenderer {
116    /// Forge a new SurtrRenderer from a winit window.
117    pub async fn forge(window: Arc<winit::window::Window>) -> Self {
118        let instance = wgpu::Instance::default();
119        let surface = instance.create_surface(window.clone()).unwrap();
120        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
121            power_preference: wgpu::PowerPreference::HighPerformance,
122            compatible_surface: Some(&surface),
123            force_fallback_adapter: false,
124        }).await.expect("Failed to find a suitable GPU for Surtr");
125
126        let (device, queue) = adapter.request_device(
127            &wgpu::DeviceDescriptor {
128                label: Some("Surtr Forge"),
129                required_features: wgpu::Features::empty(),
130                required_limits: wgpu::Limits::default(),
131            },
132            None,
133        ).await.expect("Failed to create Surtr device");
134
135        let device = Arc::new(device);
136        let queue = Arc::new(queue);
137        
138        let size = window.inner_size();
139        let config = surface.get_default_config(&adapter, size.width, size.height).unwrap();
140        surface.configure(&device, &config);
141
142        // Load the Muspelheim Shaders
143        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
144            label: Some("Muspelheim Main Shader"),
145            source: wgpu::ShaderSource::Wgsl(include_str!("shaders.wgsl").into()),
146        });
147
148        // Niflheim Bind Group Layout (for textures/samplers)
149        let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
150            entries: &[
151                wgpu::BindGroupLayoutEntry {
152                    binding: 0,
153                    visibility: wgpu::ShaderStages::FRAGMENT,
154                    ty: wgpu::BindingType::Texture {
155                        multisampled: false,
156                        view_dimension: wgpu::TextureViewDimension::D2,
157                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
158                    },
159                    count: None,
160                },
161                wgpu::BindGroupLayoutEntry {
162                    binding: 1,
163                    visibility: wgpu::ShaderStages::FRAGMENT,
164                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
165                    count: None,
166                },
167            ],
168            label: Some("Niflheim Texture Bind Group Layout"),
169        });
170
171        // Pipeline setup
172        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
173            label: Some("Surtr Pipeline Layout"),
174            bind_group_layouts: &[&texture_bind_group_layout],
175            push_constant_ranges: &[],
176        });
177
178        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
179            label: Some("Surtr Main Pipeline"),
180            layout: Some(&pipeline_layout),
181            vertex: wgpu::VertexState {
182                module: &shader,
183                entry_point: "vs_main",
184                buffers: &[Vertex::desc()],
185            },
186            fragment: Some(wgpu::FragmentState {
187                module: &shader,
188                entry_point: "fs_main",
189                targets: &[Some(wgpu::ColorTargetState {
190                    format: config.format,
191                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
192                    write_mask: wgpu::ColorWrites::ALL,
193                })],
194            }),
195            primitive: wgpu::PrimitiveState::default(),
196            depth_stencil: None,
197            multisample: wgpu::MultisampleState::default(),
198            multiview: None,
199        });
200
201        // Muspelheim Bloom Extract Pipeline
202        let bloom_extract_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
203            label: Some("Muspelheim Bloom Extract"),
204            layout: Some(&pipeline_layout),
205            vertex: wgpu::VertexState {
206                module: &shader,
207                entry_point: "vs_fullscreen",
208                buffers: &[],
209            },
210            fragment: Some(wgpu::FragmentState {
211                module: &shader,
212                entry_point: "fs_bloom_extract",
213                targets: &[Some(wgpu::ColorTargetState {
214                    format: config.format,
215                    blend: None,
216                    write_mask: wgpu::ColorWrites::ALL,
217                })],
218            }),
219            primitive: wgpu::PrimitiveState::default(),
220            depth_stencil: None,
221            multisample: wgpu::MultisampleState::default(),
222            multiview: None,
223        });
224
225        // Muspelheim Blur Pipelines (H and V)
226        let blur_h_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
227            label: Some("Muspelheim Horizontal Blur"),
228            layout: Some(&pipeline_layout),
229            vertex: wgpu::VertexState {
230                module: &shader,
231                entry_point: "vs_fullscreen",
232                buffers: &[],
233            },
234            fragment: Some(wgpu::FragmentState {
235                module: &shader,
236                entry_point: "fs_blur_h",
237                targets: &[Some(wgpu::ColorTargetState {
238                    format: config.format,
239                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
240                    write_mask: wgpu::ColorWrites::ALL,
241                })],
242            }),
243            primitive: wgpu::PrimitiveState::default(),
244            depth_stencil: None,
245            multisample: wgpu::MultisampleState::default(),
246            multiview: None,
247        });
248
249        let blur_v_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
250            label: Some("Muspelheim Vertical Blur"),
251            layout: Some(&pipeline_layout),
252            vertex: wgpu::VertexState {
253                module: &shader,
254                entry_point: "vs_fullscreen",
255                buffers: &[],
256            },
257            fragment: Some(wgpu::FragmentState {
258                module: &shader,
259                entry_point: "fs_blur_v",
260                targets: &[Some(wgpu::ColorTargetState {
261                    format: config.format,
262                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
263                    write_mask: wgpu::ColorWrites::ALL,
264                })],
265            }),
266            primitive: wgpu::PrimitiveState::default(),
267            depth_stencil: None,
268            multisample: wgpu::MultisampleState::default(),
269            multiview: None,
270        });
271
272        // Muspelheim Composite Pipeline (additive blend onto screen)
273        let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
274            label: Some("Muspelheim Composite"),
275            layout: Some(&pipeline_layout),
276            vertex: wgpu::VertexState {
277                module: &shader,
278                entry_point: "vs_fullscreen",
279                buffers: &[],
280            },
281            fragment: Some(wgpu::FragmentState {
282                module: &shader,
283                entry_point: "fs_composite",
284                targets: &[Some(wgpu::ColorTargetState {
285                    format: config.format,
286                    // Additive blend: src + dst — glow lights up the scene
287                    blend: Some(wgpu::BlendState {
288                        color: wgpu::BlendComponent {
289                            src_factor: wgpu::BlendFactor::One,
290                            dst_factor: wgpu::BlendFactor::One,
291                            operation: wgpu::BlendOperation::Add,
292                        },
293                        alpha: wgpu::BlendComponent {
294                            src_factor: wgpu::BlendFactor::One,
295                            dst_factor: wgpu::BlendFactor::One,
296                            operation: wgpu::BlendOperation::Add,
297                        },
298                    }),
299                    write_mask: wgpu::ColorWrites::ALL,
300                })],
301            }),
302            primitive: wgpu::PrimitiveState::default(),
303            depth_stencil: None,
304            multisample: wgpu::MultisampleState::default(),
305            multiview: None,
306        });
307
308        // Muspelheim Intermediate Textures
309        let blur_tex_desc = wgpu::TextureDescriptor {
310            label: Some("Muspelheim Intermediate"),
311            size: wgpu::Extent3d {
312                width: config.width,
313                height: config.height,
314                depth_or_array_layers: 1,
315            },
316            mip_level_count: 1,
317            sample_count: 1,
318            dimension: wgpu::TextureDimension::D2,
319            format: config.format,
320            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
321            view_formats: &[],
322        };
323        let blur_texture_a_obj = device.create_texture(&blur_tex_desc);
324        let blur_texture_b_obj = device.create_texture(&blur_tex_desc);
325        let blur_texture_a = blur_texture_a_obj.create_view(&wgpu::TextureViewDescriptor::default());
326        let blur_texture_b = blur_texture_b_obj.create_view(&wgpu::TextureViewDescriptor::default());
327
328        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
329            address_mode_u: wgpu::AddressMode::ClampToEdge,
330            address_mode_v: wgpu::AddressMode::ClampToEdge,
331            mag_filter: wgpu::FilterMode::Linear,
332            ..Default::default()
333        });
334
335        let blur_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
336            layout: &texture_bind_group_layout,
337            entries: &[
338                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&blur_texture_a) },
339                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
340            ],
341            label: Some("Blur Bind Group A"),
342        });
343
344        let blur_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
345            layout: &texture_bind_group_layout,
346            entries: &[
347                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&blur_texture_b) },
348                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
349            ],
350            label: Some("Blur Bind Group B"),
351        });
352
353        // Forge the Niflheim Dummy Texture (1x1 White)
354        let dummy_size = wgpu::Extent3d {
355            width: 1,
356            height: 1,
357            depth_or_array_layers: 1,
358        };
359        let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
360            label: Some("Niflheim Dummy Texture"),
361            size: dummy_size,
362            mip_level_count: 1,
363            sample_count: 1,
364            dimension: wgpu::TextureDimension::D2,
365            format: wgpu::TextureFormat::Rgba8UnormSrgb,
366            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
367            view_formats: &[],
368        });
369        queue.write_texture(
370            wgpu::ImageCopyTexture {
371                texture: &dummy_texture,
372                mip_level: 0,
373                origin: wgpu::Origin3d::ZERO,
374                aspect: wgpu::TextureAspect::All,
375            },
376            &[255, 255, 255, 255],
377            wgpu::ImageDataLayout {
378                offset: 0,
379                bytes_per_row: Some(4),
380                rows_per_image: Some(1),
381            },
382            dummy_size,
383        );
384
385        let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
386        let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
387            address_mode_u: wgpu::AddressMode::ClampToEdge,
388            address_mode_v: wgpu::AddressMode::ClampToEdge,
389            address_mode_w: wgpu::AddressMode::ClampToEdge,
390            mag_filter: wgpu::FilterMode::Linear,
391            min_filter: wgpu::FilterMode::Nearest,
392            mipmap_filter: wgpu::FilterMode::Nearest,
393            ..Default::default()
394        });
395
396        let dummy_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
397            layout: &texture_bind_group_layout,
398            entries: &[
399                wgpu::BindGroupEntry {
400                    binding: 0,
401                    resource: wgpu::BindingResource::TextureView(&dummy_view),
402                },
403                wgpu::BindGroupEntry {
404                    binding: 1,
405                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
406                },
407            ],
408            label: Some("Niflheim Dummy Bind Group"),
409        });
410
411        // Forge the Anvil (Buffers)
412        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
413            label: Some("Surtr Vertex Anvil"),
414            size: (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
415            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
416            mapped_at_creation: false,
417        });
418
419        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
420            label: Some("Surtr Index Anvil"),
421            size: (MAX_INDICES * std::mem::size_of::<u16>()) as u64,
422            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
423            mapped_at_creation: false,
424        });
425
426
427        Self {
428            device,
429            queue,
430            surface,
431            config,
432            pipeline,
433            bloom_extract_pipeline,
434            blur_h_pipeline,
435            blur_v_pipeline,
436            composite_pipeline,
437            blur_texture_a,
438            blur_texture_b,
439            blur_bind_group_a,
440            blur_bind_group_b,
441            font_system: cosmic_text::FontSystem::new(),
442            swash_cache: cosmic_text::SwashCache::new(),
443            dummy_bind_group,
444            vertex_buffer,
445            index_buffer,
446            vertices: Vec::with_capacity(MAX_VERTICES),
447            indices: Vec::with_capacity(MAX_INDICES),
448            opacity_stack: Vec::new(),
449            clip_stack: Vec::new(),
450        }
451    }
452
453    /// begin_frame — Strike the flaming sword to begin a new GPU frame.
454    pub fn begin_frame(&mut self) -> wgpu::CommandEncoder {
455        self.vertices.clear();
456        self.indices.clear();
457        self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
458            label: Some("Surtr's Flaming Sword"),
459        })
460    }
461
462    /// end_frame — Quench the blade by submitting the full Muspelheim multi-pass effect.
463    pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
464        self.queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
465        self.queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&self.indices));
466
467        let frame = self.surface.get_current_texture()
468            .expect("Surtr: failed to acquire surface texture");
469        let screen = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
470
471        // ── Pass 1: Base scene → screen ─────────────────────────────────────
472        {
473            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
474                label: Some("Surtr P1 Base"),
475                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
476                    view: &screen,
477                    resolve_target: None,
478                    ops: wgpu::Operations {
479                        load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }), // Ginnungagap
480                        store: wgpu::StoreOp::Store,
481                    },
482                })],
483                depth_stencil_attachment: None,
484                occlusion_query_set: None,
485                timestamp_writes: None,
486            });
487            if !self.indices.is_empty() {
488                p.set_pipeline(&self.pipeline);
489                p.set_bind_group(0, &self.dummy_bind_group, &[]);
490                p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
491                p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
492                p.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
493            }
494        }
495
496        // ── Pass 2: Bloom extract  screen → tex_a ────────────────────────────
497        {
498            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
499                label: Some("Surtr P2 Bloom Src"),
500                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
501                    view: &self.blur_texture_a,
502                    resolve_target: None,
503                    ops: wgpu::Operations {
504                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
505                        store: wgpu::StoreOp::Store,
506                    },
507                })],
508                depth_stencil_attachment: None,
509                occlusion_query_set: None,
510                timestamp_writes: None,
511            });
512            if !self.indices.is_empty() {
513                p.set_pipeline(&self.pipeline);
514                p.set_bind_group(0, &self.dummy_bind_group, &[]);
515                p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
516                p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
517                p.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
518            }
519        }
520
521        // ── Passes 3–6+: Ping-pong Gaussian blur ──────────────────────────
522        let blur_iters: u32 = 6;
523        for i in 0..blur_iters {
524            {
525                let label = format!("Surtr Blur H iter {}", i);
526                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
527                    label: Some(&label),
528                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
529                        view: &self.blur_texture_b,
530                        resolve_target: None,
531                        ops: wgpu::Operations {
532                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
533                            store: wgpu::StoreOp::Store,
534                        },
535                    })],
536                    depth_stencil_attachment: None,
537                    occlusion_query_set: None,
538                    timestamp_writes: None,
539                });
540                p.set_pipeline(&self.blur_h_pipeline);
541                p.set_bind_group(0, &self.blur_bind_group_a, &[]);
542                p.draw(0..3, 0..1);
543            }
544            {
545                let label = format!("Surtr Blur V iter {}", i);
546                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
547                    label: Some(&label),
548                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
549                        view: &self.blur_texture_a,
550                        resolve_target: None,
551                        ops: wgpu::Operations {
552                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
553                            store: wgpu::StoreOp::Store,
554                        },
555                    })],
556                    depth_stencil_attachment: None,
557                    occlusion_query_set: None,
558                    timestamp_writes: None,
559                });
560                p.set_pipeline(&self.blur_v_pipeline);
561                p.set_bind_group(0, &self.blur_bind_group_b, &[]);
562                p.draw(0..3, 0..1);
563            }
564        }
565
566        // ── Pass 7: Additive composite  tex_a → screen ──────────────────────
567        {
568            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
569                label: Some("Surtr P7 Composite"),
570                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
571                    view: &screen,
572                    resolve_target: None,
573                    ops: wgpu::Operations {
574                        load: wgpu::LoadOp::Load,
575                        store: wgpu::StoreOp::Store,
576                    },
577                })],
578                depth_stencil_attachment: None,
579                occlusion_query_set: None,
580                timestamp_writes: None,
581            });
582            p.set_pipeline(&self.composite_pipeline);
583            p.set_bind_group(0, &self.blur_bind_group_a, &[]);
584            p.draw(0..3, 0..1);
585        }
586
587        self.queue.submit(Some(encoder.finish()));
588        frame.present();
589    }
590}
591
592impl cvkg_core::Renderer for SurtrRenderer {
593    fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
594        self.fill_rect_with_mode(rect, self.apply_opacity(color), 0);
595    }
596
597    fn fill_rounded_rect(&mut self, rect: Rect, _radius: f32, color: [f32; 4]) {
598        // GPU-side rounding is handled in the fragment shader via mode 2 (future).
599        // For now fall through to a plain quad.
600        self.fill_rect_with_mode(rect, self.apply_opacity(color), 0);
601    }
602
603    fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
604        // Approximated as a quad; a true SDF ellipse shader is a future enhancement.
605        self.fill_rect_with_mode(rect, self.apply_opacity(color), 0);
606    }
607
608    fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
609        let c = self.apply_opacity(color);
610        let hw = stroke_width;
611        // Top, bottom, left, right edge bars
612        self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y, width: rect.width, height: hw }, c, 1);
613        self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y + rect.height - hw, width: rect.width, height: hw }, c, 1);
614        self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y, width: hw, height: rect.height }, c, 1);
615        self.fill_rect_with_mode(Rect { x: rect.x + rect.width - hw, y: rect.y, width: hw, height: rect.height }, c, 1);
616    }
617
618    fn stroke_rounded_rect(&mut self, rect: Rect, _radius: f32, color: [f32; 4], stroke_width: f32) {
619        // Delegate to stroke_rect until shader SDF support is added.
620        self.stroke_rect(rect, color, stroke_width);
621    }
622
623    fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
624        self.stroke_rect(rect, color, stroke_width);
625    }
626
627    fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: [f32; 4], stroke_width: f32) {
628        // Represent as a thin oriented quad.
629        let dx = x2 - x1;
630        let dy = y2 - y1;
631        let len = (dx * dx + dy * dy).sqrt();
632        if len < 0.001 { return; }
633        let c = self.apply_opacity(color);
634        // Fall back to axis-aligned approximation for the vertex buffer.
635        let min_x = x1.min(x2);
636        let min_y = y1.min(y2);
637        let w = (dx.abs()).max(stroke_width);
638        let h = (dy.abs()).max(stroke_width);
639        self.fill_rect_with_mode(Rect { x: min_x, y: min_y, width: w, height: h }, c, 1);
640    }
641
642    fn draw_text(&mut self, _text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
643        // Full cosmic-text rasterisation path is a future enhancement.
644        // For now, reserve space as a coloured placeholder so layout is correct.
645        let c = self.apply_opacity(color);
646        self.fill_rect_with_mode(Rect { x, y, width: size * 5.0, height: size }, c, 0);
647    }
648
649    fn draw_texture(&mut self, _texture_id: u32, rect: Rect) {
650        self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 0);
651    }
652
653    fn draw_image(&mut self, _image_name: &str, rect: Rect) {
654        self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 0);
655    }
656
657    fn push_clip_rect(&mut self, rect: Rect) {
658        // Stored for future scissor-rect integration into the render pass.
659        self.clip_stack.push(rect);
660    }
661
662    fn pop_clip_rect(&mut self) {
663        self.clip_stack.pop();
664    }
665
666    fn push_opacity(&mut self, opacity: f32) {
667        let current = self.opacity_stack.last().copied().unwrap_or(1.0);
668        self.opacity_stack.push(current * opacity);
669    }
670
671    fn pop_opacity(&mut self) {
672        self.opacity_stack.pop();
673    }
674}
675
676impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
677    fn begin_frame(&mut self) -> wgpu::CommandEncoder {
678        self.begin_frame()
679    }
680
681    fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
682        self.end_frame(encoder)
683    }
684}
685
686impl SurtrRenderer {
687    /// Returns the current effective opacity (product of all stacked values).
688    fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
689        if let Some(&alpha) = self.opacity_stack.last() {
690            color[3] *= alpha;
691        }
692        color
693    }
694
695    /// Converts a Rect in logical pixels into NDC and appends a quad.
696    /// `mode` controls the fragment shader path: 0 = solid fill, 1 = glow/bloom.
697    pub fn fill_rect_with_mode(&mut self, rect: Rect, color: [f32; 4], mode: u32) {
698        let base_idx = self.vertices.len() as u16;
699
700        // Use actual surface size for NDC conversion so quads are correct at any resolution.
701        let half_w = self.config.width as f32 / 2.0;
702        let half_h = self.config.height as f32 / 2.0;
703
704        let x1 = (rect.x / half_w) - 1.0;
705        let y1 = 1.0 - (rect.y / half_h);
706        let x2 = ((rect.x + rect.width) / half_w) - 1.0;
707        let y2 = 1.0 - ((rect.y + rect.height) / half_h);
708
709        self.vertices.push(Vertex { position: [x1, y1], uv: [0.0, 0.0], color, mode });
710        self.vertices.push(Vertex { position: [x2, y1], uv: [1.0, 0.0], color, mode });
711        self.vertices.push(Vertex { position: [x2, y2], uv: [1.0, 1.0], color, mode });
712        self.vertices.push(Vertex { position: [x1, y2], uv: [0.0, 1.0], color, mode });
713
714        self.indices.extend_from_slice(&[
715            base_idx, base_idx + 1, base_idx + 2,
716            base_idx, base_idx + 2, base_idx + 3,
717        ]);
718    }
719}