Skip to main content

wisp/render/
mesh_pipeline.rs

1//! Mesh pipeline — perspective-rotated textured quad (M0.19).
2
3use std::collections::{HashMap, HashSet};
4
5use bytemuck::{Pod, Zeroable};
6use wgpu::util::DeviceExt;
7
8use crate::application::Application;
9use crate::blend::BlendMode;
10use crate::render::blend_pipeline::BlendPipelineMap;
11use crate::render::scene_walk::walk_visible_subtree;
12use crate::scene::{Node, NodeId, Stage};
13use crate::texture::Texture;
14
15#[repr(C)]
16#[derive(Clone, Copy, Pod, Zeroable)]
17pub(crate) struct MeshInstance {
18    pub model: [[f32; 4]; 4],
19    pub tint: [f32; 4],
20    pub rotation_y: f32,
21    pub persp_strength: f32,
22    pub _pad: [f32; 2],
23}
24
25const ATTR_LAYOUT: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![
26    0 => Float32x4,
27    1 => Float32x4,
28    2 => Float32x4,
29    3 => Float32x4,
30    4 => Float32x4,
31    5 => Float32,
32    6 => Float32,
33];
34
35pub(crate) struct MeshPipeline {
36    pipelines: BlendPipelineMap,
37    texture_layout: wgpu::BindGroupLayout,
38}
39
40impl MeshPipeline {
41    pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
42        let device = app.device();
43        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
44            label: Some("wisp::mesh_perspective"),
45            source: wgpu::ShaderSource::Wgsl(
46                include_str!("../../shaders/mesh_perspective.wgsl").into(),
47            ),
48        });
49
50        let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
51            label: Some("wisp::mesh texture layout"),
52            entries: &[
53                wgpu::BindGroupLayoutEntry {
54                    binding: 0,
55                    visibility: wgpu::ShaderStages::FRAGMENT,
56                    ty: wgpu::BindingType::Texture {
57                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
58                        view_dimension: wgpu::TextureViewDimension::D2,
59                        multisampled: false,
60                    },
61                    count: None,
62                },
63                wgpu::BindGroupLayoutEntry {
64                    binding: 1,
65                    visibility: wgpu::ShaderStages::FRAGMENT,
66                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
67                    count: None,
68                },
69            ],
70        });
71
72        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
73            label: Some("wisp::mesh pipeline layout"),
74            bind_group_layouts: &[&texture_layout],
75            push_constant_ranges: &[],
76        });
77
78        let pipelines = BlendPipelineMap::new(|blend| {
79            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
80                label: Some("wisp::mesh pipeline"),
81                layout: Some(&pipeline_layout),
82                vertex: wgpu::VertexState {
83                    module: &shader,
84                    entry_point: Some("main_vs"),
85                    buffers: &[wgpu::VertexBufferLayout {
86                        array_stride: std::mem::size_of::<MeshInstance>() as wgpu::BufferAddress,
87                        step_mode: wgpu::VertexStepMode::Instance,
88                        attributes: &ATTR_LAYOUT,
89                    }],
90                    compilation_options: wgpu::PipelineCompilationOptions::default(),
91                },
92                fragment: Some(wgpu::FragmentState {
93                    module: &shader,
94                    entry_point: Some("main_fs"),
95                    targets: &[Some(wgpu::ColorTargetState {
96                        format: output_format,
97                        blend: Some(blend),
98                        write_mask: wgpu::ColorWrites::ALL,
99                    })],
100                    compilation_options: wgpu::PipelineCompilationOptions::default(),
101                }),
102                primitive: wgpu::PrimitiveState::default(),
103                depth_stencil: None,
104                multisample: wgpu::MultisampleState::default(),
105                multiview: None,
106                cache: None,
107            })
108        });
109
110        Self {
111            pipelines,
112            texture_layout,
113        }
114    }
115
116    /// Walk `stage`, batch mesh instances per shared texture, draw.
117    pub(crate) fn draw_stage(
118        &self,
119        app: &Application,
120        pass: &mut wgpu::RenderPass<'_>,
121        stage: &Stage,
122    ) -> (u32, u32) {
123        self.draw_subtree(app, pass, stage, stage.root(), &HashSet::new())
124    }
125
126    /// Subtree variant — see `SpritePipeline::draw_subtree`.
127    pub(crate) fn draw_subtree(
128        &self,
129        app: &Application,
130        pass: &mut wgpu::RenderPass<'_>,
131        stage: &Stage,
132        start: NodeId,
133        exclude: &HashSet<NodeId>,
134    ) -> (u32, u32) {
135        let batches = collect_batches(stage, start, exclude);
136        let mut draw_calls = 0u32;
137        let mut meshes = 0u32;
138
139        for batch in &batches {
140            if batch.instances.is_empty() {
141                continue;
142            }
143            let count = u32::try_from(batch.instances.len()).expect("mesh count fits in u32");
144            meshes = meshes.saturating_add(count);
145
146            let buffer = app
147                .device()
148                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
149                    label: Some("wisp::mesh instances"),
150                    contents: bytemuck::cast_slice(&batch.instances),
151                    usage: wgpu::BufferUsages::VERTEX,
152                });
153            let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
154                label: Some("wisp::mesh texture bg"),
155                layout: &self.texture_layout,
156                entries: &[
157                    wgpu::BindGroupEntry {
158                        binding: 0,
159                        resource: wgpu::BindingResource::TextureView(batch.texture.view()),
160                    },
161                    wgpu::BindGroupEntry {
162                        binding: 1,
163                        resource: wgpu::BindingResource::Sampler(batch.texture.sampler()),
164                    },
165                ],
166            });
167
168            pass.set_pipeline(self.pipelines.get(batch.blend_mode));
169            pass.set_bind_group(0, &bg, &[]);
170            pass.set_vertex_buffer(0, buffer.slice(..));
171            pass.draw(0..6, 0..count);
172            draw_calls += 1;
173        }
174
175        (draw_calls, meshes)
176    }
177}
178
179struct Batch {
180    texture: Texture,
181    blend_mode: BlendMode,
182    instances: Vec<MeshInstance>,
183}
184
185fn collect_batches(stage: &Stage, start: NodeId, exclude: &HashSet<NodeId>) -> Vec<Batch> {
186    type Key = (usize, BlendMode);
187    let mut grouped: HashMap<Key, (Texture, BlendMode, Vec<MeshInstance>)> = HashMap::new();
188    let mut order: Vec<Key> = Vec::new();
189
190    walk_visible_subtree(stage, start, exclude, |_id, node, world| {
191        let container = node.container();
192        if let Node::Mesh(mesh) = node {
193            let mode = container.blend_mode;
194            let key = (mesh.texture.id(), mode);
195            let entry = grouped.entry(key).or_insert_with(|| {
196                order.push(key);
197                (mesh.texture.clone(), mode, Vec::new())
198            });
199            entry.2.push(MeshInstance {
200                model: world.to_cols_array_2d(),
201                tint: [
202                    mesh.tint.r,
203                    mesh.tint.g,
204                    mesh.tint.b,
205                    mesh.tint.a * container.alpha,
206                ],
207                rotation_y: mesh.rotation_y,
208                persp_strength: mesh.perspective_strength,
209                _pad: [0.0, 0.0],
210            });
211        }
212    });
213
214    order
215        .into_iter()
216        .filter_map(|key| {
217            grouped
218                .remove(&key)
219                .map(|(texture, blend_mode, instances)| Batch {
220                    texture,
221                    blend_mode,
222                    instances,
223                })
224        })
225        .collect()
226}