1use astrelis_core::profiling::{profile_function, profile_scope};
35
36use crate::capability::{RenderCapability, clamp_limits_to_adapter};
37use crate::features::GpuFeatures;
38use astrelis_test_utils::{
39 GpuBindGroup, GpuBindGroupLayout, GpuBuffer, GpuComputePipeline, GpuRenderPipeline, GpuSampler,
40 GpuShaderModule, GpuTexture, RenderContext,
41};
42use std::sync::Arc;
43use wgpu::{
44 BindGroupDescriptor, BindGroupLayoutDescriptor, BufferDescriptor, ComputePipelineDescriptor,
45 RenderPipelineDescriptor, SamplerDescriptor, ShaderModuleDescriptor, TextureDescriptor,
46};
47
48#[derive(Debug, Clone)]
50pub enum GraphicsError {
51 NoAdapter,
53
54 DeviceCreationFailed(String),
56
57 MissingRequiredFeatures {
59 missing: GpuFeatures,
60 adapter_name: String,
61 supported: GpuFeatures,
62 },
63
64 SurfaceCreationFailed(String),
66
67 SurfaceConfigurationFailed(String),
69
70 SurfaceTextureAcquisitionFailed(String),
72
73 SurfaceLost,
76
77 SurfaceOutdated,
80
81 SurfaceOutOfMemory,
83
84 SurfaceTimeout,
86
87 GpuProfilingFailed(String),
89}
90
91impl std::fmt::Display for GraphicsError {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 GraphicsError::NoAdapter => {
95 write!(f, "Failed to find a suitable GPU adapter")
96 }
97 GraphicsError::DeviceCreationFailed(msg) => {
98 write!(f, "Failed to create device: {}", msg)
99 }
100 GraphicsError::MissingRequiredFeatures {
101 missing,
102 adapter_name,
103 supported,
104 } => {
105 write!(
106 f,
107 "Required GPU features not supported by adapter '{}': {:?}\nSupported: {:?}",
108 adapter_name, missing, supported
109 )
110 }
111 GraphicsError::SurfaceCreationFailed(msg) => {
112 write!(f, "Failed to create surface: {}", msg)
113 }
114 GraphicsError::SurfaceConfigurationFailed(msg) => {
115 write!(f, "Failed to get surface configuration: {}", msg)
116 }
117 GraphicsError::SurfaceTextureAcquisitionFailed(msg) => {
118 write!(f, "Failed to acquire surface texture: {}", msg)
119 }
120 GraphicsError::SurfaceLost => {
121 write!(
122 f,
123 "Surface lost - needs recreation (window minimize, GPU reset, etc.)"
124 )
125 }
126 GraphicsError::SurfaceOutdated => {
127 write!(
128 f,
129 "Surface outdated - needs reconfiguration (window resized)"
130 )
131 }
132 GraphicsError::SurfaceOutOfMemory => {
133 write!(f, "Out of memory acquiring surface texture")
134 }
135 GraphicsError::SurfaceTimeout => {
136 write!(f, "Timeout acquiring surface texture")
137 }
138 GraphicsError::GpuProfilingFailed(msg) => {
139 write!(f, "GPU profiling initialization failed: {}", msg)
140 }
141 }
142 }
143}
144
145impl std::error::Error for GraphicsError {}
146
147impl From<crate::gpu_profiling::GpuFrameProfilerError> for GraphicsError {
148 fn from(err: crate::gpu_profiling::GpuFrameProfilerError) -> Self {
149 GraphicsError::GpuProfilingFailed(err.to_string())
150 }
151}
152
153pub struct GraphicsContext {
180 pub(crate) instance: wgpu::Instance,
181 pub(crate) adapter: wgpu::Adapter,
182 pub(crate) device: wgpu::Device,
183 pub(crate) queue: wgpu::Queue,
184 enabled_features: GpuFeatures,
186}
187
188impl GraphicsContext {
189 pub async fn new_owned() -> Result<Arc<Self>, GraphicsError> {
204 profile_function!();
205 Self::new_owned_with_descriptor(GraphicsContextDescriptor::default()).await
206 }
207
208 pub fn new_owned_sync() -> Result<Arc<Self>, GraphicsError> {
239 profile_function!();
240 pollster::block_on(Self::new_owned())
241 }
242
243 pub async fn new_owned_with_descriptor(
245 descriptor: GraphicsContextDescriptor,
246 ) -> Result<Arc<Self>, GraphicsError> {
247 let context = Self::create_context_internal(descriptor).await?;
248 Ok(Arc::new(context))
249 }
250
251 async fn create_context_internal(
253 descriptor: GraphicsContextDescriptor,
254 ) -> Result<Self, GraphicsError> {
255 profile_function!();
256
257 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
258 backends: descriptor.backends,
259 ..Default::default()
260 });
261
262 let adapter = {
263 profile_scope!("request_adapter");
264 instance
265 .request_adapter(&wgpu::RequestAdapterOptions {
266 power_preference: descriptor.power_preference,
267 compatible_surface: None,
268 force_fallback_adapter: descriptor.force_fallback_adapter,
269 })
270 .await
271 .map_err(|_| GraphicsError::NoAdapter)?
272 };
273
274 let required_result = descriptor.required_gpu_features.check_support(&adapter);
276 if let Some(missing) = required_result.missing() {
277 return Err(GraphicsError::MissingRequiredFeatures {
278 missing,
279 adapter_name: adapter.get_info().name.clone(),
280 supported: GpuFeatures::from_wgpu(adapter.features()),
281 });
282 }
283
284 let available_requested =
286 descriptor.requested_gpu_features & GpuFeatures::from_wgpu(adapter.features());
287
288 let unavailable_requested = descriptor.requested_gpu_features - available_requested;
290 if !unavailable_requested.is_empty() {
291 tracing::warn!(
292 "Some requested GPU features are not available: {:?}",
293 unavailable_requested
294 );
295 }
296
297 let enabled_features = descriptor.required_gpu_features | available_requested;
299 let wgpu_features = enabled_features.to_wgpu() | descriptor.additional_wgpu_features;
300
301 let adapter_limits = adapter.limits();
303 let clamped_limits = clamp_limits_to_adapter(&descriptor.limits, &adapter_limits);
304
305 let (device, queue) = {
306 profile_scope!("request_device");
307 adapter
308 .request_device(&wgpu::DeviceDescriptor {
309 required_features: wgpu_features,
310 required_limits: clamped_limits,
311 label: descriptor.label,
312 ..Default::default()
313 })
314 .await
315 .map_err(|e| GraphicsError::DeviceCreationFailed(e.to_string()))?
316 };
317
318 tracing::info!(
319 "Created graphics context with features: {:?}",
320 enabled_features
321 );
322
323 Ok(Self {
324 instance,
325 adapter,
326 device,
327 queue,
328 enabled_features,
329 })
330 }
331
332 pub fn device(&self) -> &wgpu::Device {
334 &self.device
335 }
336
337 pub fn queue(&self) -> &wgpu::Queue {
339 &self.queue
340 }
341
342 pub fn adapter(&self) -> &wgpu::Adapter {
344 &self.adapter
345 }
346
347 pub fn instance(&self) -> &wgpu::Instance {
349 &self.instance
350 }
351
352 pub fn info(&self) -> wgpu::AdapterInfo {
354 self.adapter.get_info()
355 }
356
357 pub fn limits(&self) -> wgpu::Limits {
359 self.device.limits()
360 }
361
362 pub fn wgpu_features(&self) -> wgpu::Features {
364 self.device.features()
365 }
366
367 pub fn gpu_features(&self) -> GpuFeatures {
369 self.enabled_features
370 }
371
372 pub fn has_feature(&self, feature: GpuFeatures) -> bool {
374 self.enabled_features.contains(feature)
375 }
376
377 pub fn has_all_features(&self, features: GpuFeatures) -> bool {
379 self.enabled_features.contains(features)
380 }
381
382 pub fn require_feature(&self, feature: GpuFeatures) {
386 if !self.has_feature(feature) {
387 panic!(
388 "GPU feature {:?} is required but not enabled.\n\
389 Enabled features: {:?}\n\
390 To use this feature, add it to `required_gpu_features` in GraphicsContextDescriptor.",
391 feature, self.enabled_features
392 );
393 }
394 }
395
396 pub fn supports_texture_format(
411 &self,
412 format: wgpu::TextureFormat,
413 usages: wgpu::TextureUsages,
414 ) -> bool {
415 self.adapter
416 .get_texture_format_features(format)
417 .allowed_usages
418 .contains(usages)
419 }
420
421 pub fn texture_format_capabilities(
426 &self,
427 format: wgpu::TextureFormat,
428 ) -> wgpu::TextureFormatFeatures {
429 self.adapter.get_texture_format_features(format)
430 }
431}
432
433pub struct GraphicsContextDescriptor {
435 pub backends: wgpu::Backends,
437 pub power_preference: wgpu::PowerPreference,
439 pub force_fallback_adapter: bool,
441 pub required_gpu_features: GpuFeatures,
445 pub requested_gpu_features: GpuFeatures,
449 pub additional_wgpu_features: wgpu::Features,
451 pub limits: wgpu::Limits,
453 pub label: Option<&'static str>,
455}
456
457impl Default for GraphicsContextDescriptor {
458 fn default() -> Self {
459 Self {
460 backends: wgpu::Backends::all(),
461 power_preference: wgpu::PowerPreference::HighPerformance,
462 force_fallback_adapter: false,
463 required_gpu_features: GpuFeatures::empty(),
464 requested_gpu_features: GpuFeatures::empty(),
465 additional_wgpu_features: wgpu::Features::empty(),
466 limits: wgpu::Limits::default(),
467 label: None,
468 }
469 }
470}
471
472impl GraphicsContextDescriptor {
473 pub fn new() -> Self {
475 Self::default()
476 }
477
478 pub fn require_features(mut self, features: GpuFeatures) -> Self {
480 self.required_gpu_features = features;
481 self
482 }
483
484 pub fn request_features(mut self, features: GpuFeatures) -> Self {
486 self.requested_gpu_features = features;
487 self
488 }
489
490 pub fn with_required_features(mut self, features: GpuFeatures) -> Self {
492 self.required_gpu_features |= features;
493 self
494 }
495
496 pub fn with_requested_features(mut self, features: GpuFeatures) -> Self {
498 self.requested_gpu_features |= features;
499 self
500 }
501
502 pub fn with_wgpu_features(mut self, features: wgpu::Features) -> Self {
504 self.additional_wgpu_features = features;
505 self
506 }
507
508 pub fn power_preference(mut self, preference: wgpu::PowerPreference) -> Self {
510 self.power_preference = preference;
511 self
512 }
513
514 pub fn backends(mut self, backends: wgpu::Backends) -> Self {
516 self.backends = backends;
517 self
518 }
519
520 pub fn limits(mut self, limits: wgpu::Limits) -> Self {
522 self.limits = limits;
523 self
524 }
525
526 pub fn label(mut self, label: &'static str) -> Self {
528 self.label = Some(label);
529 self
530 }
531
532 pub fn require_capability<T: RenderCapability>(mut self) -> Self {
546 let reqs = T::requirements();
547 self.required_gpu_features |= reqs.required_features;
548 self.required_gpu_features |= reqs.requested_features;
549 self.additional_wgpu_features |= reqs.additional_wgpu_features;
550 crate::capability::merge_limits_max(&mut self.limits, &reqs.min_limits);
551 tracing::trace!("Required capability: {}", T::name());
552 self
553 }
554
555 pub fn request_capability<T: RenderCapability>(mut self) -> Self {
572 let reqs = T::requirements();
573 self.required_gpu_features |= reqs.required_features;
574 self.requested_gpu_features |= reqs.requested_features;
575 self.additional_wgpu_features |= reqs.additional_wgpu_features;
576 crate::capability::merge_limits_max(&mut self.limits, &reqs.min_limits);
577 tracing::trace!("Requested capability: {}", T::name());
578 self
579 }
580}
581
582impl RenderContext for GraphicsContext {
587 fn create_buffer(&self, desc: &BufferDescriptor) -> GpuBuffer {
588 let buffer = self.device().create_buffer(desc);
589 GpuBuffer::from_wgpu(buffer)
590 }
591
592 fn write_buffer(&self, buffer: &GpuBuffer, offset: u64, data: &[u8]) {
593 let wgpu_buffer = buffer.as_wgpu();
594 self.queue().write_buffer(wgpu_buffer, offset, data);
595 }
596
597 fn create_texture(&self, desc: &TextureDescriptor) -> GpuTexture {
598 let texture = self.device().create_texture(desc);
599 GpuTexture::from_wgpu(texture)
600 }
601
602 fn create_shader_module(&self, desc: &ShaderModuleDescriptor) -> GpuShaderModule {
603 let module = self.device().create_shader_module(desc.clone());
604 GpuShaderModule::from_wgpu(module)
605 }
606
607 fn create_render_pipeline(&self, desc: &RenderPipelineDescriptor) -> GpuRenderPipeline {
608 let pipeline = self.device().create_render_pipeline(desc);
609 GpuRenderPipeline::from_wgpu(pipeline)
610 }
611
612 fn create_compute_pipeline(&self, desc: &ComputePipelineDescriptor) -> GpuComputePipeline {
613 let pipeline = self.device().create_compute_pipeline(desc);
614 GpuComputePipeline::from_wgpu(pipeline)
615 }
616
617 fn create_bind_group_layout(&self, desc: &BindGroupLayoutDescriptor) -> GpuBindGroupLayout {
618 let layout = self.device().create_bind_group_layout(desc);
619 GpuBindGroupLayout::from_wgpu(layout)
620 }
621
622 fn create_bind_group(&self, desc: &BindGroupDescriptor) -> GpuBindGroup {
623 let bind_group = self.device().create_bind_group(desc);
624 GpuBindGroup::from_wgpu(bind_group)
625 }
626
627 fn create_sampler(&self, desc: &SamplerDescriptor) -> GpuSampler {
628 let sampler = self.device().create_sampler(desc);
629 GpuSampler::from_wgpu(sampler)
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 #[cfg(feature = "mock")]
636 use super::*;
637 #[cfg(feature = "mock")]
638 use astrelis_test_utils::MockRenderContext;
639
640 #[test]
641 #[cfg(feature = "mock")]
642 fn test_render_context_trait_object() {
643 let mock_ctx = MockRenderContext::new();
647
648 fn uses_render_context(ctx: &dyn RenderContext) {
649 let buffer = ctx.create_buffer(&BufferDescriptor {
650 label: Some("Test Buffer"),
651 size: 256,
652 usage: wgpu::BufferUsages::UNIFORM,
653 mapped_at_creation: false,
654 });
655
656 ctx.write_buffer(&buffer, 0, &[0u8; 256]);
657 }
658
659 uses_render_context(&mock_ctx);
661
662 let calls = mock_ctx.calls();
664 assert_eq!(calls.len(), 2); }
666}