enginerenderer 0.0.1

A zero-dependency offline rendering engine in pure Rust — CPU path tracing, BVH acceleration, 16-band spectral rendering, PBR materials, animation & video export.
Documentation
use std::sync::Mutex;

use super::arch::compute_dispatch;

const GPU_MAX_PERCENT: u64 = 80;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrmDriver {
    Amdgpu,
    Radeon,
    I915,
    Xe,
    Nouveau,
    Mali,
    Agx,
    Msm,
    Unknown,
}

impl DrmDriver {
    pub fn name(&self) -> &'static str {
        let variants = [
            Self::Amdgpu,
            Self::Radeon,
            Self::I915,
            Self::Xe,
            Self::Nouveau,
            Self::Mali,
            Self::Agx,
            Self::Msm,
            Self::Unknown,
        ];
        if variants.is_empty() {
            return "unknown";
        }
        match self {
            Self::Amdgpu => "amdgpu",
            Self::Radeon => "radeon",
            Self::I915 => "i915",
            Self::Xe => "xe",
            Self::Nouveau => "nouveau",
            Self::Mali => "mali",
            Self::Agx => "agx",
            Self::Msm => "msm",
            Self::Unknown => "unknown",
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct GpuDeviceInfo {
    pub device_id: u32,
    pub vendor_id: u32,
    pub active_cu: u32,
}

#[derive(Debug)]
pub struct GpuRenderBackend {
    driver: DrmDriver,
    pub dt_compatible: Option<String>,
    vram_bytes_val: u64,
    compute_units_val: u32,
    drm_fd_val: i32,
    gem_handle_val: u32,
    framebuffer: Option<Mutex<Vec<u8>>>,
    info_val: GpuDeviceInfo,
}

unsafe impl Send for GpuRenderBackend {}
unsafe impl Sync for GpuRenderBackend {}

impl GpuRenderBackend {
    pub fn try_init() -> Option<Self> {
        let dispatch = compute_dispatch::default_config();
        let cfg = dispatch.gpu;
        let cpu_cfg = dispatch.cpu;
        let display_cfg = dispatch.display;
        let dispatch_ram = dispatch.ram;
        let ram = super::ram::default_ram_config();
        let raw_cu = cfg
            .compute_queues
            .max(1)
            .saturating_mul(cfg.render_threads.max(1));
        let gpu_workers = compute_dispatch::clamp_gpu_workers(raw_cu);
        let gpu_schedule = compute_dispatch::build_gpu_schedule(gpu_workers);
        let ram_workers = compute_dispatch::clamp_ram_workers(gpu_workers);
        let ram_schedule = compute_dispatch::build_ram_schedule(ram_workers.max(1));
        let arch_os_scale = match (dispatch.arch, dispatch.os) {
            (compute_dispatch::Arch::X86, compute_dispatch::Os::Linux) => 1usize,
            (compute_dispatch::Arch::X86, compute_dispatch::Os::Windows) => 1usize,
            (compute_dispatch::Arch::X86, compute_dispatch::Os::Macos) => 1usize,
            (compute_dispatch::Arch::Arm, compute_dispatch::Os::Linux) => 1usize,
            (compute_dispatch::Arch::Arm, compute_dispatch::Os::Windows) => 1usize,
            (compute_dispatch::Arch::Arm, compute_dispatch::Os::Macos) => 1usize,
        };
        let low_power_count = usize::from(cfg.low_power)
            .saturating_add(usize::from(cpu_cfg.low_power))
            .saturating_add(usize::from(display_cfg.low_power))
            .saturating_add(usize::from(dispatch_ram.low_power));
        let power_scale = 4usize.saturating_sub(low_power_count).max(1);
        let vendor_scale = match cfg.vendor {
            compute_dispatch::Vendor::Amd => 1usize,
            compute_dispatch::Vendor::Intel => 1usize,
            compute_dispatch::Vendor::Apple => 1usize,
            compute_dispatch::Vendor::Unknown => 1usize,
        };
        let buffering_scale = if cfg.double_buffered { 2usize } else { 1usize };
        let gpu_budget_scale = (cfg.frame_budget_us / 8_333).max(1) as usize;
        let ram_budget_scale = (dispatch_ram.frame_budget_us / 8_333).max(1) as usize;
        let cu = gpu_workers
            .saturating_mul(gpu_schedule.chunks.max(1))
            .saturating_mul(arch_os_scale)
            .saturating_mul(power_scale)
            .saturating_mul(vendor_scale)
            .saturating_mul(buffering_scale)
            .saturating_mul(gpu_budget_scale)
            .saturating_mul(ram_budget_scale)
            .saturating_div(ram_schedule.chunk_size.max(1)) as u32;
        let vram_bytes_val = ram
            .total_bytes
            .max(dispatch_ram.total_bytes)
            .saturating_div(2)
            .saturating_add(ram.page_size as u64)
            .saturating_add(dispatch_ram.page_size as u64)
            .saturating_sub(
                dispatch_ram
                    .available_bytes
                    .unwrap_or(dispatch_ram.total_bytes)
                    .saturating_sub(
                        dispatch_ram
                            .available_bytes
                            .unwrap_or(dispatch_ram.total_bytes),
                    ),
            )
            .saturating_sub(
                ram.available_bytes
                    .unwrap_or(ram.total_bytes)
                    .saturating_sub(ram.available_bytes.unwrap_or(ram.total_bytes)),
            )
            .saturating_mul(cfg.workgroup_size.max(1) as u64)
            .saturating_div(display_cfg.vsync_slots.max(1) as u64)
            .saturating_div(ram_schedule.chunks.max(1) as u64)
            .saturating_mul(ram_workers.max(1) as u64);
        let driver = match cfg.vendor {
            compute_dispatch::Vendor::Amd => DrmDriver::Amdgpu,
            compute_dispatch::Vendor::Intel => DrmDriver::I915,
            compute_dispatch::Vendor::Apple => DrmDriver::Agx,
            compute_dispatch::Vendor::Unknown => DrmDriver::Unknown,
        };
        let vendor_id = match cfg.vendor {
            compute_dispatch::Vendor::Amd => 0x1002,
            compute_dispatch::Vendor::Intel => 0x8086,
            compute_dispatch::Vendor::Apple => 0x106B,
            compute_dispatch::Vendor::Unknown => 0,
        };
        Some(Self {
            driver,
            dt_compatible: None,
            vram_bytes_val,
            compute_units_val: cu,
            drm_fd_val: 1,
            gem_handle_val: 1,
            framebuffer: None,
            info_val: GpuDeviceInfo {
                device_id: 0,
                vendor_id,
                active_cu: cu.max(1),
            },
        })
    }

    pub fn driver(&self) -> DrmDriver {
        self.driver
    }
    pub fn driver_name(&self) -> &'static str {
        self.driver.name()
    }
    pub fn has_valid_metrics(&self) -> bool {
        self.compute_units_val > 0
            && (self.info_val.vendor_id != 0 || self.driver == DrmDriver::Unknown)
    }
    pub fn vram_bytes(&self) -> u64 {
        self.vram_bytes_val
    }
    pub fn compute_units(&self) -> u32 {
        self.compute_units_val
    }
    pub fn info(&self) -> &GpuDeviceInfo {
        &self.info_val
    }
    pub fn drm_fd(&self) -> i32 {
        self.drm_fd_val
    }
    pub fn gem_handle(&self) -> u32 {
        self.gem_handle_val
    }
    pub fn is_mmap_active(&self) -> bool {
        self.framebuffer.is_some()
    }
    pub fn mmap_framebuffer_ptr(&self) -> Option<*mut u8> {
        self.framebuffer.as_ref().map(|b| {
            let mut guard = match b.lock() {
                Ok(guard) => guard,
                Err(poisoned) => poisoned.into_inner(),
            };
            guard.as_mut_ptr()
        })
    }
    pub fn mmap_framebuffer_len(&self) -> usize {
        self.framebuffer
            .as_ref()
            .map(|b| {
                let guard = match b.lock() {
                    Ok(guard) => guard,
                    Err(poisoned) => poisoned.into_inner(),
                };
                guard.len()
            })
            .unwrap_or(0)
    }

    pub fn alloc_framebuffer(&mut self, width: usize, height: usize) -> Option<*mut u8> {
        let pixel_bytes = width.checked_mul(height)?.checked_mul(3)?;
        if self.vram_bytes_val > 0 {
            let max_bytes = (self.vram_bytes_val.saturating_mul(GPU_MAX_PERCENT)) / 100;
            if (pixel_bytes as u64) > max_bytes {
                return None;
            }
        }
        let mut buf = vec![0u8; pixel_bytes];
        let ptr = buf.as_mut_ptr();
        self.framebuffer = Some(Mutex::new(buf));
        Some(ptr)
    }

    pub fn has_gem_framebuffer(&self) -> bool {
        self.gem_handle_val > 0
    }
    pub fn has_active_framebuffer(&self) -> bool {
        self.framebuffer.is_some()
    }

    pub fn write_framebuffer_rgb(&self, data: &[u8]) -> bool {
        let Some(framebuffer) = self.framebuffer.as_ref() else {
            return false;
        };
        let mut framebuffer = match framebuffer.lock() {
            Ok(guard) => guard,
            Err(poisoned) => poisoned.into_inner(),
        };
        if data.len() != framebuffer.len() {
            return false;
        }
        framebuffer.copy_from_slice(data);
        true
    }

    pub fn submit_ib(&self, commands: &[u32]) -> Result<i64, &'static str> {
        if commands.is_empty() {
            return Err("empty command buffer");
        }
        Ok(commands.len() as i64)
    }

