use astrelis_core::profiling::{profile_function, profile_scope};
use crate::capability::{RenderCapability, clamp_limits_to_adapter};
use crate::features::GpuFeatures;
use astrelis_test_utils::{
GpuBindGroup, GpuBindGroupLayout, GpuBuffer, GpuComputePipeline, GpuRenderPipeline, GpuSampler,
GpuShaderModule, GpuTexture, RenderContext,
};
use std::sync::Arc;
use wgpu::{
BindGroupDescriptor, BindGroupLayoutDescriptor, BufferDescriptor, ComputePipelineDescriptor,
RenderPipelineDescriptor, SamplerDescriptor, ShaderModuleDescriptor, TextureDescriptor,
};
#[derive(Debug, Clone)]
pub enum GraphicsError {
NoAdapter,
DeviceCreationFailed(String),
MissingRequiredFeatures {
missing: GpuFeatures,
adapter_name: String,
supported: GpuFeatures,
},
SurfaceCreationFailed(String),
SurfaceConfigurationFailed(String),
SurfaceTextureAcquisitionFailed(String),
SurfaceLost,
SurfaceOutdated,
SurfaceOutOfMemory,
SurfaceTimeout,
GpuProfilingFailed(String),
}
impl std::fmt::Display for GraphicsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphicsError::NoAdapter => {
write!(f, "Failed to find a suitable GPU adapter")
}
GraphicsError::DeviceCreationFailed(msg) => {
write!(f, "Failed to create device: {}", msg)
}
GraphicsError::MissingRequiredFeatures {
missing,
adapter_name,
supported,
} => {
write!(
f,
"Required GPU features not supported by adapter '{}': {:?}\nSupported: {:?}",
adapter_name, missing, supported
)
}
GraphicsError::SurfaceCreationFailed(msg) => {
write!(f, "Failed to create surface: {}", msg)
}
GraphicsError::SurfaceConfigurationFailed(msg) => {
write!(f, "Failed to get surface configuration: {}", msg)
}
GraphicsError::SurfaceTextureAcquisitionFailed(msg) => {
write!(f, "Failed to acquire surface texture: {}", msg)
}
GraphicsError::SurfaceLost => {
write!(
f,
"Surface lost - needs recreation (window minimize, GPU reset, etc.)"
)
}
GraphicsError::SurfaceOutdated => {
write!(
f,
"Surface outdated - needs reconfiguration (window resized)"
)
}
GraphicsError::SurfaceOutOfMemory => {
write!(f, "Out of memory acquiring surface texture")
}
GraphicsError::SurfaceTimeout => {
write!(f, "Timeout acquiring surface texture")
}
GraphicsError::GpuProfilingFailed(msg) => {
write!(f, "GPU profiling initialization failed: {}", msg)
}
}
}
}
impl std::error::Error for GraphicsError {}
impl From<crate::gpu_profiling::GpuFrameProfilerError> for GraphicsError {
fn from(err: crate::gpu_profiling::GpuFrameProfilerError) -> Self {
GraphicsError::GpuProfilingFailed(err.to_string())
}
}
pub struct GraphicsContext {
pub(crate) instance: wgpu::Instance,
pub(crate) adapter: wgpu::Adapter,
pub(crate) device: wgpu::Device,
pub(crate) queue: wgpu::Queue,
enabled_features: GpuFeatures,
}
impl GraphicsContext {
pub async fn new_owned() -> Result<Arc<Self>, GraphicsError> {
profile_function!();
Self::new_owned_with_descriptor(GraphicsContextDescriptor::default()).await
}
pub fn new_owned_sync() -> Result<Arc<Self>, GraphicsError> {
profile_function!();
pollster::block_on(Self::new_owned())
}
pub async fn new_owned_with_descriptor(
descriptor: GraphicsContextDescriptor,
) -> Result<Arc<Self>, GraphicsError> {
let context = Self::create_context_internal(descriptor).await?;
Ok(Arc::new(context))
}
async fn create_context_internal(
descriptor: GraphicsContextDescriptor,
) -> Result<Self, GraphicsError> {
profile_function!();
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: descriptor.backends,
..Default::default()
});
let adapter = {
profile_scope!("request_adapter");
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: descriptor.power_preference,
compatible_surface: None,
force_fallback_adapter: descriptor.force_fallback_adapter,
})
.await
.map_err(|_| GraphicsError::NoAdapter)?
};
let required_result = descriptor.required_gpu_features.check_support(&adapter);
if let Some(missing) = required_result.missing() {
return Err(GraphicsError::MissingRequiredFeatures {
missing,
adapter_name: adapter.get_info().name.clone(),
supported: GpuFeatures::from_wgpu(adapter.features()),
});
}
let available_requested =
descriptor.requested_gpu_features & GpuFeatures::from_wgpu(adapter.features());
let unavailable_requested = descriptor.requested_gpu_features - available_requested;
if !unavailable_requested.is_empty() {
tracing::warn!(
"Some requested GPU features are not available: {:?}",
unavailable_requested
);
}
let enabled_features = descriptor.required_gpu_features | available_requested;
let wgpu_features = enabled_features.to_wgpu() | descriptor.additional_wgpu_features;
let adapter_limits = adapter.limits();
let clamped_limits = clamp_limits_to_adapter(&descriptor.limits, &adapter_limits);
let (device, queue) = {
profile_scope!("request_device");
adapter
.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu_features,
required_limits: clamped_limits,
label: descriptor.label,
..Default::default()
})
.await
.map_err(|e| GraphicsError::DeviceCreationFailed(e.to_string()))?
};
tracing::info!(
"Created graphics context with features: {:?}",
enabled_features
);
Ok(Self {
instance,
adapter,
device,
queue,
enabled_features,
})
}
pub fn device(&self) -> &wgpu::Device {
&self.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.queue
}
pub fn adapter(&self) -> &wgpu::Adapter {
&self.adapter
}
pub fn instance(&self) -> &wgpu::Instance {
&self.instance
}
pub fn info(&self) -> wgpu::AdapterInfo {
self.adapter.get_info()
}
pub fn limits(&self) -> wgpu::Limits {
self.device.limits()
}
pub fn wgpu_features(&self) -> wgpu::Features {
self.device.features()
}
pub fn gpu_features(&self) -> GpuFeatures {
self.enabled_features
}
pub fn has_feature(&self, feature: GpuFeatures) -> bool {
self.enabled_features.contains(feature)
}
pub fn has_all_features(&self, features: GpuFeatures) -> bool {
self.enabled_features.contains(features)
}
pub fn require_feature(&self, feature: GpuFeatures) {
if !self.has_feature(feature) {
panic!(
"GPU feature {:?} is required but not enabled.\n\
Enabled features: {:?}\n\
To use this feature, add it to `required_gpu_features` in GraphicsContextDescriptor.",
feature, self.enabled_features
);
}
}
pub fn supports_texture_format(
&self,
format: wgpu::TextureFormat,
usages: wgpu::TextureUsages,
) -> bool {
self.adapter
.get_texture_format_features(format)
.allowed_usages
.contains(usages)
}
pub fn texture_format_capabilities(
&self,
format: wgpu::TextureFormat,
) -> wgpu::TextureFormatFeatures {
self.adapter.get_texture_format_features(format)
}
}
pub struct GraphicsContextDescriptor {
pub backends: wgpu::Backends,
pub power_preference: wgpu::PowerPreference,
pub force_fallback_adapter: bool,
pub required_gpu_features: GpuFeatures,
pub requested_gpu_features: GpuFeatures,
pub additional_wgpu_features: wgpu::Features,
pub limits: wgpu::Limits,
pub label: Option<&'static str>,
}
impl Default for GraphicsContextDescriptor {
fn default() -> Self {
Self {
backends: wgpu::Backends::all(),
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
required_gpu_features: GpuFeatures::empty(),
requested_gpu_features: GpuFeatures::empty(),
additional_wgpu_features: wgpu::Features::empty(),
limits: wgpu::Limits::default(),
label: None,
}
}
}
impl GraphicsContextDescriptor {
pub fn new() -> Self {
Self::default()
}
pub fn require_features(mut self, features: GpuFeatures) -> Self {
self.required_gpu_features = features;
self
}
pub fn request_features(mut self, features: GpuFeatures) -> Self {
self.requested_gpu_features = features;
self
}
pub fn with_required_features(mut self, features: GpuFeatures) -> Self {
self.required_gpu_features |= features;
self
}
pub fn with_requested_features(mut self, features: GpuFeatures) -> Self {
self.requested_gpu_features |= features;
self
}
pub fn with_wgpu_features(mut self, features: wgpu::Features) -> Self {
self.additional_wgpu_features = features;
self
}
pub fn power_preference(mut self, preference: wgpu::PowerPreference) -> Self {
self.power_preference = preference;
self
}
pub fn backends(mut self, backends: wgpu::Backends) -> Self {
self.backends = backends;
self
}
pub fn limits(mut self, limits: wgpu::Limits) -> Self {
self.limits = limits;
self
}
pub fn label(mut self, label: &'static str) -> Self {
self.label = Some(label);
self
}
pub fn require_capability<T: RenderCapability>(mut self) -> Self {
let reqs = T::requirements();
self.required_gpu_features |= reqs.required_features;
self.required_gpu_features |= reqs.requested_features;
self.additional_wgpu_features |= reqs.additional_wgpu_features;
crate::capability::merge_limits_max(&mut self.limits, &reqs.min_limits);
tracing::trace!("Required capability: {}", T::name());
self
}
pub fn request_capability<T: RenderCapability>(mut self) -> Self {
let reqs = T::requirements();
self.required_gpu_features |= reqs.required_features;
self.requested_gpu_features |= reqs.requested_features;
self.additional_wgpu_features |= reqs.additional_wgpu_features;
crate::capability::merge_limits_max(&mut self.limits, &reqs.min_limits);
tracing::trace!("Requested capability: {}", T::name());
self
}
}
impl RenderContext for GraphicsContext {
fn create_buffer(&self, desc: &BufferDescriptor) -> GpuBuffer {
let buffer = self.device().create_buffer(desc);
GpuBuffer::from_wgpu(buffer)
}
fn write_buffer(&self, buffer: &GpuBuffer, offset: u64, data: &[u8]) {
let wgpu_buffer = buffer.as_wgpu();
self.queue().write_buffer(wgpu_buffer, offset, data);
}
fn create_texture(&self, desc: &TextureDescriptor) -> GpuTexture {
let texture = self.device().create_texture(desc);
GpuTexture::from_wgpu(texture)
}
fn create_shader_module(&self, desc: &ShaderModuleDescriptor) -> GpuShaderModule {
let module = self.device().create_shader_module(desc.clone());
GpuShaderModule::from_wgpu(module)
}
fn create_render_pipeline(&self, desc: &RenderPipelineDescriptor) -> GpuRenderPipeline {
let pipeline = self.device().create_render_pipeline(desc);
GpuRenderPipeline::from_wgpu(pipeline)
}
fn create_compute_pipeline(&self, desc: &ComputePipelineDescriptor) -> GpuComputePipeline {
let pipeline = self.device().create_compute_pipeline(desc);
GpuComputePipeline::from_wgpu(pipeline)
}
fn create_bind_group_layout(&self, desc: &BindGroupLayoutDescriptor) -> GpuBindGroupLayout {
let layout = self.device().create_bind_group_layout(desc);
GpuBindGroupLayout::from_wgpu(layout)
}
fn create_bind_group(&self, desc: &BindGroupDescriptor) -> GpuBindGroup {
let bind_group = self.device().create_bind_group(desc);
GpuBindGroup::from_wgpu(bind_group)
}
fn create_sampler(&self, desc: &SamplerDescriptor) -> GpuSampler {
let sampler = self.device().create_sampler(desc);
GpuSampler::from_wgpu(sampler)
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "mock")]
use super::*;
#[cfg(feature = "mock")]
use astrelis_test_utils::MockRenderContext;
#[test]
#[cfg(feature = "mock")]
fn test_render_context_trait_object() {
let mock_ctx = MockRenderContext::new();
fn uses_render_context(ctx: &dyn RenderContext) {
let buffer = ctx.create_buffer(&BufferDescriptor {
label: Some("Test Buffer"),
size: 256,
usage: wgpu::BufferUsages::UNIFORM,
mapped_at_creation: false,
});
ctx.write_buffer(&buffer, 0, &[0u8; 256]);
}
uses_render_context(&mock_ctx);
let calls = mock_ctx.calls();
assert_eq!(calls.len(), 2); }
}