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    bloom_extract_pipeline: wgpu::RenderPipeline,
82    blur_h_pipeline: wgpu::RenderPipeline,
83    blur_v_pipeline: wgpu::RenderPipeline,
84    composite_pipeline: wgpu::RenderPipeline,
85    blur_texture_a: wgpu::TextureView,
86    blur_texture_b: wgpu::TextureView,
87    blur_bind_group_a: wgpu::BindGroup,
88    blur_bind_group_b: wgpu::BindGroup,
89    
90    // Text Forge
91    font_system: cosmic_text::FontSystem,
92    swash_cache: cosmic_text::SwashCache,
93
94    // Niflheim Resources
95    dummy_bind_group: wgpu::BindGroup,
96
97    // The Forge's Anvil (GPU Buffers)
98    vertex_buffer: wgpu::Buffer,
99    index_buffer: wgpu::Buffer,
100    vertices: Vec<Vertex>,
101    indices: Vec<u16>,
102}
103
104const MAX_VERTICES: usize = 10000;
105const MAX_INDICES: usize = 15000;
106
107impl SurtrRenderer {
108    /// Forge a new SurtrRenderer from a winit window.
109    pub async fn forge(window: Arc<winit::window::Window>) -> Self {
110        let instance = wgpu::Instance::default();
111        let surface = instance.create_surface(window.clone()).unwrap();
112        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
113            power_preference: wgpu::PowerPreference::HighPerformance,
114            compatible_surface: Some(&surface),
115            force_fallback_adapter: false,
116        }).await.expect("Failed to find a suitable GPU for Surtr");
117
118        let (device, queue) = adapter.request_device(
119            &wgpu::DeviceDescriptor {
120                label: Some("Surtr Forge"),
121                required_features: wgpu::Features::empty(),
122                required_limits: wgpu::Limits::default(),
123            },
124            None,
125        ).await.expect("Failed to create Surtr device");
126
127        let device = Arc::new(device);
128        let queue = Arc::new(queue);
129        
130        let size = window.inner_size();
131        let config = surface.get_default_config(&adapter, size.width, size.height).unwrap();
132        surface.configure(&device, &config);
133
134        // Load the Muspelheim Shaders
135        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
136            label: Some("Muspelheim Main Shader"),
137            source: wgpu::ShaderSource::Wgsl(include_str!("shaders.wgsl").into()),
138        });
139
140        // Niflheim Bind Group Layout (for textures/samplers)
141        let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
142            entries: &[
143                wgpu::BindGroupLayoutEntry {
144                    binding: 0,
145                    visibility: wgpu::ShaderStages::FRAGMENT,
146                    ty: wgpu::BindingType::Texture {
147                        multisampled: false,
148                        view_dimension: wgpu::TextureViewDimension::D2,
149                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
150                    },
151                    count: None,
152                },
153                wgpu::BindGroupLayoutEntry {
154                    binding: 1,
155                    visibility: wgpu::ShaderStages::FRAGMENT,
156                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
157                    count: None,
158                },
159            ],
160            label: Some("Niflheim Texture Bind Group Layout"),
161        });
162
163        // Pipeline setup
164        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
165            label: Some("Surtr Pipeline Layout"),
166            bind_group_layouts: &[&texture_bind_group_layout],
167            push_constant_ranges: &[],
168        });
169
170        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
171            label: Some("Surtr Main Pipeline"),
172            layout: Some(&pipeline_layout),
173            vertex: wgpu::VertexState {
174                module: &shader,
175                entry_point: "vs_main",
176                buffers: &[Vertex::desc()],
177            },
178            fragment: Some(wgpu::FragmentState {
179                module: &shader,
180                entry_point: "fs_main",
181                targets: &[Some(wgpu::ColorTargetState {
182                    format: config.format,
183                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
184                    write_mask: wgpu::ColorWrites::ALL,
185                })],
186            }),
187            primitive: wgpu::PrimitiveState::default(),
188            depth_stencil: None,
189            multisample: wgpu::MultisampleState::default(),
190            multiview: None,
191        });
192
193        // Muspelheim Bloom Extract Pipeline
194        let bloom_extract_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
195            label: Some("Muspelheim Bloom Extract"),
196            layout: Some(&pipeline_layout),
197            vertex: wgpu::VertexState {
198                module: &shader,
199                entry_point: "vs_fullscreen",
200                buffers: &[],
201            },
202            fragment: Some(wgpu::FragmentState {
203                module: &shader,
204                entry_point: "fs_bloom_extract",
205                targets: &[Some(wgpu::ColorTargetState {
206                    format: config.format,
207                    blend: None,
208                    write_mask: wgpu::ColorWrites::ALL,
209                })],
210            }),
211            primitive: wgpu::PrimitiveState::default(),
212            depth_stencil: None,
213            multisample: wgpu::MultisampleState::default(),
214            multiview: None,
215        });
216
217        // Muspelheim Blur Pipelines (H and V)
218        let blur_h_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
219            label: Some("Muspelheim Horizontal Blur"),
220            layout: Some(&pipeline_layout),
221            vertex: wgpu::VertexState {
222                module: &shader,
223                entry_point: "vs_fullscreen",
224                buffers: &[],
225            },
226            fragment: Some(wgpu::FragmentState {
227                module: &shader,
228                entry_point: "fs_blur_h",
229                targets: &[Some(wgpu::ColorTargetState {
230                    format: config.format,
231                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
232                    write_mask: wgpu::ColorWrites::ALL,
233                })],
234            }),
235            primitive: wgpu::PrimitiveState::default(),
236            depth_stencil: None,
237            multisample: wgpu::MultisampleState::default(),
238            multiview: None,
239        });
240
241        let blur_v_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
242            label: Some("Muspelheim Vertical Blur"),
243            layout: Some(&pipeline_layout),
244            vertex: wgpu::VertexState {
245                module: &shader,
246                entry_point: "vs_fullscreen",
247                buffers: &[],
248            },
249            fragment: Some(wgpu::FragmentState {
250                module: &shader,
251                entry_point: "fs_blur_v",
252                targets: &[Some(wgpu::ColorTargetState {
253                    format: config.format,
254                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
255                    write_mask: wgpu::ColorWrites::ALL,
256                })],
257            }),
258            primitive: wgpu::PrimitiveState::default(),
259            depth_stencil: None,
260            multisample: wgpu::MultisampleState::default(),
261            multiview: None,
262        });
263
264        // Muspelheim Composite Pipeline (additive blend onto screen)
265        let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
266            label: Some("Muspelheim Composite"),
267            layout: Some(&pipeline_layout),
268            vertex: wgpu::VertexState {
269                module: &shader,
270                entry_point: "vs_fullscreen",
271                buffers: &[],
272            },
273            fragment: Some(wgpu::FragmentState {
274                module: &shader,
275                entry_point: "fs_composite",
276                targets: &[Some(wgpu::ColorTargetState {
277                    format: config.format,
278                    // Additive blend: src + dst — glow lights up the scene
279                    blend: Some(wgpu::BlendState {
280                        color: wgpu::BlendComponent {
281                            src_factor: wgpu::BlendFactor::One,
282                            dst_factor: wgpu::BlendFactor::One,
283                            operation: wgpu::BlendOperation::Add,
284                        },
285                        alpha: wgpu::BlendComponent {
286                            src_factor: wgpu::BlendFactor::One,
287                            dst_factor: wgpu::BlendFactor::One,
288                            operation: wgpu::BlendOperation::Add,
289                        },
290                    }),
291                    write_mask: wgpu::ColorWrites::ALL,
292                })],
293            }),
294            primitive: wgpu::PrimitiveState::default(),
295            depth_stencil: None,
296            multisample: wgpu::MultisampleState::default(),
297            multiview: None,
298        });
299
300        // Muspelheim Intermediate Textures
301        let blur_tex_desc = wgpu::TextureDescriptor {
302            label: Some("Muspelheim Intermediate"),
303            size: wgpu::Extent3d {
304                width: config.width,
305                height: config.height,
306                depth_or_array_layers: 1,
307            },
308            mip_level_count: 1,
309            sample_count: 1,
310            dimension: wgpu::TextureDimension::D2,
311            format: config.format,
312            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
313            view_formats: &[],
314        };
315        let blur_texture_a_obj = device.create_texture(&blur_tex_desc);
316        let blur_texture_b_obj = device.create_texture(&blur_tex_desc);
317        let blur_texture_a = blur_texture_a_obj.create_view(&wgpu::TextureViewDescriptor::default());
318        let blur_texture_b = blur_texture_b_obj.create_view(&wgpu::TextureViewDescriptor::default());
319
320        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
321            address_mode_u: wgpu::AddressMode::ClampToEdge,
322            address_mode_v: wgpu::AddressMode::ClampToEdge,
323            mag_filter: wgpu::FilterMode::Linear,
324            ..Default::default()
325        });
326
327        let blur_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
328            layout: &texture_bind_group_layout,
329            entries: &[
330                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&blur_texture_a) },
331                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
332            ],
333            label: Some("Blur Bind Group A"),
334        });
335
336        let blur_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
337            layout: &texture_bind_group_layout,
338            entries: &[
339                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&blur_texture_b) },
340                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
341            ],
342            label: Some("Blur Bind Group B"),
343        });
344
345        // Forge the Niflheim Dummy Texture (1x1 White)
346        let dummy_size = wgpu::Extent3d {
347            width: 1,
348            height: 1,
349            depth_or_array_layers: 1,
350        };
351        let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
352            label: Some("Niflheim Dummy Texture"),
353            size: dummy_size,
354            mip_level_count: 1,
355            sample_count: 1,
356            dimension: wgpu::TextureDimension::D2,
357            format: wgpu::TextureFormat::Rgba8UnormSrgb,
358            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
359            view_formats: &[],
360        });
361        queue.write_texture(
362            wgpu::ImageCopyTexture {
363                texture: &dummy_texture,
364                mip_level: 0,
365                origin: wgpu::Origin3d::ZERO,
366                aspect: wgpu::TextureAspect::All,
367            },
368            &[255, 255, 255, 255],
369            wgpu::ImageDataLayout {
370                offset: 0,
371                bytes_per_row: Some(4),
372                rows_per_image: Some(1),
373            },
374            dummy_size,
375        );
376
377        let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
378        let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
379            address_mode_u: wgpu::AddressMode::ClampToEdge,
380            address_mode_v: wgpu::AddressMode::ClampToEdge,
381            address_mode_w: wgpu::AddressMode::ClampToEdge,
382            mag_filter: wgpu::FilterMode::Linear,
383            min_filter: wgpu::FilterMode::Nearest,
384            mipmap_filter: wgpu::FilterMode::Nearest,
385            ..Default::default()
386        });
387
388        let dummy_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
389            layout: &texture_bind_group_layout,
390            entries: &[
391                wgpu::BindGroupEntry {
392                    binding: 0,
393                    resource: wgpu::BindingResource::TextureView(&dummy_view),
394                },
395                wgpu::BindGroupEntry {
396                    binding: 1,
397                    resource: wgpu::BindingResource::Sampler(&dummy_sampler),
398                },
399            ],
400            label: Some("Niflheim Dummy Bind Group"),
401        });
402
403        // Forge the Anvil (Buffers)
404        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
405            label: Some("Surtr Vertex Anvil"),
406            size: (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
407            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
408            mapped_at_creation: false,
409        });
410
411        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
412            label: Some("Surtr Index Anvil"),
413            size: (MAX_INDICES * std::mem::size_of::<u16>()) as u64,
414            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
415            mapped_at_creation: false,
416        });
417
418
419        Self {
420            device,
421            queue,
422            surface,
423            config,
424            pipeline,
425            bloom_extract_pipeline,
426            blur_h_pipeline,
427            blur_v_pipeline,
428            composite_pipeline,
429            blur_texture_a,
430            blur_texture_b,
431            blur_bind_group_a,
432            blur_bind_group_b,
433            font_system: cosmic_text::FontSystem::new(),
434            swash_cache: cosmic_text::SwashCache::new(),
435            dummy_bind_group,
436            vertex_buffer,
437            index_buffer,
438            vertices: Vec::with_capacity(MAX_VERTICES),
439            indices: Vec::with_capacity(MAX_INDICES),
440        }
441    }
442
443    /// Strike the flaming sword (begin frame).
444    pub fn begin_frame(&mut self) -> wgpu::CommandEncoder {
445        self.vertices.clear();
446        self.indices.clear();
447        self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
448            label: Some("Surtr's Flaming Sword"),
449        })
450    }
451
452    /// Quench the blade — submits the full Muspelheim multi-pass effect:
453    ///   Pass 1  — Base geometry → screen (clear + all solid/glow rects)
454    ///   Pass 2  — Bloom extract from screen → tex_a (threshold bright pixels)
455    ///   Pass 3–6 — Ping-pong blur (H→B, V→A, H→B, V→A) — 4 iters
456    ///   Pass 7  — Additive composite  tex_a → screen  (glow on top)
457    pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
458        self.queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
459        self.queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&self.indices));
460
461        let frame = self.surface.get_current_texture()
462            .expect("Surtr: failed to acquire surface texture");
463        let screen = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
464
465        // ── Pass 1: Base scene → screen ─────────────────────────────────────
466        {
467            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
468                label: Some("Surtr P1 Base"),
469                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
470                    view: &screen,
471                    resolve_target: None,
472                    ops: wgpu::Operations {
473                        load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }), // Ginnungagap
474                        store: wgpu::StoreOp::Store,
475                    },
476                })],
477                depth_stencil_attachment: None,
478                occlusion_query_set: None,
479                timestamp_writes: None,
480            });
481            if !self.indices.is_empty() {
482                p.set_pipeline(&self.pipeline);
483                p.set_bind_group(0, &self.dummy_bind_group, &[]);
484                p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
485                p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
486                p.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
487            }
488        }
489
490        // ── Pass 2: Bloom extract  screen → tex_a ────────────────────────────
491        // Re-render geometry into tex_a at amplified brightness so the blur
492        // has a source to operate on. fs_bloom_extract thresholds bright pixels.
493        {
494            let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
495                label: Some("Surtr P2 Bloom Src"),
496                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
497                    view: &self.blur_texture_a,
498                    resolve_target: None,
499                    ops: wgpu::Operations {
500                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
501                        store: wgpu::StoreOp::Store,
502                    },
503                })],
504                depth_stencil_attachment: None,
505                occlusion_query_set: None,
506                timestamp_writes: None,
507            });
508            if !self.indices.is_empty() {
509                p.set_pipeline(&self.pipeline);
510                p.set_bind_group(0, &self.dummy_bind_group, &[]);
511                p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
512                p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
513                p.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
514            }
515        }
516
517        // ── Passes 3–6+: Ping-pong Gaussian blur (6 iterations) ──────────
518        // 6 H+V pairs with a 5-tap kernel gives ~30px soft glow halo.
519        let blur_iters: u32 = 6;
520        for i in 0..blur_iters {
521            // H pass: tex_a → tex_b
522            {
523                let label = format!("Surtr Blur H iter {}", i);
524                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
525                    label: Some(&label),
526                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
527                        view: &self.blur_texture_b,
528                        resolve_target: None,
529                        ops: wgpu::Operations {
530                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
531                            store: wgpu::StoreOp::Store,
532                        },
533                    })],
534                    depth_stencil_attachment: None,
535                    occlusion_query_set: None,
536                    timestamp_writes: None,
537                });
538                p.set_pipeline(&self.blur_h_pipeline);
539                p.set_bind_group(0, &self.blur_bind_group_a, &[]);
540                p.draw(0..3, 0..1);
541            }
542            // V pass: tex_b → tex_a
543            {
544                let label = format!("Surtr Blur V iter {}", i);
545                let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
546                    label: Some(&label),
547                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
548                        view: &self.blur_texture_a,
549                        resolve_target: None,
550                        ops: wgpu::Operations {
551                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
552                            store: wgpu::StoreOp::Store,
553                        },
554                    })],
555                    depth_stencil_attachment: None,
556                    occlusion_query_set: None,
557                    timestamp_writes: None,
558                });
559                p.set_pipeline(&self.blur_v_pipeline);
560                p.set_bind_group(0, &self.blur_bind_group_b, &[]);
561                p.draw(0..3, 0..1);
562            }
563        }
564
565        // ── Pass 7: Additive composite  tex_a → screen ──────────────────────
566        // LoadOp::Load keeps the intact base scene; additive blend adds glow.
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    /// Forge a rectangle into vertices
592    pub fn fill_rect(&mut self, rect: Rect, color: [f32; 4], mode: u32) {
593        let base_idx = self.vertices.len() as u16;
594        
595        // Normalize coordinates to NDC [-1, 1] (Assuming 800x600 for now, should use config)
596        let x1 = (rect.x / 400.0) - 1.0;
597        let y1 = 1.0 - (rect.y / 300.0);
598        let x2 = ((rect.x + rect.width) / 400.0) - 1.0;
599        let y2 = 1.0 - ((rect.y + rect.height) / 300.0);
600
601        self.vertices.push(Vertex { position: [x1, y1], uv: [0.0, 0.0], color, mode });
602        self.vertices.push(Vertex { position: [x2, y1], uv: [1.0, 0.0], color, mode });
603        self.vertices.push(Vertex { position: [x2, y2], uv: [1.0, 1.0], color, mode });
604        self.vertices.push(Vertex { position: [x1, y2], uv: [0.0, 1.0], color, mode });
605
606        self.indices.extend_from_slice(&[
607            base_idx, base_idx + 1, base_idx + 2,
608            base_idx, base_idx + 2, base_idx + 3,
609        ]);
610    }
611}