//! GPU acquisition: instance, adapter, device, queue.

use super::RenderError;

/// A handle to the GPU: instance, adapter, and the device/queue pair every
/// rendering operation goes through.
///
/// Created either for a window surface (via [`RenderContext`]) or headless
/// with [`Gpu::headless`] for offscreen rendering and tests.
///
/// [`RenderContext`]: super::RenderContext
pub struct Gpu {
    instance: wgpu::Instance,
    adapter: wgpu::Adapter,
    device: wgpu::Device,
    queue: wgpu::Queue,
}

impl Gpu {
    /// Acquires an adapter and device from an existing `instance`, preferring
    /// an adapter that can present to `compatible_surface` when one is given.
    ///
    /// `force_fallback_adapter` requests a software adapter (e.g. llvmpipe),
    /// used as a last resort when no hardware adapter works.
    pub(crate) fn with_instance(
        instance: wgpu::Instance,
        compatible_surface: Option<&wgpu::Surface<'_>>,
        force_fallback_adapter: bool,
    ) -> Result<Self, RenderError> {
        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
            power_preference: wgpu::PowerPreference::HighPerformance,
            force_fallback_adapter,
            compatible_surface,
        }))?;
        log::info!(
            "GPU adapter: {} ({:?})",
            adapter.get_info().name,
            adapter.get_info().backend
        );
        let (device, queue) =
            pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
                label: Some("oxide.device"),
                ..Default::default()
            }))?;
        Ok(Self {
            instance,
            adapter,
            device,
            queue,
        })
    }

    /// Acquires the GPU without any surface, for offscreen rendering and
    /// automated tests.
    ///
    /// Tries a hardware adapter first, then falls back to a software adapter
    /// (e.g. llvmpipe) so headless rendering also works on machines without a
    /// usable GPU.
    pub fn headless() -> Result<Self, RenderError> {
        // `from_env` keeps backend/flags overridable via WGPU_* env vars.
        let instance =
            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
        match Self::with_instance(instance, None, false) {
            Ok(gpu) => Ok(gpu),
            Err(hardware_err) => {
                log::warn!("no hardware GPU adapter ({hardware_err}); trying software fallback");
                let instance = wgpu::Instance::new(
                    wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
                );
                Self::with_instance(instance, None, true)
            }
        }
    }

    /// The wgpu instance the adapter was created from.
    pub fn instance(&self) -> &wgpu::Instance {
        &self.instance
    }

    /// The physical adapter in use.
    pub fn adapter(&self) -> &wgpu::Adapter {
        &self.adapter
    }

    /// The logical device used to create GPU resources.
    pub fn device(&self) -> &wgpu::Device {
        &self.device
    }

    /// The queue used to submit command buffers.
    pub fn queue(&self) -> &wgpu::Queue {
        &self.queue
    }
}