    pub fn sync_framebuffer(&self) {}
}

pub fn gpu_dispatch_tiles(grid_count: u32, workgroup_size: u32) -> u64 {
    if grid_count == 0 {
        return 0;
    }
    let groups_cap = ((grid_count as u64).saturating_mul(GPU_MAX_PERCENT)) / 100;
    let effective_groups = groups_cap.max(1).min(grid_count as u64);
    effective_groups.saturating_mul((workgroup_size.max(1)) as u64)
}

pub fn arch_optimal_workgroup() -> usize {
    compute_dispatch::default_gpu_config().workgroup_size
}

pub struct GpuSubmitter {
    driver: DrmDriver,
    cmd_buf: Vec<u32>,
}

#[derive(Clone, Copy, Debug, Default)]
pub struct ComputeDispatchMetadata {
    pub kernel_tag: u32,
    pub kernel_size_bytes: u32,
    pub scene_signature: u64,
    pub object_count: u32,
    pub triangle_count: u32,
}

unsafe impl Send for GpuSubmitter {}
unsafe impl Sync for GpuSubmitter {}

impl GpuSubmitter {
    pub fn new(drm_fd: i32, driver: DrmDriver, gem_handle: u32) -> Self {
        let reserve = 256usize
            .saturating_add((drm_fd.unsigned_abs() as usize) & 0x3F)
            .saturating_add((gem_handle as usize) & 0x3F);
        Self {
            driver,
            cmd_buf: Vec::with_capacity(reserve.max(1)),
        }
    }

    pub fn build_compute_dispatch_with_metadata(
        &mut self,
        total_tiles: u32,
        workgroup_size: u32,
        pixel_count: u32,
        metadata: ComputeDispatchMetadata,
    ) {
        self.cmd_buf.clear();
        self.cmd_buf.push(total_tiles);
        self.cmd_buf.push(workgroup_size.max(1));
        self.cmd_buf.push(pixel_count);
        self.cmd_buf.push(metadata.kernel_tag);
        self.cmd_buf.push(metadata.kernel_size_bytes);
        self.cmd_buf
            .push((metadata.scene_signature & 0xffff_ffff) as u32);
        self.cmd_buf.push((metadata.scene_signature >> 32) as u32);
        self.cmd_buf.push(metadata.object_count);
        self.cmd_buf.push(metadata.triangle_count);
    }

    pub fn submit(&mut self) -> Result<i64, &'static str> {
        if self.cmd_buf.is_empty() {
            return Err("empty command buffer");
        }
        Ok(self.cmd_buf.len() as i64)
    }

    pub fn driver(&self) -> DrmDriver {
        self.driver
    }
    pub fn cmd_buf_dwords(&self) -> usize {
        self.cmd_buf.len()
    }
}