1use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
6
7use crate::core::binding::GlobalBindGroupCache;
8use crate::graph::composer::ComposerContext;
9use crate::graph::core::allocator::TransientPool;
10use crate::graph::core::arena::FrameArena;
11use crate::graph::core::graph::GraphStorage;
12use crate::graph::frame::RenderLists;
13#[cfg(feature = "debug_view")]
14use crate::graph::passes::DebugViewFeature;
15use crate::graph::passes::{
16 BloomFeature, BrdfLutFeature, CasFeature, FxaaFeature, IblComputeFeature, MsaaSyncFeature,
17 OpaqueFeature, PrepassFeature, ShadowFeature, SimpleForwardFeature, SkyboxFeature, SsaoFeature,
18 SsssFeature, TaaFeature, ToneMappingFeature, TransmissionCopyFeature, TransparentFeature,
19};
20use myth_assets::AssetServer;
21use myth_core::Result;
22use myth_scene::Scene;
23use myth_scene::camera::RenderCamera;
24
25use crate::core::{ResourceManager, WgpuContext};
26use crate::graph::{FrameComposer, RenderFrame};
27use crate::pipeline::PipelineCache;
28use crate::pipeline::ShaderManager;
29use crate::settings::{RenderPath, RendererInitConfig, RendererSettings};
30
31pub struct Renderer {
46 size: (u32, u32),
47 init_config: RendererInitConfig,
48 settings: RendererSettings,
49 context: Option<RendererState>,
50}
51
52struct RendererState {
54 wgpu_ctx: WgpuContext,
55 resource_manager: ResourceManager,
56 pipeline_cache: PipelineCache,
57 shader_manager: ShaderManager,
58
59 render_frame: RenderFrame,
60 render_lists: RenderLists,
62 global_bind_group_cache: GlobalBindGroupCache,
65
66 pub(crate) graph_storage: GraphStorage,
68 pub(crate) transient_pool: TransientPool,
70 pub(crate) frame_arena: FrameArena,
71
72 pub(crate) fxaa_pass: FxaaFeature,
74 pub(crate) taa_pass: TaaFeature,
75 pub(crate) cas_pass: CasFeature,
76 pub(crate) tone_map_pass: ToneMappingFeature,
77 pub(crate) bloom_pass: BloomFeature,
78 pub(crate) ssao_pass: SsaoFeature,
79
80 pub(crate) prepass: PrepassFeature,
82 pub(crate) opaque_pass: OpaqueFeature,
83 pub(crate) skybox_pass: SkyboxFeature,
84 pub(crate) transparent_pass: TransparentFeature,
85 pub(crate) transmission_copy_pass: TransmissionCopyFeature,
86 pub(crate) simple_forward_pass: SimpleForwardFeature,
87 pub(crate) ssss_pass: SsssFeature,
88 pub(crate) msaa_sync_pass: MsaaSyncFeature,
89
90 pub(crate) shadow_pass: ShadowFeature,
92 pub(crate) brdf_pass: BrdfLutFeature,
93 pub(crate) ibl_pass: IblComputeFeature,
94
95 #[cfg(feature = "debug_view")]
97 pub(crate) debug_view_pass: DebugViewFeature,
98
99 cached_readback_buffer: Option<wgpu::Buffer>,
102 cached_readback_buffer_size: u64,
104}
105
106#[derive(Debug, Clone, Copy, Default)]
107pub struct FrameTime {
108 pub time: f32,
109 pub delta_time: f32,
110 pub frame_count: u64,
111}
112
113impl Renderer {
114 #[must_use]
124 pub fn new(init_config: RendererInitConfig, settings: RendererSettings) -> Self {
125 Self {
126 init_config,
127 settings,
128 context: None,
129 size: (0, 0),
130 }
131 }
132
133 #[inline]
135 #[must_use]
136 pub fn size(&self) -> (u32, u32) {
137 self.size
138 }
139
140 pub async fn init<W>(&mut self, window: W, width: u32, height: u32) -> Result<()>
148 where
149 W: HasWindowHandle + HasDisplayHandle + Send + Sync + 'static,
150 {
151 if self.context.is_some() {
152 return Ok(());
153 }
154
155 self.size = (width, height);
156
157 let wgpu_ctx =
158 WgpuContext::new(window, &self.init_config, &self.settings, width, height).await?;
159
160 self.assemble_state(wgpu_ctx);
161 log::info!("Renderer initialized (windowed)");
162 Ok(())
163 }
164
165 pub async fn init_headless(
178 &mut self,
179 width: u32,
180 height: u32,
181 format: Option<myth_resources::PixelFormat>,
182 ) -> Result<()> {
183 if self.context.is_some() {
184 return Ok(());
185 }
186
187 self.size = (width, height);
188
189 let wgpu_format = format.map(|f| f.to_wgpu(myth_resources::ColorSpace::Srgb));
190
191 let wgpu_ctx = WgpuContext::new_headless(
192 &self.init_config,
193 &self.settings,
194 width,
195 height,
196 wgpu_format,
197 )
198 .await?;
199
200 self.assemble_state(wgpu_ctx);
201 log::info!("Renderer initialized (headless {width}×{height})");
202 Ok(())
203 }
204
205 fn assemble_state(&mut self, wgpu_ctx: WgpuContext) {
207 let resource_manager = ResourceManager::new(
208 wgpu_ctx.device.clone(),
209 wgpu_ctx.queue.clone(),
210 self.settings.anisotropy_clamp,
211 );
212
213 let render_frame = RenderFrame::new();
214 let global_bind_group_cache = GlobalBindGroupCache::new();
215
216 let shadow_pass = ShadowFeature::new(&wgpu_ctx.device);
217 let brdf_pass = BrdfLutFeature::new(&wgpu_ctx.device);
218 let ibl_pass = IblComputeFeature::new(&wgpu_ctx.device);
219
220 self.context = Some(RendererState {
221 wgpu_ctx,
222 resource_manager,
223 pipeline_cache: PipelineCache::new(),
224 shader_manager: ShaderManager::new(),
225
226 render_frame,
227 render_lists: RenderLists::new(),
228 global_bind_group_cache,
229
230 graph_storage: GraphStorage::new(),
231 transient_pool: TransientPool::new(),
232 frame_arena: FrameArena::new(),
233 fxaa_pass: FxaaFeature::new(),
234 taa_pass: TaaFeature::new(),
235 cas_pass: CasFeature::new(),
236 tone_map_pass: ToneMappingFeature::new(),
237 bloom_pass: BloomFeature::new(),
238 ssao_pass: SsaoFeature::new(),
239
240 prepass: PrepassFeature::new(),
241 opaque_pass: OpaqueFeature::new(),
242 skybox_pass: SkyboxFeature::new(),
243 transparent_pass: TransparentFeature::new(),
244 transmission_copy_pass: TransmissionCopyFeature::new(),
245 simple_forward_pass: SimpleForwardFeature::new(),
246 ssss_pass: SsssFeature::new(),
247 msaa_sync_pass: MsaaSyncFeature::new(),
248
249 shadow_pass,
250 brdf_pass,
251 ibl_pass,
252
253 #[cfg(feature = "debug_view")]
254 debug_view_pass: DebugViewFeature::new(),
255
256 cached_readback_buffer: None,
257 cached_readback_buffer_size: 0,
258 });
259 }
260
261 pub fn resize(&mut self, width: u32, height: u32) {
262 self.size = (width, height);
263 if let Some(state) = &mut self.context {
264 state.wgpu_ctx.resize(width, height);
265 state.global_bind_group_cache.clear();
267 }
268 }
269
270 pub fn begin_frame<'a>(
299 &'a mut self,
300 scene: &'a mut Scene,
301 camera: &'a RenderCamera,
302 assets: &'a AssetServer,
303 frame_time: FrameTime,
304 ) -> Option<FrameComposer<'a>> {
305 if self.size.0 == 0 || self.size.1 == 0 {
306 return None;
307 }
308
309 let state = self.context.as_mut()?;
310
311 state.frame_arena.reset();
315
316 state.global_bind_group_cache.begin_frame();
318
319 let surface_size = state.wgpu_ctx.size();
321 state.render_frame.extract_and_prepare(
322 &mut state.resource_manager,
323 scene,
324 camera,
325 assets,
326 frame_time,
327 &mut state.render_lists,
328 surface_size,
329 );
330
331 let requested_msaa = camera.aa_mode.msaa_sample_count();
332 if state.wgpu_ctx.msaa_samples != requested_msaa {
333 state.wgpu_ctx.msaa_samples = requested_msaa;
334 state.wgpu_ctx.pipeline_settings_version += 1;
335 }
336
337 crate::graph::culling::cull_and_sort(
339 &state.render_frame.extracted_scene,
340 &state.render_frame.render_state,
341 &state.wgpu_ctx,
342 &mut state.resource_manager,
343 &mut state.pipeline_cache,
344 &mut state.shader_manager,
345 &mut state.render_lists,
346 camera,
347 assets,
348 );
349
350 {
356 use crate::HDR_TEXTURE_FORMAT;
357 use crate::graph::core::context::ExtractContext;
358
359 let view_format = state.wgpu_ctx.surface_view_format;
360 let is_hf = state.wgpu_ctx.render_path.supports_post_processing();
361 let scene_id_val = scene.id();
362 let render_state_id = state.render_frame.render_state.id;
363 let global_state_key = (render_state_id, scene_id_val);
364
365 let ssao_enabled = scene.ssao.enabled && is_hf;
366 let needs_feature_id =
367 is_hf && (scene.screen_space.enable_sss || scene.screen_space.enable_ssr);
368
369 #[cfg(feature = "debug_view")]
371 {
372 let dv = camera.debug_view;
373 state.render_frame.render_state.debug_view_mode = dv.mode;
374 state.render_frame.render_state.debug_view_scale = dv.custom_scale;
375 }
376
377 #[cfg(feature = "debug_view")]
378 let (dbg_needs_normal, dbg_needs_velocity) = {
379 use crate::graph::render_state::DebugViewTarget;
380 let target =
381 DebugViewTarget::from_mode(state.render_frame.render_state.debug_view_mode);
382 (
383 target == DebugViewTarget::SceneNormal,
384 target == DebugViewTarget::Velocity,
385 )
386 };
387
388 #[cfg(not(feature = "debug_view"))]
389 let (dbg_needs_normal, dbg_needs_velocity) = (false, false);
390
391 let needs_normal = ssao_enabled || needs_feature_id || dbg_needs_normal;
392 let needs_velocity = camera.aa_mode.is_taa() || dbg_needs_velocity;
393
394 let needs_skybox = scene.background.needs_skybox_pass();
396 let bloom_enabled = scene.bloom.enabled && is_hf;
397
398 let mut extract_ctx = ExtractContext {
399 device: &state.wgpu_ctx.device,
400 queue: &state.wgpu_ctx.queue,
401 pipeline_cache: &mut state.pipeline_cache,
402 shader_manager: &mut state.shader_manager,
403 global_bind_group_cache: &mut state.global_bind_group_cache,
404 resource_manager: &mut state.resource_manager,
405 wgpu_ctx: &state.wgpu_ctx,
406 render_lists: &mut state.render_lists,
407 extracted_scene: &state.render_frame.extracted_scene,
408 render_state: &state.render_frame.render_state,
409 render_camera: camera,
410 assets,
411 };
412
413 state.brdf_pass.extract_and_prepare(&mut extract_ctx);
415 state.ibl_pass.extract_and_prepare(&mut extract_ctx);
416 state.shadow_pass.extract_and_prepare(&mut extract_ctx);
417
418 if needs_skybox {
420 let color_format = if is_hf {
421 HDR_TEXTURE_FORMAT
422 } else {
423 view_format
424 };
425 state.skybox_pass.extract_and_prepare(
426 &mut extract_ctx,
427 &scene.background.mode,
428 &scene.background.uniforms,
429 global_state_key,
430 color_format,
431 );
432 }
433
434 if is_hf {
435 if let Some(taa_settins) = camera.aa_mode.taa_settings() {
436 state.taa_pass.extract_and_prepare(
437 &mut extract_ctx,
438 taa_settins.feedback_weight,
439 self.size,
440 HDR_TEXTURE_FORMAT,
441 );
442
443 if taa_settins.sharpen_intensity > 0.0 {
444 state.cas_pass.extract_and_prepare(
445 &mut extract_ctx,
446 taa_settins.sharpen_intensity,
447 HDR_TEXTURE_FORMAT,
448 );
449 }
450 }
451
452 if let Some(fxaa_settings) = camera.aa_mode.fxaa_settings() {
453 state.fxaa_pass.target_quality = fxaa_settings.quality();
454 state
455 .fxaa_pass
456 .extract_and_prepare(&mut extract_ctx, view_format);
457 }
458
459 state.prepass.extract_and_prepare(
460 &mut extract_ctx,
461 needs_normal,
462 needs_feature_id,
463 needs_velocity,
464 );
465
466 if ssao_enabled {
467 state
468 .ssao_pass
469 .extract_and_prepare(&mut extract_ctx, &scene.ssao.uniforms);
470 }
471
472 state.ssss_pass.extract_and_prepare(&mut extract_ctx);
473
474 let msaa = state.wgpu_ctx.msaa_samples;
477 let needs_specular = scene.screen_space.enable_sss;
478 if msaa > 1 && needs_specular {
479 state
480 .msaa_sync_pass
481 .extract_and_prepare(&mut extract_ctx, msaa);
482 }
483
484 if bloom_enabled {
485 state.bloom_pass.extract_and_prepare(
486 &mut extract_ctx,
487 &scene.bloom.upsample_uniforms,
488 &scene.bloom.composite_uniforms,
489 );
490 }
491
492 state.tone_map_pass.extract_and_prepare(
493 &mut extract_ctx,
494 scene.tone_mapping.mode,
495 view_format,
496 global_state_key,
497 &scene.tone_mapping.uniforms,
498 scene.tone_mapping.lut_texture,
499 );
500
501 #[cfg(feature = "debug_view")]
503 {
504 use crate::graph::passes::debug_view::DebugViewUniforms;
505 use crate::graph::render_state::DebugViewTarget;
506
507 let dv = camera.debug_view;
508 let target = DebugViewTarget::from_mode(dv.mode);
509 if target != DebugViewTarget::None {
510 let params = DebugViewUniforms {
511 view_mode: target.view_mode(),
512 custom_scale: dv.custom_scale,
513 z_near: camera.near,
514 z_far: if camera.far.is_infinite() {
515 10000.0
516 } else {
517 camera.far
518 },
519 };
520 let is_depth = target == DebugViewTarget::SceneDepth;
521 state.debug_view_pass.extract_and_prepare(
522 &mut extract_ctx,
523 view_format,
524 params,
525 is_depth,
526 );
527 }
528 }
529 }
530 }
531
532 let ctx = ComposerContext {
534 wgpu_ctx: &mut state.wgpu_ctx,
535 resource_manager: &mut state.resource_manager,
536 pipeline_cache: &mut state.pipeline_cache,
537 shader_manager: &mut state.shader_manager,
538
539 extracted_scene: &state.render_frame.extracted_scene,
540 render_state: &state.render_frame.render_state,
541
542 global_bind_group_cache: &mut state.global_bind_group_cache,
543
544 render_lists: &mut state.render_lists,
545
546 scene,
548 camera,
549 assets,
550 frame_time,
551
552 graph_storage: &mut state.graph_storage,
553 transient_pool: &mut state.transient_pool,
554 frame_arena: &state.frame_arena,
556 fxaa_pass: &mut state.fxaa_pass,
557 taa_pass: &mut state.taa_pass,
558 cas_pass: &mut state.cas_pass,
559 tone_map_pass: &mut state.tone_map_pass,
560 bloom_pass: &mut state.bloom_pass,
561 ssao_pass: &mut state.ssao_pass,
562
563 prepass: &mut state.prepass,
564 opaque_pass: &mut state.opaque_pass,
565 skybox_pass: &mut state.skybox_pass,
566 transparent_pass: &mut state.transparent_pass,
567 transmission_copy_pass: &mut state.transmission_copy_pass,
568 simple_forward_pass: &mut state.simple_forward_pass,
569 ssss_pass: &mut state.ssss_pass,
570 msaa_sync_pass: &mut state.msaa_sync_pass,
571
572 shadow_pass: &mut state.shadow_pass,
573 brdf_pass: &mut state.brdf_pass,
574 ibl_pass: &mut state.ibl_pass,
575
576 #[cfg(feature = "debug_view")]
577 debug_view_pass: &mut state.debug_view_pass,
578 };
579
580 Some(FrameComposer::new(ctx, self.size))
582 }
583
584 pub fn maybe_prune(&mut self) {
589 if let Some(state) = &mut self.context {
590 state.render_frame.maybe_prune(&mut state.resource_manager);
591 state.global_bind_group_cache.garbage_collect();
593 }
594 }
595
596 #[inline]
600 pub fn render_path(&self) -> &RenderPath {
601 &self.settings.path
602 }
603
604 #[inline]
606 pub fn settings(&self) -> &RendererSettings {
607 &self.settings
608 }
609
610 #[inline]
612 pub fn init_config(&self) -> &RendererInitConfig {
613 &self.init_config
614 }
615
616 pub fn update_settings(&mut self, new_settings: RendererSettings) {
623 if self.settings == new_settings {
624 return;
625 }
626
627 let old = std::mem::replace(&mut self.settings, new_settings);
628
629 if let Some(state) = &mut self.context {
630 if old.vsync != self.settings.vsync {
632 state.wgpu_ctx.set_vsync(self.settings.vsync);
633 }
634
635 if old.path != self.settings.path {
637 state.wgpu_ctx.render_path = self.settings.path;
638 state.wgpu_ctx.pipeline_settings_version += 1;
639 log::info!("RenderPath changed to {:?}", self.settings.path);
640 }
641
642 if old.anisotropy_clamp != self.settings.anisotropy_clamp {
644 state
645 .resource_manager
646 .sampler_registry
647 .set_global_anisotropy(self.settings.anisotropy_clamp);
648 log::info!(
649 "Anisotropy clamp changed to {}",
650 self.settings.anisotropy_clamp
651 );
652 }
653 }
654 }
655
656 pub fn set_render_path(&mut self, path: RenderPath) {
661 if self.settings.path != path {
662 let mut new = self.settings.clone();
663 new.path = path;
664 self.update_settings(new);
665 }
666 }
667
668 #[cfg(feature = "debug_view")]
676 pub fn set_debug_view_mode(&mut self, mode: myth_scene::camera::DebugViewMode) {
677 if let Some(state) = &mut self.context {
678 state.render_frame.render_state.debug_view_mode = mode;
679 }
680 }
681
682 #[cfg(feature = "debug_view")]
684 pub fn debug_view_mode(&self) -> myth_scene::camera::DebugViewMode {
685 self.context
686 .as_ref()
687 .map(|s| s.render_frame.render_state.debug_view_mode)
688 .unwrap_or_default()
689 }
690
691 pub fn device(&self) -> Option<&wgpu::Device> {
697 self.context.as_ref().map(|s| &s.wgpu_ctx.device)
698 }
699
700 pub fn queue(&self) -> Option<&wgpu::Queue> {
704 self.context.as_ref().map(|s| &s.wgpu_ctx.queue)
705 }
706
707 pub fn surface_format(&self) -> Option<wgpu::TextureFormat> {
712 self.context
713 .as_ref()
714 .map(|s| s.wgpu_ctx.surface_view_format)
715 }
716
717 pub fn wgpu_ctx(&self) -> Option<&WgpuContext> {
722 self.context.as_ref().map(|s| &s.wgpu_ctx)
723 }
724
725 pub fn dump_graph_mermaid(&self) -> Option<String> {
726 self.context
727 .as_ref()
728 .map(|s| s.graph_storage.dump_mermaid())
729 }
730
731 pub fn register_shader_template(&mut self, name: &str, source: &str) {
755 let state = self
756 .context
757 .as_mut()
758 .expect("Renderer must be initialized before registering shader templates");
759 state.shader_manager.register_template(name, source);
760 }
761
762 #[inline]
764 #[must_use]
765 pub fn is_headless(&self) -> bool {
766 self.context
767 .as_ref()
768 .is_some_and(|s| s.wgpu_ctx.is_headless())
769 }
770
771 pub fn readback_pixels(&mut self) -> Result<Vec<u8>> {
790 let state = self
791 .context
792 .as_mut()
793 .ok_or(myth_core::RenderError::NotInitialized)?;
794
795 let texture = state
796 .wgpu_ctx
797 .headless_texture
798 .as_ref()
799 .ok_or(myth_core::RenderError::NoHeadlessTarget)?;
800
801 let width = state.wgpu_ctx.target_width;
802 let height = state.wgpu_ctx.target_height;
803 let format = state.wgpu_ctx.surface_view_format;
804
805 let bytes_per_pixel = format.block_copy_size(None).ok_or_else(|| {
806 myth_core::RenderError::ReadbackFailed(format!(
807 "unsupported readback format: {format:?}"
808 ))
809 })?;
810
811 let unpadded_bytes_per_row = width * bytes_per_pixel;
812 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
813 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
814 let buffer_size = u64::from(padded_bytes_per_row) * u64::from(height);
815
816 if state.cached_readback_buffer_size != buffer_size {
818 state.cached_readback_buffer = Some(state.wgpu_ctx.device.create_buffer(
819 &wgpu::BufferDescriptor {
820 label: Some("Readback Buffer"),
821 size: buffer_size,
822 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
823 mapped_at_creation: false,
824 },
825 ));
826 state.cached_readback_buffer_size = buffer_size;
827 }
828
829 let readback_buffer = state.cached_readback_buffer.as_ref().unwrap();
830
831 let mut encoder =
832 state
833 .wgpu_ctx
834 .device
835 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
836 label: Some("Readback Encoder"),
837 });
838
839 encoder.copy_texture_to_buffer(
840 wgpu::TexelCopyTextureInfo {
841 texture,
842 mip_level: 0,
843 origin: wgpu::Origin3d::ZERO,
844 aspect: wgpu::TextureAspect::All,
845 },
846 wgpu::TexelCopyBufferInfo {
847 buffer: readback_buffer,
848 layout: wgpu::TexelCopyBufferLayout {
849 offset: 0,
850 bytes_per_row: Some(padded_bytes_per_row),
851 rows_per_image: Some(height),
852 },
853 },
854 wgpu::Extent3d {
855 width,
856 height,
857 depth_or_array_layers: 1,
858 },
859 );
860
861 state
862 .wgpu_ctx
863 .queue
864 .submit(std::iter::once(encoder.finish()));
865
866 let buffer_slice = readback_buffer.slice(..);
868
869 let (tx, rx) = std::sync::mpsc::sync_channel(1);
870 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
871 tx.send(result).ok();
872 });
873 state
874 .wgpu_ctx
875 .device
876 .poll(wgpu::PollType::wait_indefinitely())
877 .map_err(|e| myth_core::RenderError::ReadbackFailed(e.to_string()))?;
878
879 rx.recv()
880 .map_err(|e| myth_core::RenderError::ReadbackFailed(e.to_string()))?
881 .map_err(|e| myth_core::RenderError::ReadbackFailed(e.to_string()))?;
882
883 let mapped = buffer_slice.get_mapped_range();
885 let mut pixels = Vec::with_capacity((width * height * bytes_per_pixel) as usize);
886 for row in 0..height {
887 let start = (row * padded_bytes_per_row) as usize;
888 let end = start + unpadded_bytes_per_row as usize;
889 pixels.extend_from_slice(&mapped[start..end]);
890 }
891 drop(mapped);
892 readback_buffer.unmap();
893
894 Ok(pixels)
895 }
896
897 pub fn create_readback_stream(
908 &self,
909 buffer_count: usize,
910 max_stash_size: usize,
911 ) -> Result<crate::core::ReadbackStream> {
912 let state = self
913 .context
914 .as_ref()
915 .ok_or(myth_core::RenderError::NotInitialized)?;
916
917 if state.wgpu_ctx.headless_texture.is_none() {
918 return Err(myth_core::RenderError::NoHeadlessTarget.into());
919 }
920
921 let width = state.wgpu_ctx.target_width;
922 let height = state.wgpu_ctx.target_height;
923 let format = state.wgpu_ctx.surface_view_format;
924
925 let stream = crate::core::ReadbackStream::new(
926 &state.wgpu_ctx.device,
927 width,
928 height,
929 format,
930 buffer_count,
931 max_stash_size,
932 )?;
933
934 Ok(stream)
935 }
936
937 pub fn poll_device(&self) {
943 if let Some(state) = &self.context {
944 let _ = state.wgpu_ctx.device.poll(wgpu::PollType::Poll);
945 }
946 }
947
948 #[must_use]
950 pub fn headless_texture(&self) -> Option<&wgpu::Texture> {
951 self.context
952 .as_ref()
953 .and_then(|s| s.wgpu_ctx.headless_texture.as_ref())
954 }
955}