Skip to main content

lumen_engine_gpu/
renderer.rs

1use std::{collections::HashMap, env, sync::Arc};
2
3use anyhow::{Context, Result, anyhow, bail};
4
5use crate::{
6    BindGroupLayoutSpec, Binding, BindingResource, BufferDesc, BufferId, BufferResource,
7    ComputeDispatch, ComputePassDesc, ComputeProgramDesc, CopyTextureDesc, DrawCommand,
8    FrameUpdate, LoadOp, PassDesc, Program, ProgramDesc, ProgramId, RenderPassDesc, RenderPlan,
9    RenderProgramDesc, SamplerId, TextureDesc, TextureId, TextureResource, Upload,
10};
11
12struct RuntimeTexture {
13    texture: Arc<wgpu::Texture>,
14    view: wgpu::TextureView,
15    desc: TextureDesc,
16}
17
18struct RuntimeBuffer {
19    buffer: wgpu::Buffer,
20    desc: BufferDesc,
21}
22
23struct RuntimeSampler {
24    sampler: wgpu::Sampler,
25}
26
27struct RuntimeRenderProgram {
28    pipeline: wgpu::RenderPipeline,
29    bind_group_layouts: Vec<wgpu::BindGroupLayout>,
30}
31
32struct RuntimeComputeProgram {
33    pipeline: wgpu::ComputePipeline,
34    bind_group_layouts: Vec<wgpu::BindGroupLayout>,
35}
36
37enum RuntimeProgram {
38    Render(RuntimeRenderProgram),
39    Compute(RuntimeComputeProgram),
40}
41
42pub struct Renderer {
43    pub device: wgpu::Device,
44    pub queue: wgpu::Queue,
45    adapter_info: GpuAdapterInfo,
46    textures: Vec<RuntimeTexture>,
47    buffers: Vec<RuntimeBuffer>,
48    samplers: Vec<RuntimeSampler>,
49    programs: Vec<RuntimeProgram>,
50}
51
52#[derive(Clone)]
53pub struct ExternalTexture {
54    texture: Arc<wgpu::Texture>,
55    desc: TextureDesc,
56}
57
58impl std::fmt::Debug for ExternalTexture {
59    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        formatter
61            .debug_struct("ExternalTexture")
62            .field("desc", &self.desc)
63            .finish_non_exhaustive()
64    }
65}
66
67impl ExternalTexture {
68    pub fn new(texture: Arc<wgpu::Texture>, desc: TextureDesc) -> Self {
69        Self { texture, desc }
70    }
71
72    pub fn from_texture(texture: wgpu::Texture, desc: TextureDesc) -> Self {
73        Self::new(Arc::new(texture), desc)
74    }
75
76    pub fn texture(&self) -> &wgpu::Texture {
77        &self.texture
78    }
79
80    pub fn texture_arc(&self) -> Arc<wgpu::Texture> {
81        Arc::clone(&self.texture)
82    }
83
84    pub fn desc(&self) -> TextureDesc {
85        self.desc
86    }
87}
88
89#[derive(Debug, Clone)]
90pub struct SubmittedExternalTexture {
91    submission: wgpu::SubmissionIndex,
92    texture: Arc<wgpu::Texture>,
93}
94
95impl SubmittedExternalTexture {
96    pub fn submission(&self) -> wgpu::SubmissionIndex {
97        self.submission.clone()
98    }
99
100    pub fn texture(&self) -> &wgpu::Texture {
101        &self.texture
102    }
103
104    pub fn wait(&self, device: &wgpu::Device) -> Result<()> {
105        device
106            .poll(wgpu::PollType::Wait {
107                submission_index: Some(self.submission.clone()),
108                timeout: None,
109            })
110            .context("wait for external texture submission")?;
111        Ok(())
112    }
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct GpuAdapterInfo {
117    pub name: String,
118    pub backend: wgpu::Backend,
119    pub device_type: wgpu::DeviceType,
120    pub vendor: u32,
121    pub device: u32,
122    pub driver: String,
123    pub driver_info: String,
124}
125
126impl From<wgpu::AdapterInfo> for GpuAdapterInfo {
127    fn from(info: wgpu::AdapterInfo) -> Self {
128        Self {
129            name: info.name,
130            backend: info.backend,
131            device_type: info.device_type,
132            vendor: info.vendor,
133            device: info.device,
134            driver: info.driver,
135            driver_info: info.driver_info,
136        }
137    }
138}
139
140impl Renderer {
141    pub async fn new() -> Result<Self> {
142        let instance =
143            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
144        let power_preference =
145            wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
146        let force_fallback_adapter = env_flag("LUMEN_GPU_FORCE_FALLBACK_ADAPTER");
147        let adapter = instance
148            .request_adapter(&wgpu::RequestAdapterOptions {
149                power_preference,
150                compatible_surface: None,
151                force_fallback_adapter,
152            })
153            .await
154            .context("no compatible wgpu adapter")?;
155        let adapter_info = GpuAdapterInfo::from(adapter.get_info());
156        tracing::info!(
157            target: "lumen_gpu",
158            adapter = %adapter_info.name,
159            backend = ?adapter_info.backend,
160            device_type = ?adapter_info.device_type,
161            vendor = adapter_info.vendor,
162            device = adapter_info.device,
163            driver = %adapter_info.driver,
164            driver_info = %adapter_info.driver_info,
165            "selected wgpu adapter"
166        );
167        let (device, queue) = adapter
168            .request_device(&wgpu::DeviceDescriptor::default())
169            .await
170            .context("create wgpu device")?;
171        Ok(Self::from_device_with_adapter_info(
172            device,
173            queue,
174            adapter_info,
175        ))
176    }
177
178    pub fn from_device(device: wgpu::Device, queue: wgpu::Queue) -> Self {
179        Self::from_device_with_adapter_info(
180            device,
181            queue,
182            GpuAdapterInfo {
183                name: "external device".to_string(),
184                backend: wgpu::Backend::Noop,
185                device_type: wgpu::DeviceType::Other,
186                vendor: 0,
187                device: 0,
188                driver: "external".to_string(),
189                driver_info: "created outside lumen-gpu".to_string(),
190            },
191        )
192    }
193
194    pub fn from_device_with_adapter_info(
195        device: wgpu::Device,
196        queue: wgpu::Queue,
197        adapter_info: GpuAdapterInfo,
198    ) -> Self {
199        Self {
200            device,
201            queue,
202            adapter_info,
203            textures: Vec::new(),
204            buffers: Vec::new(),
205            samplers: Vec::new(),
206            programs: Vec::new(),
207        }
208    }
209
210    pub fn adapter_info(&self) -> &GpuAdapterInfo {
211        &self.adapter_info
212    }
213
214    pub fn prepare_plan(&mut self, plan: &RenderPlan) -> Result<()> {
215        tracing::debug!(
216            target: "lumen_gpu",
217            textures = plan.textures.len(),
218            buffers = plan.buffers.len(),
219            samplers = plan.samplers.len(),
220            programs = plan.programs.len(),
221            passes = plan.passes.len(),
222            "prepare render plan"
223        );
224        self.textures = plan
225            .textures
226            .iter()
227            .map(|resource| self.create_texture(resource))
228            .collect::<Result<Vec<_>>>()?;
229        self.buffers = plan
230            .buffers
231            .iter()
232            .map(|resource| self.create_buffer(resource))
233            .collect();
234        self.samplers = plan
235            .samplers
236            .iter()
237            .map(|resource| RuntimeSampler {
238                sampler: self.device.create_sampler(&resource.desc),
239            })
240            .collect();
241        self.programs = plan
242            .programs
243            .iter()
244            .map(|program| self.create_program(program))
245            .collect::<Result<Vec<_>>>()?;
246        Ok(())
247    }
248
249    pub fn texture_view(&self, id: TextureId) -> Option<&wgpu::TextureView> {
250        self.textures
251            .get(id.0 as usize)
252            .map(|texture| &texture.view)
253    }
254
255    pub fn texture(&self, id: TextureId) -> Option<&wgpu::Texture> {
256        self.textures
257            .get(id.0 as usize)
258            .map(|texture| texture.texture.as_ref())
259    }
260
261    pub fn texture_arc(&self, id: TextureId) -> Option<Arc<wgpu::Texture>> {
262        self.textures
263            .get(id.0 as usize)
264            .map(|texture| Arc::clone(&texture.texture))
265    }
266
267    pub fn texture_desc(&self, id: TextureId) -> Option<TextureDesc> {
268        self.textures.get(id.0 as usize).map(|texture| texture.desc)
269    }
270
271    pub fn replace_texture(
272        &mut self,
273        id: TextureId,
274        texture: wgpu::Texture,
275        desc: TextureDesc,
276    ) -> Result<Arc<wgpu::Texture>> {
277        self.replace_texture_arc(id, Arc::new(texture), desc)
278    }
279
280    pub fn replace_texture_arc(
281        &mut self,
282        id: TextureId,
283        texture: Arc<wgpu::Texture>,
284        desc: TextureDesc,
285    ) -> Result<Arc<wgpu::Texture>> {
286        let runtime = self
287            .textures
288            .get_mut(id.0 as usize)
289            .ok_or_else(|| anyhow!("unknown texture id {id:?}"))?;
290        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
291        let old = std::mem::replace(&mut runtime.texture, texture);
292        runtime.view = view;
293        runtime.desc = desc;
294        Ok(old)
295    }
296
297    pub fn replace_texture_discard_old(
298        &mut self,
299        id: TextureId,
300        texture: wgpu::Texture,
301        desc: TextureDesc,
302    ) -> Result<()> {
303        let _ = self.replace_texture(id, texture, desc)?;
304        Ok(())
305    }
306
307    pub fn copy_texture_to_external(
308        &self,
309        source_id: TextureId,
310        destination: &wgpu::Texture,
311    ) -> Result<wgpu::SubmissionIndex> {
312        let source = self.runtime_texture(source_id)?;
313        let mut encoder = self
314            .device
315            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
316                label: Some("lumen-gpu external texture copy"),
317            });
318        encoder.copy_texture_to_texture(
319            wgpu::TexelCopyTextureInfo {
320                texture: &source.texture,
321                mip_level: 0,
322                origin: wgpu::Origin3d::ZERO,
323                aspect: wgpu::TextureAspect::All,
324            },
325            wgpu::TexelCopyTextureInfo {
326                texture: destination,
327                mip_level: 0,
328                origin: wgpu::Origin3d::ZERO,
329                aspect: wgpu::TextureAspect::All,
330            },
331            source.desc.domain.storage_size.as_extent(),
332        );
333        Ok(self.queue.submit([encoder.finish()]))
334    }
335
336    pub fn copy_texture_to_external_discard_submission(
337        &self,
338        source_id: TextureId,
339        destination: &wgpu::Texture,
340    ) -> Result<()> {
341        let _ = self.copy_texture_to_external(source_id, destination)?;
342        Ok(())
343    }
344
345    pub fn submit_plan_with_external_texture(
346        &mut self,
347        plan: &RenderPlan,
348        texture_id: TextureId,
349        external: ExternalTexture,
350    ) -> Result<SubmittedExternalTexture> {
351        self.validate_external_texture(texture_id, external.desc())?;
352        let old_desc = self
353            .texture_desc(texture_id)
354            .ok_or_else(|| anyhow!("unknown texture id {texture_id:?}"))?;
355        let retained = external.texture_arc();
356        let old_texture =
357            self.replace_texture_arc(texture_id, retained.clone(), external.desc())?;
358        let submission = match self.submit_plan(plan) {
359            Ok(submission) => submission,
360            Err(error) => {
361                let _ = self.replace_texture_arc(texture_id, old_texture, old_desc);
362                return Err(error);
363            }
364        };
365        let _ = self.replace_texture_arc(texture_id, old_texture, old_desc)?;
366        Ok(SubmittedExternalTexture {
367            submission,
368            texture: retained,
369        })
370    }
371
372    pub fn buffer(&self, id: BufferId) -> Option<&wgpu::Buffer> {
373        self.buffers.get(id.0 as usize).map(|buffer| &buffer.buffer)
374    }
375
376    pub fn execute(
377        &mut self,
378        plan: &RenderPlan,
379        update: &FrameUpdate<'_>,
380    ) -> Result<wgpu::SubmissionIndex> {
381        self.apply_frame_update(plan, update)?;
382        self.submit_plan(plan)
383    }
384
385    pub fn apply_frame_update(
386        &mut self,
387        plan: &RenderPlan,
388        update: &FrameUpdate<'_>,
389    ) -> Result<()> {
390        self.validate_prepared(plan)?;
391        tracing::trace!(
392            target: "lumen_gpu",
393            uploads = update.uploads().len(),
394            "apply frame update"
395        );
396        self.apply_uploads(update)
397    }
398
399    pub fn submit_plan(&mut self, plan: &RenderPlan) -> Result<wgpu::SubmissionIndex> {
400        self.validate_prepared(plan)?;
401        tracing::trace!(
402            target: "lumen_gpu",
403            passes = plan.passes.len(),
404            "submit render plan"
405        );
406
407        let mut encoder = self
408            .device
409            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
410                label: Some("lumen-gpu render-plan encoder"),
411            });
412
413        for pass in &plan.passes {
414            match &pass.desc {
415                PassDesc::Render(desc) => self.execute_render_pass(desc, &mut encoder)?,
416                PassDesc::Compute(desc) => self.execute_compute_pass(desc, &mut encoder)?,
417                PassDesc::CopyTexture(desc) => self.execute_copy_texture(desc, &mut encoder)?,
418            }
419        }
420
421        Ok(self.queue.submit([encoder.finish()]))
422    }
423
424    pub fn execute_discard_submission(
425        &mut self,
426        plan: &RenderPlan,
427        update: &FrameUpdate<'_>,
428    ) -> Result<()> {
429        let _ = self.execute(plan, update)?;
430        Ok(())
431    }
432
433    fn create_texture(&self, resource: &TextureResource) -> Result<RuntimeTexture> {
434        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
435            label: resource.label.as_deref(),
436            size: resource.desc.domain.storage_size.as_extent(),
437            mip_level_count: 1,
438            sample_count: 1,
439            dimension: wgpu::TextureDimension::D2,
440            format: resource.desc.format,
441            usage: resource.desc.usage,
442            view_formats: &[],
443        });
444        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
445        Ok(RuntimeTexture {
446            texture: Arc::new(texture),
447            view,
448            desc: resource.desc,
449        })
450    }
451
452    fn create_buffer(&self, resource: &BufferResource) -> RuntimeBuffer {
453        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
454            label: resource.label.as_deref(),
455            size: resource.desc.size.max(1),
456            usage: resource.desc.usage,
457            mapped_at_creation: false,
458        });
459        RuntimeBuffer {
460            buffer,
461            desc: resource.desc,
462        }
463    }
464
465    fn create_program(&self, program: &Program) -> Result<RuntimeProgram> {
466        match &program.desc {
467            ProgramDesc::Render(desc) => {
468                self.create_render_program(desc).map(RuntimeProgram::Render)
469            }
470            ProgramDesc::Compute(desc) => self
471                .create_compute_program(desc)
472                .map(RuntimeProgram::Compute),
473        }
474    }
475
476    fn create_render_program(&self, desc: &RenderProgramDesc) -> Result<RuntimeRenderProgram> {
477        let shader = self
478            .device
479            .create_shader_module(wgpu::ShaderModuleDescriptor {
480                label: desc.label.as_deref(),
481                source: wgpu::ShaderSource::Wgsl(desc.shader.clone().into()),
482            });
483        let bind_group_layouts = create_bind_group_layouts(&self.device, &desc.bind_groups);
484        let layout_refs: Vec<_> = bind_group_layouts.iter().map(Some).collect();
485        let pipeline_layout = self
486            .device
487            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
488                label: desc.label.as_deref(),
489                bind_group_layouts: &layout_refs,
490                immediate_size: 0,
491            });
492        let pipeline = self
493            .device
494            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
495                label: desc.label.as_deref(),
496                layout: Some(&pipeline_layout),
497                vertex: wgpu::VertexState {
498                    module: &shader,
499                    entry_point: Some(&desc.vertex_entry),
500                    compilation_options: Default::default(),
501                    buffers: &desc.vertex_buffers,
502                },
503                primitive: desc.primitive,
504                depth_stencil: None,
505                multisample: wgpu::MultisampleState::default(),
506                fragment: Some(wgpu::FragmentState {
507                    module: &shader,
508                    entry_point: Some(&desc.fragment_entry),
509                    compilation_options: Default::default(),
510                    targets: &desc.targets,
511                }),
512                multiview_mask: None,
513                cache: None,
514            });
515        Ok(RuntimeRenderProgram {
516            pipeline,
517            bind_group_layouts,
518        })
519    }
520
521    fn create_compute_program(&self, desc: &ComputeProgramDesc) -> Result<RuntimeComputeProgram> {
522        let shader = self
523            .device
524            .create_shader_module(wgpu::ShaderModuleDescriptor {
525                label: desc.label.as_deref(),
526                source: wgpu::ShaderSource::Wgsl(desc.shader.clone().into()),
527            });
528        let bind_group_layouts = create_bind_group_layouts(&self.device, &desc.bind_groups);
529        let layout_refs: Vec<_> = bind_group_layouts.iter().map(Some).collect();
530        let pipeline_layout = self
531            .device
532            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
533                label: desc.label.as_deref(),
534                bind_group_layouts: &layout_refs,
535                immediate_size: 0,
536            });
537        let pipeline = self
538            .device
539            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
540                label: desc.label.as_deref(),
541                layout: Some(&pipeline_layout),
542                module: &shader,
543                entry_point: Some(&desc.entry),
544                compilation_options: Default::default(),
545                cache: None,
546            });
547        Ok(RuntimeComputeProgram {
548            pipeline,
549            bind_group_layouts,
550        })
551    }
552
553    fn validate_prepared(&self, plan: &RenderPlan) -> Result<()> {
554        if self.textures.len() != plan.textures.len()
555            || self.buffers.len() != plan.buffers.len()
556            || self.samplers.len() != plan.samplers.len()
557            || self.programs.len() != plan.programs.len()
558        {
559            bail!("render plan has not been prepared or has changed since prepare_plan");
560        }
561        Ok(())
562    }
563
564    fn validate_external_texture(&self, id: TextureId, external: TextureDesc) -> Result<()> {
565        let slot = self
566            .textures
567            .get(id.0 as usize)
568            .ok_or_else(|| anyhow!("unknown texture id {id:?}"))?;
569        let expected = slot.desc;
570        if expected.domain.storage_size != external.domain.storage_size {
571            bail!(
572                "external texture for {id:?} has size {:?}, expected {:?}",
573                external.domain.storage_size,
574                expected.domain.storage_size
575            );
576        }
577        if expected.format != external.format {
578            bail!(
579                "external texture for {id:?} has format {:?}, expected {:?}",
580                external.format,
581                expected.format
582            );
583        }
584        if !external.usage.contains(expected.usage) {
585            bail!(
586                "external texture for {id:?} has usage {:?}, expected at least {:?}",
587                external.usage,
588                expected.usage
589            );
590        }
591        Ok(())
592    }
593
594    fn apply_uploads(&self, update: &FrameUpdate<'_>) -> Result<()> {
595        for upload in update.uploads() {
596            match upload {
597                Upload::Buffer { id, offset, data } => {
598                    tracing::trace!(
599                        target: "lumen_gpu",
600                        ?id,
601                        offset,
602                        bytes = data.len(),
603                        "upload buffer"
604                    );
605                    let buffer = self.runtime_buffer(*id)?;
606                    let end = offset.saturating_add(data.len() as u64);
607                    if end > buffer.desc.size {
608                        bail!("buffer upload for {id:?} exceeds declared buffer size");
609                    }
610                    self.queue.write_buffer(&buffer.buffer, *offset, data);
611                }
612                Upload::TextureRgba8 {
613                    id,
614                    data,
615                    bytes_per_row,
616                    rows_per_image,
617                } => {
618                    tracing::trace!(
619                        target: "lumen_gpu",
620                        ?id,
621                        bytes = data.len(),
622                        bytes_per_row,
623                        rows_per_image,
624                        "upload rgba8 texture"
625                    );
626                    let texture = self.runtime_texture(*id)?;
627                    self.queue.write_texture(
628                        texture.texture.as_image_copy(),
629                        data,
630                        wgpu::TexelCopyBufferLayout {
631                            offset: 0,
632                            bytes_per_row: Some(*bytes_per_row),
633                            rows_per_image: Some(*rows_per_image),
634                        },
635                        texture.desc.domain.storage_size.as_extent(),
636                    );
637                }
638                Upload::TextureRgba8Region {
639                    id,
640                    data,
641                    origin,
642                    size,
643                    bytes_per_row,
644                    rows_per_image,
645                } => {
646                    tracing::trace!(
647                        target: "lumen_gpu",
648                        ?id,
649                        bytes = data.len(),
650                        origin_x = origin[0],
651                        origin_y = origin[1],
652                        origin_z = origin[2],
653                        width = size.width,
654                        height = size.height,
655                        "upload rgba8 texture region"
656                    );
657                    let texture = self.runtime_texture(*id)?;
658                    self.queue.write_texture(
659                        wgpu::TexelCopyTextureInfo {
660                            texture: &texture.texture,
661                            mip_level: 0,
662                            origin: wgpu::Origin3d {
663                                x: origin[0],
664                                y: origin[1],
665                                z: origin[2],
666                            },
667                            aspect: wgpu::TextureAspect::All,
668                        },
669                        data,
670                        wgpu::TexelCopyBufferLayout {
671                            offset: 0,
672                            bytes_per_row: Some(*bytes_per_row),
673                            rows_per_image: Some(*rows_per_image),
674                        },
675                        size.as_extent(),
676                    );
677                }
678                Upload::TextureRgba16Float {
679                    id,
680                    data,
681                    bytes_per_row,
682                    rows_per_image,
683                } => {
684                    tracing::trace!(
685                        target: "lumen_gpu",
686                        ?id,
687                        bytes = std::mem::size_of_val(*data),
688                        bytes_per_row,
689                        rows_per_image,
690                        "upload rgba16f texture"
691                    );
692                    let texture = self.runtime_texture(*id)?;
693                    self.queue.write_texture(
694                        texture.texture.as_image_copy(),
695                        bytemuck::cast_slice(data),
696                        wgpu::TexelCopyBufferLayout {
697                            offset: 0,
698                            bytes_per_row: Some(*bytes_per_row),
699                            rows_per_image: Some(*rows_per_image),
700                        },
701                        texture.desc.domain.storage_size.as_extent(),
702                    );
703                }
704            }
705        }
706        Ok(())
707    }
708
709    fn execute_render_pass(
710        &self,
711        desc: &RenderPassDesc,
712        encoder: &mut wgpu::CommandEncoder,
713    ) -> Result<()> {
714        tracing::trace!(
715            target: "lumen_gpu",
716            label = desc.label.as_deref().unwrap_or(""),
717            targets = desc.targets.len(),
718            bind_groups = desc.bindings.len(),
719            vertex_buffers = desc.vertex_buffers.len(),
720            "encode render pass"
721        );
722        let RuntimeProgram::Render(program) = self.runtime_program(desc.program)? else {
723            bail!("program {:?} is not a render program", desc.program);
724        };
725        let attachments = desc
726            .targets
727            .iter()
728            .map(|target| {
729                let texture = self.runtime_texture(target.texture)?;
730                Ok(Some(wgpu::RenderPassColorAttachment {
731                    view: &texture.view,
732                    depth_slice: None,
733                    resolve_target: None,
734                    ops: wgpu::Operations {
735                        load: match target.load {
736                            LoadOp::Load => wgpu::LoadOp::Load,
737                            LoadOp::Clear(color) => wgpu::LoadOp::Clear(color),
738                        },
739                        store: target.store,
740                    },
741                }))
742            })
743            .collect::<Result<Vec<_>>>()?;
744        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
745            label: desc.label.as_deref(),
746            color_attachments: &attachments,
747            depth_stencil_attachment: None,
748            occlusion_query_set: None,
749            timestamp_writes: None,
750            multiview_mask: None,
751        });
752        pass.set_pipeline(&program.pipeline);
753        let bind_groups =
754            self.create_pass_bind_groups(&program.bind_group_layouts, &desc.bindings)?;
755        for (group, bind_group) in bind_groups.iter().enumerate() {
756            pass.set_bind_group(group as u32, bind_group, &[]);
757        }
758        for (slot, buffer_id) in desc.vertex_buffers.iter().enumerate() {
759            pass.set_vertex_buffer(
760                slot as u32,
761                self.runtime_buffer(*buffer_id)?.buffer.slice(..),
762            );
763        }
764        if let Some((buffer_id, format)) = desc.index_buffer {
765            pass.set_index_buffer(self.runtime_buffer(buffer_id)?.buffer.slice(..), format);
766        }
767        if let Some(scissor) = desc.scissor {
768            pass.set_scissor_rect(scissor.x, scissor.y, scissor.width, scissor.height);
769        }
770        match &desc.draw {
771            DrawCommand::Draw(draw) => pass.draw(draw.vertices.clone(), draw.instances.clone()),
772            DrawCommand::DrawIndexed(draw) => pass.draw_indexed(
773                draw.indices.clone(),
774                draw.base_vertex,
775                draw.instances.clone(),
776            ),
777        }
778        Ok(())
779    }
780
781    fn execute_compute_pass(
782        &self,
783        desc: &ComputePassDesc,
784        encoder: &mut wgpu::CommandEncoder,
785    ) -> Result<()> {
786        match desc.dispatch {
787            ComputeDispatch::Direct(dispatch) => tracing::trace!(
788                target: "lumen_gpu",
789                label = desc.label.as_deref().unwrap_or(""),
790                x = dispatch.x,
791                y = dispatch.y,
792                z = dispatch.z,
793                "encode compute pass"
794            ),
795            ComputeDispatch::Indirect { buffer, offset } => tracing::trace!(
796                target: "lumen_gpu",
797                label = desc.label.as_deref().unwrap_or(""),
798                ?buffer,
799                offset,
800                "encode indirect compute pass"
801            ),
802        }
803        let RuntimeProgram::Compute(program) = self.runtime_program(desc.program)? else {
804            bail!("program {:?} is not a compute program", desc.program);
805        };
806        let bind_groups =
807            self.create_pass_bind_groups(&program.bind_group_layouts, &desc.bindings)?;
808        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
809            label: desc.label.as_deref(),
810            timestamp_writes: None,
811        });
812        pass.set_pipeline(&program.pipeline);
813        for (group, bind_group) in bind_groups.iter().enumerate() {
814            pass.set_bind_group(group as u32, bind_group, &[]);
815        }
816        match desc.dispatch {
817            ComputeDispatch::Direct(dispatch) => {
818                pass.dispatch_workgroups(dispatch.x, dispatch.y, dispatch.z);
819            }
820            ComputeDispatch::Indirect { buffer, offset } => {
821                let buffer = &self.runtime_buffer(buffer)?.buffer;
822                pass.dispatch_workgroups_indirect(buffer, offset);
823            }
824        }
825        Ok(())
826    }
827
828    fn execute_copy_texture(
829        &self,
830        desc: &CopyTextureDesc,
831        encoder: &mut wgpu::CommandEncoder,
832    ) -> Result<()> {
833        tracing::trace!(
834            target: "lumen_gpu",
835            source = ?desc.source,
836            destination = ?desc.destination,
837            width = desc.size.width,
838            height = desc.size.height,
839            "encode texture copy"
840        );
841        let source = self.runtime_texture(desc.source)?;
842        let destination = self.runtime_texture(desc.destination)?;
843        encoder.copy_texture_to_texture(
844            source.texture.as_image_copy(),
845            wgpu::TexelCopyTextureInfo {
846                texture: &destination.texture,
847                mip_level: 0,
848                origin: desc.origin,
849                aspect: wgpu::TextureAspect::All,
850            },
851            desc.size.as_extent(),
852        );
853        Ok(())
854    }
855
856    fn create_pass_bind_groups(
857        &self,
858        layouts: &[wgpu::BindGroupLayout],
859        bindings: &[Binding],
860    ) -> Result<Vec<wgpu::BindGroup>> {
861        let mut grouped: HashMap<u32, Vec<&Binding>> = HashMap::new();
862        for binding in bindings {
863            grouped.entry(binding.group).or_default().push(binding);
864        }
865        let mut bind_groups = Vec::with_capacity(layouts.len());
866        for (group_index, layout) in layouts.iter().enumerate() {
867            let mut group_entries = grouped.remove(&(group_index as u32)).unwrap_or_default();
868            group_entries.sort_by_key(|entry| entry.binding);
869            let entries = group_entries
870                .iter()
871                .map(|binding| self.bind_group_entry(binding))
872                .collect::<Result<Vec<_>>>()?;
873            bind_groups.push(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
874                label: None,
875                layout,
876                entries: &entries,
877            }));
878        }
879        if !grouped.is_empty() {
880            bail!("pass contains bindings for groups not declared by its program");
881        }
882        Ok(bind_groups)
883    }
884
885    fn bind_group_entry(&self, binding: &Binding) -> Result<wgpu::BindGroupEntry<'_>> {
886        let resource = match binding.resource {
887            BindingResource::Texture { id, .. } => {
888                wgpu::BindingResource::TextureView(&self.runtime_texture(id)?.view)
889            }
890            BindingResource::Buffer { id, .. } => {
891                self.runtime_buffer(id)?.buffer.as_entire_binding()
892            }
893            BindingResource::Sampler(id) => {
894                wgpu::BindingResource::Sampler(&self.runtime_sampler(id)?.sampler)
895            }
896        };
897        Ok(wgpu::BindGroupEntry {
898            binding: binding.binding,
899            resource,
900        })
901    }
902
903    fn runtime_texture(&self, id: TextureId) -> Result<&RuntimeTexture> {
904        self.textures
905            .get(id.0 as usize)
906            .ok_or_else(|| anyhow!("unknown texture id {id:?}"))
907    }
908
909    fn runtime_buffer(&self, id: BufferId) -> Result<&RuntimeBuffer> {
910        self.buffers
911            .get(id.0 as usize)
912            .ok_or_else(|| anyhow!("unknown buffer id {id:?}"))
913    }
914
915    fn runtime_sampler(&self, id: SamplerId) -> Result<&RuntimeSampler> {
916        self.samplers
917            .get(id.0 as usize)
918            .ok_or_else(|| anyhow!("unknown sampler id {id:?}"))
919    }
920
921    fn runtime_program(&self, id: ProgramId) -> Result<&RuntimeProgram> {
922        self.programs
923            .get(id.0 as usize)
924            .ok_or_else(|| anyhow!("unknown program id {id:?}"))
925    }
926}
927
928fn env_flag(name: &str) -> bool {
929    env::var(name)
930        .map(|value| {
931            matches!(
932                value.to_ascii_lowercase().as_str(),
933                "1" | "true" | "yes" | "on"
934            )
935        })
936        .unwrap_or(false)
937}
938
939fn create_bind_group_layouts(
940    device: &wgpu::Device,
941    specs: &[BindGroupLayoutSpec],
942) -> Vec<wgpu::BindGroupLayout> {
943    specs
944        .iter()
945        .map(|spec| {
946            let entries: Vec<_> = spec
947                .entries
948                .iter()
949                .map(|entry| wgpu::BindGroupLayoutEntry {
950                    binding: entry.binding,
951                    visibility: entry.visibility,
952                    ty: entry.ty,
953                    count: None,
954                })
955                .collect();
956            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
957                label: spec.label.as_deref(),
958                entries: &entries,
959            })
960        })
961        .collect()
962}