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#![allow(
25    clippy::type_complexity,
26    clippy::unwrap_or_default,
27    dead_code,
28    unused_variables,
29    unused_imports,
30    unused_mut,
31    unused_parens
32)]
33
34pub mod error;
35pub mod kvasir;
36mod material;
37
38// Re-export material types for downstream users
39pub use material::builtins;
40pub use material::{CompiledMaterial, MaterialCompiler, MaterialError, MaterialGraph, MaterialOp};
41
42pub mod accessibility;
43pub mod ai;
44mod api;
45pub mod draw;
46pub mod filter;
47pub mod passes;
48pub mod pyramid;
49pub mod renderer;
50mod surtr_util;
51pub mod types;
52pub mod vertex;
53
54pub mod heim;
55pub use heim::SkylinePacker;
56
57// P1-1 (phase 6): subsystems module. Each subsystem (config,
58// geometry, text, svg, particles) is a self-contained module
59// that can be tested, reviewed, and modified in isolation.
60pub mod subsystems;
61pub use subsystems::RendererConfig;
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    use super::heim::SkylinePacker;
68
69    #[test]
70    fn test_shelf_packer_basic() {
71        let mut packer = SkylinePacker::new(100, 100);
72        assert_eq!(packer.pack(10, 10), Some((0, 0)));
73        assert_eq!(packer.pack(20, 15), Some((10, 0)));
74    }
75
76    #[test]
77    fn test_shelf_packer_wrap() {
78        let mut packer = SkylinePacker::new(100, 100);
79        packer.pack(60, 10);
80        assert_eq!(packer.pack(50, 20), Some((0, 10)));
81    }
82
83    #[test]
84    fn test_parse_svg_animations() {
85        let svg = r##"
86            <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
87                <g id="spinner">
88                    <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="2s" />
89                </g>
90                <circle id="pulse">
91                    <animate attributeName="opacity" from="0.5" to="1.0" dur="0.5s" />
92                </circle>
93                <!-- Edge cases: xlink:href, ms suffix, values list -->
94                <rect>
95                    <animate xlink:href="#myRect" attributeName="x" values="10; 20; 30" dur="500ms" />
96                </rect>
97            </svg>
98        "##;
99        let anims = draw::parse_svg_animations(svg.as_bytes());
100        assert_eq!(anims.len(), 3);
101
102        assert_eq!(anims[0].target_id, "spinner");
103        assert_eq!(anims[0].keyframe_values, vec![0.0, 360.0]);
104
105        assert_eq!(anims[1].target_id, "pulse");
106        assert_eq!(anims[1].attribute_name, "opacity");
107        assert_eq!(anims[1].duration, 0.5);
108        assert_eq!(anims[1].keyframe_values, vec![0.5, 1.0]);
109
110        assert_eq!(anims[2].target_id, "myRect");
111        assert_eq!(anims[2].attribute_name, "x");
112        assert_eq!(anims[2].duration, 0.5); // 500ms parsed as 0.5
113        assert_eq!(anims[2].keyframe_values, vec![10.0, 20.0, 30.0]);
114    }
115
116    #[test]
117    fn test_shelf_packer_full() {
118        let mut packer = SkylinePacker::new(10, 10);
119        assert_eq!(packer.pack(11, 5), None);
120        assert_eq!(packer.pack(5, 11), None);
121    }
122}
123
124// P1-12 fix: on wasm32/WebGL2, texture binding arrays are not supported.
125// The bind group layout uses count: None (single texture) on WASM, so the
126// WGSL must declare t_diffuse as a single texture, not a binding_array.
127// We swap the three affected WGSL files (common, material_opaque, bloom)
128// to WASM-specific variants on wasm32 targets. All other shader files
129// (shapes, material_glass, bifrost, color_blind, tonemap, particles) are
130// the same on both targets.
131#[cfg(target_arch = "wasm32")]
132pub(crate) const WGSL_COMMON: &str = include_str!("shaders/common_wasm.wgsl");
133#[cfg(not(target_arch = "wasm32"))]
134pub(crate) const WGSL_COMMON: &str = include_str!("shaders/common.wgsl");
135
136pub(crate) const WGSL_SHAPES: &str = include_str!("shaders/shapes.wgsl");
137
138#[cfg(target_arch = "wasm32")]
139pub(crate) const WGSL_MATERIAL_OPAQUE: &str = include_str!("shaders/material_opaque_wasm.wgsl");
140#[cfg(not(target_arch = "wasm32"))]
141pub(crate) const WGSL_MATERIAL_OPAQUE: &str = include_str!("shaders/material_opaque.wgsl");
142
143pub(crate) const WGSL_MATERIAL_GLASS: &str = include_str!("shaders/material_glass.wgsl");
144pub(crate) const WGSL_MATERIAL_PBR: &str = include_str!("shaders/material_pbr.wgsl");
145pub(crate) const WGSL_MATERIAL_SHADOW: &str = include_str!("shaders/material_shadow.wgsl");
146pub(crate) const WGSL_BIFROST: &str = include_str!("shaders/bifrost.wgsl");
147
148#[cfg(target_arch = "wasm32")]
149pub(crate) const WGSL_BLOOM: &str = include_str!("shaders/bloom_wasm.wgsl");
150#[cfg(not(target_arch = "wasm32"))]
151pub(crate) const WGSL_BLOOM: &str = include_str!("shaders/bloom.wgsl");
152
153pub(crate) const WGSL_COLOR_BLIND: &str = include_str!("shaders/color_blind.wgsl");
154pub(crate) const WGSL_TONEMAP: &str = include_str!("shaders/tonemap.wgsl");
155pub(crate) const WGSL_PARTICLES: &str = include_str!("shaders/particles.wgsl");
156pub(crate) const WGSL_SKINNING: &str = include_str!("shaders/skinning.wgsl");
157
158pub mod color_blindness;
159
160// Re-export ColorBlindMode for downstream users
161pub use color_blindness::ColorBlindMode;
162
163// ShieldWall -- re-export AccessKit types so callers can build tree updates
164// without depending on accesskit directly.
165pub use accesskit::{
166    ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Node, NodeId, Role, Tree,
167    TreeId, TreeUpdate,
168};
169pub use accesskit_winit::Adapter as ShieldWallAdapter;
170
171// Re-export ColorTheme and SceneUniforms for cvkg-render-gpu users
172pub use cvkg_core::{ColorTheme, SceneUniforms};
173
174pub use renderer::GpuRenderer;
175
176// P1-35: SVG filter graph integration (moved to filter/ module)
177pub use types::{SvgAnimation, SvgModel};
178pub use vertex::{InstanceData, InstanceData3D, Vertex};
179
180/// Re-export PassNode for convenience.
181pub use cvkg_core::PassNode;
182
183/// Frame manifest for cvkg-render-gpu.
184/// Contributes: Render + Composite phases.
185pub const MANIFEST: cvkg_core::FrameManifest = cvkg_core::FrameManifest {
186    phase_contributions: &[
187        cvkg_core::FramePhase::Render,
188        cvkg_core::FramePhase::Composite,
189    ],
190    pass_nodes: &[
191        cvkg_core::PassNodeDescriptor {
192            id: "geometry",
193            label: "Geometry Pass (Opaque)",
194            inputs: &[],
195            outputs: &["scene_color", "scene_depth"],
196            after: &[],
197            constructor: || -> Box<dyn cvkg_core::PassNode> {
198                Box::new(crate::passes::geometry::GeometryNode::new())
199            },
200        },
201        cvkg_core::PassNodeDescriptor {
202            id: "shadow",
203            label: "Shadow Pass (Depth-Only)",
204            inputs: &[],
205            outputs: &["shadow_map"],
206            after: &[],
207            constructor: || -> Box<dyn cvkg_core::PassNode> {
208                Box::new(crate::passes::shadow::ShadowNode {
209                    light: crate::passes::shadow::DirectionalLight::default(),
210                    shadow_map: crate::kvasir::ResourceId(0),
211                    mesh_instances: Vec::new(),
212                    cascade_splits: [8.0, 25.0, 70.0, 200.0],
213                    camera_view_proj: glam::Mat4::IDENTITY,
214                })
215            },
216        },
217        cvkg_core::PassNodeDescriptor {
218            id: "opaque3d",
219            label: "Opaque 3D Pass (PBR + Shadows)",
220            inputs: &["shadow_map"],
221            outputs: &["scene_color"],
222            after: &["shadow"],
223            constructor: || -> Box<dyn cvkg_core::PassNode> {
224                Box::new(crate::passes::opaque3d::Opaque3dNode {
225                    mesh_instances: Vec::new(),
226                    light: crate::passes::shadow::DirectionalLight::default(),
227                    shadow_map: crate::kvasir::ResourceId(0),
228                })
229            },
230        },
231        cvkg_core::PassNodeDescriptor {
232            id: "ui",
233            label: "UI Compositing",
234            inputs: &["scene_color", "scene_depth"],
235            outputs: &["ui_output"],
236            after: &["geometry"],
237            constructor: || -> Box<dyn cvkg_core::PassNode> {
238                Box::new(crate::passes::ui::UINode::new())
239            },
240        },
241        cvkg_core::PassNodeDescriptor {
242            id: "bloom_extract",
243            label: "Bloom Extract",
244            inputs: &["ui_output"],
245            outputs: &["bloom_src"],
246            after: &["ui"],
247            constructor: || -> Box<dyn cvkg_core::PassNode> {
248                Box::new(crate::passes::bloom::BloomExtractNode::new())
249            },
250        },
251        cvkg_core::PassNodeDescriptor {
252            id: "volumetric",
253            label: "Volumetric Raymarching",
254            inputs: &["scene_color"],
255            outputs: &["scene_color"],
256            after: &["geometry"],
257            constructor: || -> Box<dyn cvkg_core::PassNode> {
258                Box::new(crate::passes::volumetric::VolumetricNode::new())
259            },
260        },
261        cvkg_core::PassNodeDescriptor {
262            id: "accessibility",
263            label: "Accessibility Transform",
264            inputs: &["scene_color"],
265            outputs: &["scene_color"],
266            after: &["bloom_extract"],
267            constructor: || -> Box<dyn cvkg_core::PassNode> {
268                Box::new(crate::passes::accessibility::AccessibilityNode::new())
269            },
270        },
271    ],
272    time_budget_requests: &[
273        cvkg_core::TimeBudgetRequest {
274            phase: cvkg_core::FramePhase::Render,
275            time_slice_us: 8000,
276            skippable: false,
277            name: "render_gpu",
278        },
279        cvkg_core::TimeBudgetRequest {
280            phase: cvkg_core::FramePhase::Composite,
281            time_slice_us: 4000,
282            skippable: false,
283            name: "composite_gpu",
284        },
285    ],
286};