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}