Skip to main content

cvkg_render_gpu/passes/
skinning.rs

1// Pass: skinning.rs
2// Purpose: Compute-skinning dispatch pass resolving bone matrices and morph target blend shapes.
3
4use crate::kvasir::nodes::PassId;
5use crate::kvasir::{ExecutionContext, KvasirNode, ResourceId};
6
7/// Node representing the GPU compute skinning pass.
8pub struct SkinningNode {
9    /// Inputs slice satisfying KvasirNode lifetime requirements.
10    pub inputs: Vec<ResourceId>,
11    /// Outputs slice.
12    pub outputs: Vec<ResourceId>,
13}
14
15impl KvasirNode for SkinningNode {
16    fn label(&self) -> &'static str {
17        "SkinningComputePass"
18    }
19
20    fn inputs(&self) -> &[ResourceId] {
21        &self.inputs
22    }
23
24    fn outputs(&self) -> &[ResourceId] {
25        &self.outputs
26    }
27
28    fn pass_id(&self) -> PassId {
29        PassId::ComputeSkinning
30    }
31
32    fn execute(&self, ctx: &mut ExecutionContext) {
33        if ctx.renderer.skinning_buffer_pairs.is_empty() {
34            return;
35        }
36
37        let total_vertices: u32 = ctx.renderer.skinning_buffer_pairs.iter()
38            .map(|(src, _)| {
39                // Each src buffer contains SkinnedVertex records; compute size from buffer size
40                (src.size() as usize / std::mem::size_of::<crate::vertex::SkinnedVertex>()) as u32
41            })
42            .sum();
43
44        tracing::debug!(
45            "SkinningNode::execute - Dispatching GPU skinning compute shader for {} meshes ({} total vertices)",
46            ctx.renderer.skinning_buffer_pairs.len(),
47            total_vertices
48        );
49
50        // 1. Create Bind Groups for joint matrices and morphs (shared across all mesh dispatches)
51        let bind_group_1 = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
52            label: Some("Skinning Bind Group 1 (Joints)"),
53            layout: &ctx.renderer.skinning_bgl1,
54            entries: &[
55                wgpu::BindGroupEntry {
56                    binding: 0,
57                    resource: ctx.renderer.skinning_joint_matrices.as_entire_binding(),
58                },
59            ],
60        });
61
62        let bind_group_2 = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
63            label: Some("Skinning Bind Group 2 (Morphs)"),
64            layout: &ctx.renderer.skinning_bgl2,
65            entries: &[
66                wgpu::BindGroupEntry {
67                    binding: 0,
68                    resource: ctx.renderer.skinning_morph_positions.as_entire_binding(),
69                },
70                wgpu::BindGroupEntry {
71                    binding: 1,
72                    resource: ctx.renderer.skinning_morph_weights.as_entire_binding(),
73                },
74            ],
75        });
76
77        // 2. Begin compute pass and dispatch workgroups for each mesh
78        let mut compute_pass = ctx.encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
79            label: Some("Skeletal Skinning Compute Pass"),
80            timestamp_writes: None,
81        });
82
83        compute_pass.set_pipeline(&ctx.renderer.skinning_compute_pipeline);
84        compute_pass.set_bind_group(1, &bind_group_1, &[]);
85        compute_pass.set_bind_group(2, &bind_group_2, &[]);
86
87        for (src_buffer, dst_buffer) in ctx.renderer.skinning_buffer_pairs.iter() {
88            let vertex_count = (src_buffer.size() as usize / std::mem::size_of::<crate::vertex::SkinnedVertex>()) as u32;
89            if vertex_count == 0 {
90                continue;
91            }
92
93            // Create per-mesh bind group for src/dst buffers
94            let bind_group_0 = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
95                label: Some("Skinning Bind Group 0 (Geometry)"),
96                layout: &ctx.renderer.skinning_bgl0,
97                entries: &[
98                    wgpu::BindGroupEntry {
99                        binding: 0,
100                        resource: src_buffer.as_entire_binding(),
101                    },
102                    wgpu::BindGroupEntry {
103                        binding: 1,
104                        resource: dst_buffer.as_entire_binding(),
105                    },
106                ],
107            });
108
109            compute_pass.set_bind_group(0, &bind_group_0, &[]);
110
111            let workgroup_count = (vertex_count + 63) / 64;
112            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
113        }
114
115        // NOTE: skinning_buffer_pairs is cleared by the frame loop after all nodes execute,
116        // not here, because ExecutionContext.renderer is immutable during node execution.
117    }
118}