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    textures: Vec<RuntimeTexture>,
46    buffers: Vec<RuntimeBuffer>,
47    samplers: Vec<RuntimeSampler>,
48    programs: Vec<RuntimeProgram>,
49}
50
51impl Renderer {
52    pub async fn new() -> Result<Self> {
53        let instance =
54            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
55        let power_preference =
56            wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
57        let force_fallback_adapter = env_flag("LUMEN_GPU_FORCE_FALLBACK_ADAPTER");
58        let adapter = instance
59            .request_adapter(&wgpu::RequestAdapterOptions {
60                power_preference,
61                compatible_surface: None,
62                force_fallback_adapter,
63            })
64            .await
65            .context("no compatible wgpu adapter")?;
66        let adapter_info = adapter.get_info();
67        tracing::info!(
68            target: "lumen_gpu",
69            adapter = %adapter_info.name,
70            backend = ?adapter_info.backend,
71            device_type = ?adapter_info.device_type,
72            "selected wgpu adapter"
73        );
74        let (device, queue) = adapter
75            .request_device(&wgpu::DeviceDescriptor::default())
76            .await
77            .context("create wgpu device")?;
78        Ok(Self::from_device(device, queue))
79    }
80
81    pub fn from_device(device: wgpu::Device, queue: wgpu::Queue) -> Self {
82        Self {
83            device,
84            queue,
85            textures: Vec::new(),
86            buffers: Vec::new(),
87            samplers: Vec::new(),
88            programs: Vec::new(),
89        }
90    }
91
92    pub fn prepare_plan(&mut self, plan: &RenderPlan) -> Result<()> {
93        tracing::debug!(
94            target: "lumen_gpu",
95            textures = plan.textures.len(),
96            buffers = plan.buffers.len(),
97            samplers = plan.samplers.len(),
98            programs = plan.programs.len(),
99            passes = plan.passes.len(),
100            "prepare render plan"
101        );
102        self.textures = plan
103            .textures
104            .iter()
105            .map(|resource| self.create_texture(resource))
106            .collect::<Result<Vec<_>>>()?;
107        self.buffers = plan
108            .buffers
109            .iter()
110            .map(|resource| self.create_buffer(resource))
111            .collect();
112        self.samplers = plan
113            .samplers
114            .iter()
115            .map(|resource| RuntimeSampler {
116                sampler: self.device.create_sampler(&resource.desc),
117            })
118            .collect();
119        self.programs = plan
120            .programs
121            .iter()
122            .map(|program| self.create_program(program))
123            .collect::<Result<Vec<_>>>()?;
124        Ok(())
125    }
126
127    pub fn texture_view(&self, id: TextureId) -> Option<&wgpu::TextureView> {
128        self.textures
129            .get(id.0 as usize)
130            .map(|texture| &texture.view)
131    }
132
133    pub fn texture(&self, id: TextureId) -> Option<&wgpu::Texture> {
134        self.textures
135            .get(id.0 as usize)
136            .map(|texture| texture.texture.as_ref())
137    }
138
139    pub fn texture_arc(&self, id: TextureId) -> Option<Arc<wgpu::Texture>> {
140        self.textures
141            .get(id.0 as usize)
142            .map(|texture| Arc::clone(&texture.texture))
143    }
144
145    pub fn replace_texture(
146        &mut self,
147        id: TextureId,
148        texture: wgpu::Texture,
149        desc: TextureDesc,
150    ) -> Result<Arc<wgpu::Texture>> {
151        self.replace_texture_arc(id, Arc::new(texture), desc)
152    }
153
154    pub fn replace_texture_arc(
155        &mut self,
156        id: TextureId,
157        texture: Arc<wgpu::Texture>,
158        desc: TextureDesc,
159    ) -> Result<Arc<wgpu::Texture>> {
160        let runtime = self
161            .textures
162            .get_mut(id.0 as usize)
163            .ok_or_else(|| anyhow!("unknown texture id {id:?}"))?;
164        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
165        let old = std::mem::replace(&mut runtime.texture, texture);
166        runtime.view = view;
167        runtime.desc = desc;
168        Ok(old)
169    }
170
171    pub fn replace_texture_discard_old(
172        &mut self,
173        id: TextureId,
174        texture: wgpu::Texture,
175        desc: TextureDesc,
176    ) -> Result<()> {
177        let _ = self.replace_texture(id, texture, desc)?;
178        Ok(())
179    }
180
181    pub fn copy_texture_to_external(
182        &self,
183        source_id: TextureId,
184        destination: &wgpu::Texture,
185    ) -> Result<wgpu::SubmissionIndex> {
186        let source = self.runtime_texture(source_id)?;
187        let mut encoder = self
188            .device
189            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
190                label: Some("lumen-gpu external texture copy"),
191            });
192        encoder.copy_texture_to_texture(
193            wgpu::TexelCopyTextureInfo {
194                texture: &source.texture,
195                mip_level: 0,
196                origin: wgpu::Origin3d::ZERO,
197                aspect: wgpu::TextureAspect::All,
198            },
199            wgpu::TexelCopyTextureInfo {
200                texture: destination,
201                mip_level: 0,
202                origin: wgpu::Origin3d::ZERO,
203                aspect: wgpu::TextureAspect::All,
204            },
205            source.desc.domain.storage_size.as_extent(),
206        );
207        Ok(self.queue.submit([encoder.finish()]))
208    }
209
210    pub fn copy_texture_to_external_discard_submission(
211        &self,
212        source_id: TextureId,
213        destination: &wgpu::Texture,
214    ) -> Result<()> {
215        let _ = self.copy_texture_to_external(source_id, destination)?;
216        Ok(())
217    }
218
219    pub fn buffer(&self, id: BufferId) -> Option<&wgpu::Buffer> {
220        self.buffers.get(id.0 as usize).map(|buffer| &buffer.buffer)
221    }
222
223    pub fn execute(
224        &mut self,
225        plan: &RenderPlan,
226        update: &FrameUpdate<'_>,
227    ) -> Result<wgpu::SubmissionIndex> {
228        self.apply_frame_update(plan, update)?;
229        self.submit_plan(plan)
230    }
231
232    pub fn apply_frame_update(
233        &mut self,
234        plan: &RenderPlan,
235        update: &FrameUpdate<'_>,
236    ) -> Result<()> {
237        self.validate_prepared(plan)?;
238        tracing::trace!(
239            target: "lumen_gpu",
240            uploads = update.uploads().len(),
241            "apply frame update"
242        );
243        self.apply_uploads(update)
244    }
245
246    pub fn submit_plan(&mut self, plan: &RenderPlan) -> Result<wgpu::SubmissionIndex> {
247        self.validate_prepared(plan)?;
248        tracing::trace!(
249            target: "lumen_gpu",
250            passes = plan.passes.len(),
251            "submit render plan"
252        );
253
254        let mut encoder = self
255            .device
256            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
257                label: Some("lumen-gpu render-plan encoder"),
258            });
259
260        for pass in &plan.passes {
261            match &pass.desc {
262                PassDesc::Render(desc) => self.execute_render_pass(desc, &mut encoder)?,
263                PassDesc::Compute(desc) => self.execute_compute_pass(desc, &mut encoder)?,
264                PassDesc::CopyTexture(desc) => self.execute_copy_texture(desc, &mut encoder)?,
265            }
266        }
267
268        Ok(self.queue.submit([encoder.finish()]))
269    }
270
271    pub fn execute_discard_submission(
272        &mut self,
273        plan: &RenderPlan,
274        update: &FrameUpdate<'_>,
275    ) -> Result<()> {
276        let _ = self.execute(plan, update)?;
277        Ok(())
278    }
279
280    fn create_texture(&self, resource: &TextureResource) -> Result<RuntimeTexture> {
281        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
282            label: resource.label.as_deref(),
283            size: resource.desc.domain.storage_size.as_extent(),
284            mip_level_count: 1,
285            sample_count: 1,
286            dimension: wgpu::TextureDimension::D2,
287            format: resource.desc.format,
288            usage: resource.desc.usage,
289            view_formats: &[],
290        });
291        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
292        Ok(RuntimeTexture {
293            texture: Arc::new(texture),
294            view,
295            desc: resource.desc,
296        })
297    }
298
299    fn create_buffer(&self, resource: &BufferResource) -> RuntimeBuffer {
300        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
301            label: resource.label.as_deref(),
302            size: resource.desc.size.max(1),
303            usage: resource.desc.usage,
304            mapped_at_creation: false,
305        });
306        RuntimeBuffer {
307            buffer,
308            desc: resource.desc,
309        }
310    }
311
312    fn create_program(&self, program: &Program) -> Result<RuntimeProgram> {
313        match &program.desc {
314            ProgramDesc::Render(desc) => {
315                self.create_render_program(desc).map(RuntimeProgram::Render)
316            }
317            ProgramDesc::Compute(desc) => self
318                .create_compute_program(desc)
319                .map(RuntimeProgram::Compute),
320        }
321    }
322
323    fn create_render_program(&self, desc: &RenderProgramDesc) -> Result<RuntimeRenderProgram> {
324        let shader = self
325            .device
326            .create_shader_module(wgpu::ShaderModuleDescriptor {
327                label: desc.label.as_deref(),
328                source: wgpu::ShaderSource::Wgsl(desc.shader.clone().into()),
329            });
330        let bind_group_layouts = create_bind_group_layouts(&self.device, &desc.bind_groups);
331        let layout_refs: Vec<_> = bind_group_layouts.iter().map(Some).collect();
332        let pipeline_layout = self
333            .device
334            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
335                label: desc.label.as_deref(),
336                bind_group_layouts: &layout_refs,
337                immediate_size: 0,
338            });
339        let pipeline = self
340            .device
341            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
342                label: desc.label.as_deref(),
343                layout: Some(&pipeline_layout),
344                vertex: wgpu::VertexState {
345                    module: &shader,
346                    entry_point: Some(&desc.vertex_entry),
347                    compilation_options: Default::default(),
348                    buffers: &desc.vertex_buffers,
349                },
350                primitive: desc.primitive,
351                depth_stencil: None,
352                multisample: wgpu::MultisampleState::default(),
353                fragment: Some(wgpu::FragmentState {
354                    module: &shader,
355                    entry_point: Some(&desc.fragment_entry),
356                    compilation_options: Default::default(),
357                    targets: &desc.targets,
358                }),
359                multiview_mask: None,
360                cache: None,
361            });
362        Ok(RuntimeRenderProgram {
363            pipeline,
364            bind_group_layouts,
365        })
366    }
367
368    fn create_compute_program(&self, desc: &ComputeProgramDesc) -> Result<RuntimeComputeProgram> {
369        let shader = self
370            .device
371            .create_shader_module(wgpu::ShaderModuleDescriptor {
372                label: desc.label.as_deref(),
373                source: wgpu::ShaderSource::Wgsl(desc.shader.clone().into()),
374            });
375        let bind_group_layouts = create_bind_group_layouts(&self.device, &desc.bind_groups);
376        let layout_refs: Vec<_> = bind_group_layouts.iter().map(Some).collect();
377        let pipeline_layout = self
378            .device
379            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
380                label: desc.label.as_deref(),
381                bind_group_layouts: &layout_refs,
382                immediate_size: 0,
383            });
384        let pipeline = self
385            .device
386            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
387                label: desc.label.as_deref(),
388                layout: Some(&pipeline_layout),
389                module: &shader,
390                entry_point: Some(&desc.entry),
391                compilation_options: Default::default(),
392                cache: None,
393            });
394        Ok(RuntimeComputeProgram {
395            pipeline,
396            bind_group_layouts,
397        })
398    }
399
400    fn validate_prepared(&self, plan: &RenderPlan) -> Result<()> {
401        if self.textures.len() != plan.textures.len()
402            || self.buffers.len() != plan.buffers.len()
403            || self.samplers.len() != plan.samplers.len()
404            || self.programs.len() != plan.programs.len()
405        {
406            bail!("render plan has not been prepared or has changed since prepare_plan");
407        }
408        Ok(())
409    }
410
411    fn apply_uploads(&self, update: &FrameUpdate<'_>) -> Result<()> {
412        for upload in update.uploads() {
413            match upload {
414                Upload::Buffer { id, offset, data } => {
415                    tracing::trace!(
416                        target: "lumen_gpu",
417                        ?id,
418                        offset,
419                        bytes = data.len(),
420                        "upload buffer"
421                    );
422                    let buffer = self.runtime_buffer(*id)?;
423                    let end = offset.saturating_add(data.len() as u64);
424                    if end > buffer.desc.size {
425                        bail!("buffer upload for {id:?} exceeds declared buffer size");
426                    }
427                    self.queue.write_buffer(&buffer.buffer, *offset, data);
428                }
429                Upload::TextureRgba8 {
430                    id,
431                    data,
432                    bytes_per_row,
433                    rows_per_image,
434                } => {
435                    tracing::trace!(
436                        target: "lumen_gpu",
437                        ?id,
438                        bytes = data.len(),
439                        bytes_per_row,
440                        rows_per_image,
441                        "upload rgba8 texture"
442                    );
443                    let texture = self.runtime_texture(*id)?;
444                    self.queue.write_texture(
445                        texture.texture.as_image_copy(),
446                        data,
447                        wgpu::TexelCopyBufferLayout {
448                            offset: 0,
449                            bytes_per_row: Some(*bytes_per_row),
450                            rows_per_image: Some(*rows_per_image),
451                        },
452                        texture.desc.domain.storage_size.as_extent(),
453                    );
454                }
455                Upload::TextureRgba8Region {
456                    id,
457                    data,
458                    origin,
459                    size,
460                    bytes_per_row,
461                    rows_per_image,
462                } => {
463                    tracing::trace!(
464                        target: "lumen_gpu",
465                        ?id,
466                        bytes = data.len(),
467                        origin_x = origin[0],
468                        origin_y = origin[1],
469                        origin_z = origin[2],
470                        width = size.width,
471                        height = size.height,
472                        "upload rgba8 texture region"
473                    );
474                    let texture = self.runtime_texture(*id)?;
475                    self.queue.write_texture(
476                        wgpu::TexelCopyTextureInfo {
477                            texture: &texture.texture,
478                            mip_level: 0,
479                            origin: wgpu::Origin3d {
480                                x: origin[0],
481                                y: origin[1],
482                                z: origin[2],
483                            },
484                            aspect: wgpu::TextureAspect::All,
485                        },
486                        data,
487                        wgpu::TexelCopyBufferLayout {
488                            offset: 0,
489                            bytes_per_row: Some(*bytes_per_row),
490                            rows_per_image: Some(*rows_per_image),
491                        },
492                        size.as_extent(),
493                    );
494                }
495                Upload::TextureRgba16Float {
496                    id,
497                    data,
498                    bytes_per_row,
499                    rows_per_image,
500                } => {
501                    tracing::trace!(
502                        target: "lumen_gpu",
503                        ?id,
504                        bytes = data.len() * std::mem::size_of::<u16>(),
505                        bytes_per_row,
506                        rows_per_image,
507                        "upload rgba16f texture"
508                    );
509                    let texture = self.runtime_texture(*id)?;
510                    self.queue.write_texture(
511                        texture.texture.as_image_copy(),
512                        bytemuck::cast_slice(data),
513                        wgpu::TexelCopyBufferLayout {
514                            offset: 0,
515                            bytes_per_row: Some(*bytes_per_row),
516                            rows_per_image: Some(*rows_per_image),
517                        },
518                        texture.desc.domain.storage_size.as_extent(),
519                    );
520                }
521            }
522        }
523        Ok(())
524    }
525
526    fn execute_render_pass(
527        &self,
528        desc: &RenderPassDesc,
529        encoder: &mut wgpu::CommandEncoder,
530    ) -> Result<()> {
531        tracing::trace!(
532            target: "lumen_gpu",
533            label = desc.label.as_deref().unwrap_or(""),
534            targets = desc.targets.len(),
535            bind_groups = desc.bindings.len(),
536            vertex_buffers = desc.vertex_buffers.len(),
537            "encode render pass"
538        );
539        let RuntimeProgram::Render(program) = self.runtime_program(desc.program)? else {
540            bail!("program {:?} is not a render program", desc.program);
541        };
542        let attachments = desc
543            .targets
544            .iter()
545            .map(|target| {
546                let texture = self.runtime_texture(target.texture)?;
547                Ok(Some(wgpu::RenderPassColorAttachment {
548                    view: &texture.view,
549                    depth_slice: None,
550                    resolve_target: None,
551                    ops: wgpu::Operations {
552                        load: match target.load {
553                            LoadOp::Load => wgpu::LoadOp::Load,
554                            LoadOp::Clear(color) => wgpu::LoadOp::Clear(color),
555                        },
556                        store: target.store,
557                    },
558                }))
559            })
560            .collect::<Result<Vec<_>>>()?;
561        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
562            label: desc.label.as_deref(),
563            color_attachments: &attachments,
564            depth_stencil_attachment: None,
565            occlusion_query_set: None,
566            timestamp_writes: None,
567            multiview_mask: None,
568        });
569        pass.set_pipeline(&program.pipeline);
570        let bind_groups =
571            self.create_pass_bind_groups(&program.bind_group_layouts, &desc.bindings)?;
572        for (group, bind_group) in bind_groups.iter().enumerate() {
573            pass.set_bind_group(group as u32, bind_group, &[]);
574        }
575        for (slot, buffer_id) in desc.vertex_buffers.iter().enumerate() {
576            pass.set_vertex_buffer(
577                slot as u32,
578                self.runtime_buffer(*buffer_id)?.buffer.slice(..),
579            );
580        }
581        if let Some((buffer_id, format)) = desc.index_buffer {
582            pass.set_index_buffer(self.runtime_buffer(buffer_id)?.buffer.slice(..), format);
583        }
584        if let Some(scissor) = desc.scissor {
585            pass.set_scissor_rect(scissor.x, scissor.y, scissor.width, scissor.height);
586        }
587        match &desc.draw {
588            DrawCommand::Draw(draw) => pass.draw(draw.vertices.clone(), draw.instances.clone()),
589            DrawCommand::DrawIndexed(draw) => pass.draw_indexed(
590                draw.indices.clone(),
591                draw.base_vertex,
592                draw.instances.clone(),
593            ),
594        }
595        Ok(())
596    }
597
598    fn execute_compute_pass(
599        &self,
600        desc: &ComputePassDesc,
601        encoder: &mut wgpu::CommandEncoder,
602    ) -> Result<()> {
603        match desc.dispatch {
604            ComputeDispatch::Direct(dispatch) => tracing::trace!(
605                target: "lumen_gpu",
606                label = desc.label.as_deref().unwrap_or(""),
607                x = dispatch.x,
608                y = dispatch.y,
609                z = dispatch.z,
610                "encode compute pass"
611            ),
612            ComputeDispatch::Indirect { buffer, offset } => tracing::trace!(
613                target: "lumen_gpu",
614                label = desc.label.as_deref().unwrap_or(""),
615                ?buffer,
616                offset,
617                "encode indirect compute pass"
618            ),
619        }
620        let RuntimeProgram::Compute(program) = self.runtime_program(desc.program)? else {
621            bail!("program {:?} is not a compute program", desc.program);
622        };
623        let bind_groups =
624            self.create_pass_bind_groups(&program.bind_group_layouts, &desc.bindings)?;
625        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
626            label: desc.label.as_deref(),
627            timestamp_writes: None,
628        });
629        pass.set_pipeline(&program.pipeline);
630        for (group, bind_group) in bind_groups.iter().enumerate() {
631            pass.set_bind_group(group as u32, bind_group, &[]);
632        }
633        match desc.dispatch {
634            ComputeDispatch::Direct(dispatch) => {
635                pass.dispatch_workgroups(dispatch.x, dispatch.y, dispatch.z);
636            }
637            ComputeDispatch::Indirect { buffer, offset } => {
638                let buffer = &self.runtime_buffer(buffer)?.buffer;
639                pass.dispatch_workgroups_indirect(buffer, offset);
640            }
641        }
642        Ok(())
643    }
644
645    fn execute_copy_texture(
646        &self,
647        desc: &CopyTextureDesc,
648        encoder: &mut wgpu::CommandEncoder,
649    ) -> Result<()> {
650        tracing::trace!(
651            target: "lumen_gpu",
652            source = ?desc.source,
653            destination = ?desc.destination,
654            width = desc.size.width,
655            height = desc.size.height,
656            "encode texture copy"
657        );
658        let source = self.runtime_texture(desc.source)?;
659        let destination = self.runtime_texture(desc.destination)?;
660        encoder.copy_texture_to_texture(
661            source.texture.as_image_copy(),
662            wgpu::TexelCopyTextureInfo {
663                texture: &destination.texture,
664                mip_level: 0,
665                origin: desc.origin,
666                aspect: wgpu::TextureAspect::All,
667            },
668            desc.size.as_extent(),
669        );
670        Ok(())
671    }
672
673    fn create_pass_bind_groups(
674        &self,
675        layouts: &[wgpu::BindGroupLayout],
676        bindings: &[Binding],
677    ) -> Result<Vec<wgpu::BindGroup>> {
678        let mut grouped: HashMap<u32, Vec<&Binding>> = HashMap::new();
679        for binding in bindings {
680            grouped.entry(binding.group).or_default().push(binding);
681        }
682        let mut bind_groups = Vec::with_capacity(layouts.len());
683        for (group_index, layout) in layouts.iter().enumerate() {
684            let mut group_entries = grouped.remove(&(group_index as u32)).unwrap_or_default();
685            group_entries.sort_by_key(|entry| entry.binding);
686            let entries = group_entries
687                .iter()
688                .map(|binding| self.bind_group_entry(binding))
689                .collect::<Result<Vec<_>>>()?;
690            bind_groups.push(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
691                label: None,
692                layout,
693                entries: &entries,
694            }));
695        }
696        if !grouped.is_empty() {
697            bail!("pass contains bindings for groups not declared by its program");
698        }
699        Ok(bind_groups)
700    }
701
702    fn bind_group_entry(&self, binding: &Binding) -> Result<wgpu::BindGroupEntry<'_>> {
703        let resource = match binding.resource {
704            BindingResource::Texture { id, .. } => {
705                wgpu::BindingResource::TextureView(&self.runtime_texture(id)?.view)
706            }
707            BindingResource::Buffer { id, .. } => {
708                self.runtime_buffer(id)?.buffer.as_entire_binding()
709            }
710            BindingResource::Sampler(id) => {
711                wgpu::BindingResource::Sampler(&self.runtime_sampler(id)?.sampler)
712            }
713        };
714        Ok(wgpu::BindGroupEntry {
715            binding: binding.binding,
716            resource,
717        })
718    }
719
720    fn runtime_texture(&self, id: TextureId) -> Result<&RuntimeTexture> {
721        self.textures
722            .get(id.0 as usize)
723            .ok_or_else(|| anyhow!("unknown texture id {id:?}"))
724    }
725
726    fn runtime_buffer(&self, id: BufferId) -> Result<&RuntimeBuffer> {
727        self.buffers
728            .get(id.0 as usize)
729            .ok_or_else(|| anyhow!("unknown buffer id {id:?}"))
730    }
731
732    fn runtime_sampler(&self, id: SamplerId) -> Result<&RuntimeSampler> {
733        self.samplers
734            .get(id.0 as usize)
735            .ok_or_else(|| anyhow!("unknown sampler id {id:?}"))
736    }
737
738    fn runtime_program(&self, id: ProgramId) -> Result<&RuntimeProgram> {
739        self.programs
740            .get(id.0 as usize)
741            .ok_or_else(|| anyhow!("unknown program id {id:?}"))
742    }
743}
744
745fn env_flag(name: &str) -> bool {
746    env::var(name)
747        .map(|value| {
748            matches!(
749                value.to_ascii_lowercase().as_str(),
750                "1" | "true" | "yes" | "on"
751            )
752        })
753        .unwrap_or(false)
754}
755
756fn create_bind_group_layouts(
757    device: &wgpu::Device,
758    specs: &[BindGroupLayoutSpec],
759) -> Vec<wgpu::BindGroupLayout> {
760    specs
761        .iter()
762        .map(|spec| {
763            let entries: Vec<_> = spec
764                .entries
765                .iter()
766                .map(|entry| wgpu::BindGroupLayoutEntry {
767                    binding: entry.binding,
768                    visibility: entry.visibility,
769                    ty: entry.ty,
770                    count: None,
771                })
772                .collect();
773            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
774                label: spec.label.as_deref(),
775                entries: &entries,
776            })
777        })
778        .collect()
779}