1use astrelis_core::profiling::{profile_function, profile_scope};
35
36use crate::capability::{clamp_limits_to_adapter, RenderCapability};
37use crate::features::GpuFeatures;
38use astrelis_test_utils::{
39 GpuBindGroup, GpuBindGroupLayout, GpuBuffer, GpuComputePipeline, GpuRenderPipeline,
40 GpuSampler, 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
88impl std::fmt::Display for GraphicsError {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 GraphicsError::NoAdapter => {
92 write!(f, "Failed to find a suitable GPU adapter")
93 }
94 GraphicsError::DeviceCreationFailed(msg) => {
95 write!(f, "Failed to create device: {}", msg)
96 }
97 GraphicsError::MissingRequiredFeatures { missing, adapter_name, supported } => {
98 write!(
99 f,
100 "Required GPU features not supported by adapter '{}': {:?}\nSupported: {:?}",
101 adapter_name, missing, supported
102 )
103 }
104 GraphicsError::SurfaceCreationFailed(msg) => {
105 write!(f, "Failed to create surface: {}", msg)
106 }
107 GraphicsError::SurfaceConfigurationFailed(msg) => {
108 write!(f, "Failed to get surface configuration: {}", msg)
109 }
110 GraphicsError::SurfaceTextureAcquisitionFailed(msg) => {
111 write!(f, "Failed to acquire surface texture: {}", msg)
112 }
113 GraphicsError::SurfaceLost => {
114 write!(f, "Surface lost - needs recreation (window minimize, GPU reset, etc.)")
115 }
116 GraphicsError::SurfaceOutdated => {
117 write!(f, "Surface outdated - needs reconfiguration (window resized)")
118 }
119 GraphicsError::SurfaceOutOfMemory => {
120 write!(f, "Out of memory acquiring surface texture")
121 }
122 GraphicsError::SurfaceTimeout => {
123 write!(f, "Timeout acquiring surface texture")
124 }
125 }
126 }
127}
128
129impl std::error::Error for GraphicsError {}
130
131pub struct GraphicsContext {
158 pub(crate) instance: wgpu::Instance,
159 pub(crate) adapter: wgpu::Adapter,
160 pub(crate) device: wgpu::Device,
161 pub(crate) queue: wgpu::Queue,
162 enabled_features: GpuFeatures,
164}
165
166impl GraphicsContext {
167 pub async fn new_owned() -> Result<Arc<Self>, GraphicsError> {
182 profile_function!();
183 Self::new_owned_with_descriptor(GraphicsContextDescriptor::default()).await
184 }
185
186 pub fn new_owned_sync() -> Result<Arc<Self>, GraphicsError> {
217 profile_function!();
218 pollster::block_on(Self::new_owned())
219 }
220
221 pub async fn new_owned_with_descriptor(descriptor: GraphicsContextDescriptor) -> Result<Arc<Self>, GraphicsError> {
223 let context = Self::create_context_internal(descriptor).await?;
224 Ok(Arc::new(context))
225 }
226
227 async fn create_context_internal(descriptor: GraphicsContextDescriptor) -> Result<Self, GraphicsError> {
229 profile_function!();
230
231 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
232 backends: descriptor.backends,
233 ..Default::default()
234 });
235
236 let adapter = {
237 profile_scope!("request_adapter");
238 instance
239 .request_adapter(&wgpu::RequestAdapterOptions {
240 power_preference: descriptor.power_preference,
241 compatible_surface: None,
242 force_fallback_adapter: descriptor.force_fallback_adapter,
243 })
244 .await
245 .map_err(|_| GraphicsError::NoAdapter)?
246 };
247
248 let required_result = descriptor.required_gpu_features.check_support(&adapter);
250 if let Some(missing) = required_result.missing() {
251 return Err(GraphicsError::MissingRequiredFeatures {
252 missing,
253 adapter_name: adapter.get_info().name.clone(),
254 supported: GpuFeatures::from_wgpu(adapter.features()),
255 });
256 }
257
258 let available_requested = descriptor.requested_gpu_features
260 & GpuFeatures::from_wgpu(adapter.features());
261
262 let unavailable_requested =
264 descriptor.requested_gpu_features - available_requested;
265 if !unavailable_requested.is_empty() {
266 tracing::warn!(
267 "Some requested GPU features are not available: {:?}",
268 unavailable_requested
269 );
270 }
271
272 let enabled_features = descriptor.required_gpu_features | available_requested;
274 let wgpu_features = enabled_features.to_wgpu() | descriptor.additional_wgpu_features;
275
276 let adapter_limits = adapter.limits();
278 let clamped_limits = clamp_limits_to_adapter(&descriptor.limits, &adapter_limits);
279
280 let (device, queue) = {
281 profile_scope!("request_device");
282 adapter
283 .request_device(&wgpu::DeviceDescriptor {
284 required_features: wgpu_features,
285 required_limits: clamped_limits,
286 label: descriptor.label,
287 ..Default::default()
288 })
289 .await
290 .map_err(|e| GraphicsError::DeviceCreationFailed(e.to_string()))?
291 };
292
293 tracing::info!(
294 "Created graphics context with features: {:?}",
295 enabled_features
296 );
297
298 Ok(Self {
299 instance,
300 adapter,
301 device,
302 queue,
303 enabled_features,
304 })
305 }
306
307 pub fn device(&self) -> &wgpu::Device {
309 &self.device
310 }
311
312 pub fn queue(&self) -> &wgpu::Queue {
314 &self.queue
315 }
316
317 pub fn adapter(&self) -> &wgpu::Adapter {
319 &self.adapter
320 }
321
322 pub fn instance(&self) -> &wgpu::Instance {
324 &self.instance
325 }
326
327 pub fn info(&self) -> wgpu::AdapterInfo {
329 self.adapter.get_info()
330 }
331
332 pub fn limits(&self) -> wgpu::Limits {
334 self.device.limits()
335 }
336
337 pub fn wgpu_features(&self) -> wgpu::Features {
339 self.device.features()
340 }
341
342 pub fn gpu_features(&self) -> GpuFeatures {
344 self.enabled_features
345 }
346
347 pub fn has_feature(&self, feature: GpuFeatures) -> bool {
349 self.enabled_features.contains(feature)
350 }
351
352 pub fn has_all_features(&self, features: GpuFeatures) -> bool {
354 self.enabled_features.contains(features)
355 }
356
357 pub fn require_feature(&self, feature: GpuFeatures) {
361 if !self.has_feature(feature) {
362 panic!(
363 "GPU feature {:?} is required but not enabled.\n\
364 Enabled features: {:?}\n\
365 To use this feature, add it to `required_gpu_features` in GraphicsContextDescriptor.",
366 feature, self.enabled_features
367 );
368 }
369 }
370
371 pub fn supports_texture_format(
386 &self,
387 format: wgpu::TextureFormat,
388 usages: wgpu::TextureUsages,
389 ) -> bool {
390 self.adapter
391 .get_texture_format_features(format)
392 .allowed_usages
393 .contains(usages)
394 }
395
396 pub fn texture_format_capabilities(
401 &self,
402 format: wgpu::TextureFormat,
403 ) -> wgpu::TextureFormatFeatures {
404 self.adapter.get_texture_format_features(format)
405 }
406}
407
408pub struct GraphicsContextDescriptor {
410 pub backends: wgpu::Backends,
412 pub power_preference: wgpu::PowerPreference,
414 pub force_fallback_adapter: bool,
416 pub required_gpu_features: GpuFeatures,
420 pub requested_gpu_features: GpuFeatures,
424 pub additional_wgpu_features: wgpu::Features,
426 pub limits: wgpu::Limits,
428 pub label: Option<&'static str>,
430}
431
432impl Default for GraphicsContextDescriptor {
433 fn default() -> Self {
434 Self {
435 backends: wgpu::Backends::all(),
436 power_preference: wgpu::PowerPreference::HighPerformance,
437 force_fallback_adapter: false,
438 required_gpu_features: GpuFeatures::empty(),
439 requested_gpu_features: GpuFeatures::empty(),
440 additional_wgpu_features: wgpu::Features::empty(),
441 limits: wgpu::Limits::default(),
442 label: None,
443 }
444 }
445}
446
447impl GraphicsContextDescriptor {
448 pub fn new() -> Self {
450 Self::default()
451 }
452
453 pub fn require_features(mut self, features: GpuFeatures) -> Self {
455 self.required_gpu_features = features;
456 self
457 }
458
459 pub fn request_features(mut self, features: GpuFeatures) -> Self {
461 self.requested_gpu_features = features;
462 self
463 }
464
465 pub fn with_required_features(mut self, features: GpuFeatures) -> Self {
467 self.required_gpu_features |= features;
468 self
469 }
470
471 pub fn with_requested_features(mut self, features: GpuFeatures) -> Self {
473 self.requested_gpu_features |= features;
474 self
475 }
476
477 pub fn with_wgpu_features(mut self, features: wgpu::Features) -> Self {
479 self.additional_wgpu_features = features;
480 self
481 }
482
483 pub fn power_preference(mut self, preference: wgpu::PowerPreference) -> Self {
485 self.power_preference = preference;
486 self
487 }
488
489 pub fn backends(mut self, backends: wgpu::Backends) -> Self {
491 self.backends = backends;
492 self
493 }
494
495 pub fn limits(mut self, limits: wgpu::Limits) -> Self {
497 self.limits = limits;
498 self
499 }
500
501 pub fn label(mut self, label: &'static str) -> Self {
503 self.label = Some(label);
504 self
505 }
506
507 pub fn require_capability<T: RenderCapability>(mut self) -> Self {
521 let reqs = T::requirements();
522 self.required_gpu_features |= reqs.required_features;
523 self.required_gpu_features |= reqs.requested_features;
524 self.additional_wgpu_features |= reqs.additional_wgpu_features;
525 crate::capability::merge_limits_max(&mut self.limits, &reqs.min_limits);
526 tracing::trace!("Required capability: {}", T::name());
527 self
528 }
529
530 pub fn request_capability<T: RenderCapability>(mut self) -> Self {
547 let reqs = T::requirements();
548 self.required_gpu_features |= reqs.required_features;
549 self.requested_gpu_features |= reqs.requested_features;
550 self.additional_wgpu_features |= reqs.additional_wgpu_features;
551 crate::capability::merge_limits_max(&mut self.limits, &reqs.min_limits);
552 tracing::trace!("Requested capability: {}", T::name());
553 self
554 }
555}
556
557impl RenderContext for GraphicsContext {
562 fn create_buffer(&self, desc: &BufferDescriptor) -> GpuBuffer {
563 let buffer = self.device().create_buffer(desc);
564 GpuBuffer::from_wgpu(buffer)
565 }
566
567 fn write_buffer(&self, buffer: &GpuBuffer, offset: u64, data: &[u8]) {
568 let wgpu_buffer = buffer.as_wgpu();
569 self.queue().write_buffer(wgpu_buffer, offset, data);
570 }
571
572 fn create_texture(&self, desc: &TextureDescriptor) -> GpuTexture {
573 let texture = self.device().create_texture(desc);
574 GpuTexture::from_wgpu(texture)
575 }
576
577 fn create_shader_module(&self, desc: &ShaderModuleDescriptor) -> GpuShaderModule {
578 let module = self.device().create_shader_module(desc.clone());
579 GpuShaderModule::from_wgpu(module)
580 }
581
582 fn create_render_pipeline(&self, desc: &RenderPipelineDescriptor) -> GpuRenderPipeline {
583 let pipeline = self.device().create_render_pipeline(desc);
584 GpuRenderPipeline::from_wgpu(pipeline)
585 }
586
587 fn create_compute_pipeline(&self, desc: &ComputePipelineDescriptor) -> GpuComputePipeline {
588 let pipeline = self.device().create_compute_pipeline(desc);
589 GpuComputePipeline::from_wgpu(pipeline)
590 }
591
592 fn create_bind_group_layout(&self, desc: &BindGroupLayoutDescriptor) -> GpuBindGroupLayout {
593 let layout = self.device().create_bind_group_layout(desc);
594 GpuBindGroupLayout::from_wgpu(layout)
595 }
596
597 fn create_bind_group(&self, desc: &BindGroupDescriptor) -> GpuBindGroup {
598 let bind_group = self.device().create_bind_group(desc);
599 GpuBindGroup::from_wgpu(bind_group)
600 }
601
602 fn create_sampler(&self, desc: &SamplerDescriptor) -> GpuSampler {
603 let sampler = self.device().create_sampler(desc);
604 GpuSampler::from_wgpu(sampler)
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 #[cfg(feature = "mock")]
611 use super::*;
612 #[cfg(feature = "mock")]
613 use astrelis_test_utils::MockRenderContext;
614
615 #[test]
616 #[cfg(feature = "mock")]
617 fn test_render_context_trait_object() {
618 let mock_ctx = MockRenderContext::new();
622
623 fn uses_render_context(ctx: &dyn RenderContext) {
624 let buffer = ctx.create_buffer(&BufferDescriptor {
625 label: Some("Test Buffer"),
626 size: 256,
627 usage: wgpu::BufferUsages::UNIFORM,
628 mapped_at_creation: false,
629 });
630
631 ctx.write_buffer(&buffer, 0, &[0u8; 256]);
632 }
633
634 uses_render_context(&mock_ctx);
636
637 let calls = mock_ctx.calls();
639 assert_eq!(calls.len(), 2); }
641}