sable-gpu 0.1.0

GPU abstraction layer for Sable Engine - wgpu-based rendering primitives
Documentation
//! GPU context and device management.
//!
//! The [`GpuContext`] is the main entry point for GPU operations.

use std::sync::Arc;

use sable_platform::prelude::Window;
use tracing::{debug, info};
use wgpu::{
    Adapter, Device, DeviceDescriptor, Features, Instance, InstanceDescriptor, Limits, PresentMode,
    Queue, Surface, SurfaceCapabilities, SurfaceConfiguration, TextureFormat, TextureUsages,
};

use crate::Result;

/// The main GPU context holding the instance, adapter, device, queue, and surface.
pub struct GpuContext {
    #[allow(dead_code)]
    instance: Instance,
    adapter: Adapter,
    device: Arc<Device>,
    queue: Arc<Queue>,
    surface: Surface<'static>,
    surface_config: SurfaceConfiguration,
    #[allow(dead_code)]
    surface_capabilities: SurfaceCapabilities,
}

impl GpuContext {
    /// Create a new GPU context for the given window.
    ///
    /// This is an async operation as it requires requesting the GPU adapter and device.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - No suitable GPU adapter could be found
    /// - The GPU device could not be requested
    /// - The surface could not be created
    pub async fn new(window: &Window) -> Result<Self> {
        // Create instance with default backends
        let instance = Instance::new(&InstanceDescriptor {
            backends: wgpu::Backends::all(),
            ..Default::default()
        });

        // Create surface from window
        // SAFETY: The window handle is valid for the lifetime of the window
        let surface = unsafe {
            let target = wgpu::SurfaceTargetUnsafe::from_window(&window.winit_window())?;
            instance.create_surface_unsafe(target)?
        };

        // Request adapter
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::HighPerformance,
                force_fallback_adapter: false,
                compatible_surface: Some(&surface),
            })
            .await?;

        info!(
            "Selected GPU adapter: {} ({:?})",
            adapter.get_info().name,
            adapter.get_info().backend
        );
        debug!("Adapter features: {:?}", adapter.features());
        debug!("Adapter limits: {:?}", adapter.limits());

        // Request device and queue
        let (device, queue) = adapter
            .request_device(&DeviceDescriptor {
                label: Some("Sable GPU Device"),
                required_features: Features::empty(),
                required_limits: Limits::default(),
                memory_hints: wgpu::MemoryHints::Performance,
                trace: wgpu::Trace::Off,
            })
            .await?;

        let device = Arc::new(device);
        let queue = Arc::new(queue);

        // Get surface capabilities
        let surface_capabilities = surface.get_capabilities(&adapter);
        let surface_format = surface_capabilities
            .formats
            .iter()
            .find(|f| f.is_srgb())
            .copied()
            .unwrap_or(surface_capabilities.formats[0]);

        debug!("Surface format: {:?}", surface_format);
        debug!(
            "Available present modes: {:?}",
            surface_capabilities.present_modes
        );

        // Configure surface
        let size = window.inner_size();
        let present_mode = if window.vsync() {
            if surface_capabilities
                .present_modes
                .contains(&PresentMode::Fifo)
            {
                PresentMode::Fifo
            } else {
                surface_capabilities.present_modes[0]
            }
        } else if surface_capabilities
            .present_modes
            .contains(&PresentMode::Immediate)
        {
            PresentMode::Immediate
        } else if surface_capabilities
            .present_modes
            .contains(&PresentMode::Mailbox)
        {
            PresentMode::Mailbox
        } else {
            surface_capabilities.present_modes[0]
        };

        let surface_config = SurfaceConfiguration {
            usage: TextureUsages::RENDER_ATTACHMENT,
            format: surface_format,
            width: size.width.max(1),
            height: size.height.max(1),
            present_mode,
            alpha_mode: surface_capabilities.alpha_modes[0],
            view_formats: vec![],
            desired_maximum_frame_latency: 2,
        };

        surface.configure(&device, &surface_config);

        Ok(Self {
            instance,
            adapter,
            device,
            queue,
            surface,
            surface_config,
            surface_capabilities,
        })
    }

    /// Resize the surface to a new size.
    ///
    /// Call this when the window is resized.
    pub fn resize(&mut self, width: u32, height: u32) {
        if width > 0 && height > 0 {
            self.surface_config.width = width;
            self.surface_config.height = height;
            self.surface.configure(&self.device, &self.surface_config);
        }
    }

    /// Get the current surface texture for rendering.
    ///
    /// # Errors
    ///
    /// Returns an error if the surface texture could not be acquired.
    pub fn get_current_texture(&self) -> Result<wgpu::SurfaceTexture> {
        Ok(self.surface.get_current_texture()?)
    }

    /// Get a reference to the GPU device.
    #[must_use]
    pub fn device(&self) -> &Arc<Device> {
        &self.device
    }

    /// Get a reference to the GPU queue.
    #[must_use]
    pub fn queue(&self) -> &Arc<Queue> {
        &self.queue
    }

    /// Get the surface texture format.
    #[must_use]
    pub fn surface_format(&self) -> TextureFormat {
        self.surface_config.format
    }

    /// Get the current surface size.
    #[must_use]
    pub fn surface_size(&self) -> (u32, u32) {
        (self.surface_config.width, self.surface_config.height)
    }

    /// Get the adapter info.
    #[must_use]
    pub fn adapter_info(&self) -> wgpu::AdapterInfo {
        self.adapter.get_info()
    }

    /// Create a command encoder for recording GPU commands.
    #[must_use]
    pub fn create_command_encoder(&self) -> wgpu::CommandEncoder {
        self.device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Sable Command Encoder"),
            })
    }

    /// Submit command buffers to the GPU queue.
    pub fn submit<I: IntoIterator<Item = wgpu::CommandBuffer>>(&self, commands: I) {
        self.queue.submit(commands);
    }
}