amdgpu: VRAM-backed GEM allocator, CS ioctl docs, firmware stub

- Add VramManager (vram.rs): bump-allocator with free-list coalescing for BAR2 VRAM
  aperture. gem_create auto-selects VRAM for scanout buffers (width>0 && height>0)
  with fallback to system RAM on exhaustion. gem_close frees VRAM when gpu_addr is
  within BAR2 range. ensure_gem_gpu_mapping detects VRAM-backed buffers and skips
  GTT mapping.

- Add amdgpu_dc_upload_firmware() stub documenting DMUB firmware upload sequence
  prerequisites (requires Linux DC tree compilation).

- Replace generic 'unavailable' CS ioctl/virgl error messages with specific
  messages documenting what component is needed (amdgpu core driver, Mesa radeonsi/
  iris cross-compilation, CS ioctl backend).
This commit is contained in:
2026-05-31 22:59:46 +03:00
parent 0a3e1fa7db
commit 61f758a881
4 changed files with 331 additions and 55 deletions
@@ -371,6 +371,25 @@ int amdgpu_dc_init(void *mmio_base, size_t mmio_size)
return ret;
}
int amdgpu_dc_upload_firmware(void)
{
const struct asic_props *props = asic_props();
printk("amdgpu_redox: firmware upload stub — DMUB upload requires imported DC tree\n");
if (!props) {
pr_err("amdgpu_redox: cannot determine DMUB firmware target without ASIC identification\n");
return -ENODEV;
}
printk("amdgpu_redox: DMUB firmware upload not yet implemented for %s.\n", props->name);
printk("amdgpu_redox: Required: compile display/dmub/ from imported Linux DC tree,\n");
printk("amdgpu_redox: then call dmub_srv_create() / dmub_srv_hw_init() / dmub_srv_cmd_submit().\n");
printk("amdgpu_redox: Until DC tree is compiled, DMUB-assisted features (PSR, ABM, etc.) are unavailable.\n");
return 0;
}
/* Initialize AMD GPU hardware for display */
int amdgpu_redox_init(void *mmio_base, size_t mmio_size, uint64_t fb_phys, size_t fb_size)
{
@@ -124,7 +124,7 @@ pub trait GpuDriver: Send + Sync {
_submit: &RedoxPrivateCsSubmit,
) -> Result<RedoxPrivateCsSubmitResult> {
Err(DriverError::Unsupported(
"private command submission is unavailable on this backend",
"GPU command submission requires amdgpu core driver (imported Linux tree not yet compiled; see local/recipes/gpu/amdgpu-source/) and a per-generation ring buffer backend",
))
}
@@ -133,7 +133,7 @@ pub trait GpuDriver: Send + Sync {
_wait: &RedoxPrivateCsWait,
) -> Result<RedoxPrivateCsWaitResult> {
Err(DriverError::Unsupported(
"private command completion waits are unavailable on this backend",
"GPU fence/completion wait requires amdgpu core driver (imported Linux tree not yet compiled)",
))
}
@@ -142,15 +142,19 @@ pub trait GpuDriver: Send + Sync {
}
fn virgl_get_capset_info(&self, _capset_index: u32) -> Result<VirglCapsetInfo> {
Err(DriverError::Unsupported("virgl capset info"))
Err(DriverError::Unsupported(
"virgl 3D requires Mesa virgl driver (swrast backend only; gallium-drivers=swrast,virgl not yet fully compiled for Redox target)",
))
}
fn virgl_get_capset(&self, _capset_id: u32, _capset_version: u32) -> Result<VirglCapset> {
Err(DriverError::Unsupported("virgl capset"))
Err(DriverError::Unsupported("virgl capset requires Mesa virgl cross-compilation"))
}
fn virgl_ctx_create(&self, _ctx_id: u32, _debug_name: &str, _context_init: u32) -> Result<()> {
Err(DriverError::Unsupported("virgl context creation"))
Err(DriverError::Unsupported(
"virgl GL context creation requires Mesa virgl driver + redox-drm CS ioctl backend",
))
}
fn virgl_ctx_destroy(&self, _ctx_id: u32) -> Result<()> {
@@ -172,56 +176,39 @@ pub trait GpuDriver: Send + Sync {
_flags: u32,
_ctx_id: u32,
) -> Result<()> {
Err(DriverError::Unsupported("virgl 3D resource creation"))
Err(DriverError::Unsupported(
"virgl 3D resource creation requires Mesa radeonsi/iris cross-compilation for hardware, or llvmpipe for software rendering",
))
}
fn virgl_submit_3d(&self, _ctx_id: u32, _command_data: &[u8]) -> Result<()> {
Err(DriverError::Unsupported("virgl submit 3D"))
Err(DriverError::Unsupported(
"virgl command submission requires compiled Mesa gallium driver and CS ioctl backend",
))
}
fn virgl_transfer_to_host_3d(
&self,
_ctx_id: u32,
_resource_id: u32,
_x: u32,
_y: u32,
_z: u32,
_w: u32,
_h: u32,
_d: u32,
_offset: u64,
_level: u32,
_stride: u32,
_layer_stride: u32,
_ctx_id: u32, _resource_id: u32, _x: u32, _y: u32, _z: u32,
_w: u32, _h: u32, _d: u32, _offset: u64, _level: u32,
_stride: u32, _layer_stride: u32,
) -> Result<()> {
Err(DriverError::Unsupported("virgl transfer to host"))
Err(DriverError::Unsupported("virgl transfer-to-host requires Mesa virgl cross-compilation"))
}
fn virgl_transfer_from_host_3d(
&self,
_ctx_id: u32,
_resource_id: u32,
_x: u32,
_y: u32,
_z: u32,
_w: u32,
_h: u32,
_d: u32,
_offset: u64,
_level: u32,
_stride: u32,
_layer_stride: u32,
_ctx_id: u32, _resource_id: u32, _x: u32, _y: u32, _z: u32,
_w: u32, _h: u32, _d: u32, _offset: u64, _level: u32,
_stride: u32, _layer_stride: u32,
) -> Result<()> {
Err(DriverError::Unsupported("virgl transfer from host"))
Err(DriverError::Unsupported("virgl transfer-from-host requires Mesa virgl cross-compilation"))
}
fn virgl_resource_attach_backing(
&self,
_resource_id: u32,
_phys_addr: u64,
_length: u32,
&self, _resource_id: u32, _phys_addr: u64, _length: u32,
) -> Result<()> {
Err(DriverError::Unsupported("virgl resource attach backing"))
Err(DriverError::Unsupported("virgl resource attach backing requires Mesa virgl + redox-drm GEM integration"))
}
}
@@ -1,8 +1,9 @@
pub mod display;
pub mod gtt;
pub mod ring;
pub mod vram;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Mutex;
@@ -21,6 +22,7 @@ use crate::kms::{ConnectorInfo, ModeInfo};
use self::display::DisplayCore;
use self::gtt::GttManager;
use self::ring::RingManager;
use self::vram::VramManager;
const AMD_IH_RB_CNTL: usize = 0x0080;
const AMD_IH_RB_RPTR: usize = 0x0083;
@@ -55,6 +57,9 @@ pub struct AmdDriver {
vblank_count: AtomicU64,
hotplug_pending: AtomicBool,
firmware: HashMap<String, Vec<u8>>,
vram: Mutex<VramManager>,
bar2_start: u64,
bar2_size: u64,
}
impl AmdDriver {
@@ -150,6 +155,8 @@ impl AmdDriver {
irq_mode
);
let vram = VramManager::new(fb_phys, fb_size as u64);
Ok(Self {
info,
mmio,
@@ -164,6 +171,9 @@ impl AmdDriver {
vblank_count: AtomicU64::new(0),
hotplug_pending: AtomicBool::new(false),
firmware,
vram: Mutex::new(vram),
bar2_start: fb_phys,
bar2_size: fb_size as u64,
})
}
@@ -339,6 +349,16 @@ impl AmdDriver {
(obj.phys_addr as u64, obj.size)
};
if phys_addr >= self.bar2_start
&& phys_addr < self.bar2_start + self.bar2_size
{
self.gem
.lock()
.map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?
.set_gpu_addr(fb_handle, phys_addr)?;
return Ok(phys_addr);
}
let gpu_addr = {
let mut gtt = self
.gtt
@@ -479,33 +499,83 @@ impl GpuDriver for AmdDriver {
Ok(self.vblank_count.load(Ordering::SeqCst))
}
fn gem_create(&self, size: u64, _width: u32, _height: u32) -> Result<GemHandle> {
let mut gem = self
.gem
fn gem_create(&self, size: u64, width: u32, height: u32) -> Result<GemHandle> {
if width > 0 && height > 0 {
let mut vram = self
.vram
.lock()
.map_err(|_| DriverError::Buffer("VRAM manager poisoned".to_string()))?;
let vram_addr = match vram.allocate(size) {
Ok(addr) => addr,
Err(_) => {
log::warn!(
"redox-drm: VRAM allocation failed for {}x{} ({} bytes), falling back to system RAM",
width, height, size
);
return self
.gem
.lock()
.map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?
.create(size);
}
};
let handle = {
let mut gem = self
.gem
.lock()
.map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?;
let handle = gem.create(size)?;
gem.set_gpu_addr(handle, vram_addr)?;
debug!(
"redox-drm: created VRAM-backed GEM handle {} for {}x{} at VRAM {:#x}",
handle, width, height, vram_addr
);
handle
};
return Ok(handle);
}
self.gem
.lock()
.map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?;
gem.create(size)
.map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?
.create(size)
}
fn gem_close(&self, handle: GemHandle) -> Result<()> {
let gpu_info = {
let (gpu_addr, size, phys_addr) = {
let gem = self
.gem
.lock()
.map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?;
let obj = gem.object(handle)?;
(obj.gpu_addr, obj.size)
(obj.gpu_addr, obj.size, obj.phys_addr as u64)
};
if let (Some(gpu_addr), fb_size) = gpu_info {
let mut gtt = self
.gtt
.lock()
.map_err(|_| DriverError::Initialization("GTT manager poisoned".to_string()))?;
gtt.flush_tlb(&self.mmio)?;
gtt.unmap_range(gpu_addr, fb_size)?;
gtt.release_range(gpu_addr, fb_size);
if let Some(gpu_addr) = gpu_addr {
if gpu_addr >= self.bar2_start
&& gpu_addr < self.bar2_start + self.bar2_size
{
let mut vram = self
.vram
.lock()
.map_err(|_| DriverError::Buffer("VRAM manager poisoned".to_string()))?;
vram.free(gpu_addr, size);
debug!(
"redox-drm: freed VRAM GEM handle {} at {:#x} ({} bytes)",
handle, gpu_addr, size
);
} else {
let mut gtt = self
.gtt
.lock()
.map_err(|_| DriverError::Initialization("GTT manager poisoned".to_string()))?;
gtt.flush_tlb(&self.mmio)?;
gtt.unmap_range(gpu_addr, size)?;
gtt.release_range(gpu_addr, size);
}
}
let _ = phys_addr;
self.gem
.lock()
@@ -576,7 +646,8 @@ impl GpuDriver for AmdDriver {
Ok(Some(DriverEvent::Vblank { crtc_id, count }))
}
Some(DriverEvent::Hotplug { connector_id }) => {
info!(
let vram = VramManager::new(fb_phys, fb_size as u64);
info!("redox-drm: {}", vram.summary());
"redox-drm: handled AMD hotplug IRQ for {} connector {} irq={:?}",
self.info.location, connector_id, irq
);
@@ -0,0 +1,199 @@
use log::{debug, info};
use crate::driver::{DriverError, Result};
const VRAM_PAGE_SIZE: u64 = 4096;
const VRAM_MIN_ALIGNMENT: u64 = 256;
const VRAM_MAX_ALLOC: u64 = 64 * 1024 * 1024;
pub struct VramManager {
bar_start: u64,
bar_size: u64,
next_alloc: u64,
allocated: u64,
free_list: Vec<(u64, u64)>,
peak_allocated: u64,
}
impl VramManager {
pub fn new(bar_start: u64, bar_size: u64) -> Self {
let usable = if bar_size > VRAM_PAGE_SIZE {
bar_size - VRAM_PAGE_SIZE
} else {
0
};
Self {
bar_start,
bar_size,
next_alloc: bar_start + VRAM_PAGE_SIZE,
allocated: 0,
free_list: Vec::new(),
peak_allocated: VRAM_PAGE_SIZE,
}
}
pub fn allocate(&mut self, size: u64) -> Result<u64> {
if size == 0 {
return Err(DriverError::InvalidArgument(
"VRAM allocation size must be non-zero",
));
}
if size > VRAM_MAX_ALLOC {
return Err(DriverError::InvalidArgument(
"VRAM allocation exceeds single-buffer limit",
));
}
let aligned = align_up(size, VRAM_MIN_ALIGNMENT);
if let Some(idx) = self
.free_list
.iter()
.position(|&(_, free_size)| free_size >= aligned)
{
let (start, free_size) = self.free_list.remove(idx);
let remainder = free_size - aligned;
if remainder >= VRAM_MIN_ALIGNMENT {
self.free_list.push((start + aligned, remainder));
}
self.allocated += aligned;
if self.allocated > self.peak_allocated {
self.peak_allocated = self.allocated;
}
debug!(
"redox-drm: VRAM alloc {:#x} bytes at {:#x} (from free list)",
aligned, start
);
return Ok(start);
}
let addr = self.next_alloc;
let new_next = match addr.checked_add(aligned) {
Some(n) => n,
None => {
return Err(DriverError::Buffer(
"VRAM allocation address overflow".into(),
));
}
};
let bar_end = self.bar_start + self.bar_size;
if new_next > bar_end {
return Err(DriverError::Buffer(format!(
"VRAM exhausted: need {:#x}..{:#x}, BAR2 ends at {:#x} (allocated: {:#x})",
addr, new_next, bar_end, self.allocated
)));
}
self.next_alloc = new_next;
self.allocated += aligned;
if self.allocated > self.peak_allocated {
self.peak_allocated = self.allocated;
}
debug!(
"redox-drm: VRAM alloc {:#x} bytes at {:#x} (bump, next={:#x})",
aligned, addr, self.next_alloc
);
Ok(addr)
}
pub fn free(&mut self, addr: u64, size: u64) {
if addr < self.bar_start || size == 0 {
return;
}
let aligned = align_up(size, VRAM_MIN_ALIGNMENT);
self.allocated = self.allocated.saturating_sub(aligned);
self.free_list.push((addr, aligned));
self.coalesce();
debug!(
"redox-drm: VRAM free {:#x} bytes at {:#x} (remaining allocated: {:#x})",
aligned, addr, self.allocated
);
}
fn coalesce(&mut self) {
self.free_list
.sort_by(|(a_start, _), (b_start, _)| a_start.cmp(b_start));
let mut merged: Vec<(u64, u64)> = Vec::with_capacity(self.free_list.len());
for &(start, size) in &self.free_list {
if let Some(last) = merged.last_mut() {
if last.0 + last.1 == start {
last.1 += size;
continue;
}
}
merged.push((start, size));
}
self.free_list = merged;
}
pub fn bar_start(&self) -> u64 {
self.bar_start
}
pub fn bar_size(&self) -> u64 {
self.bar_size
}
pub fn usable_end(&self) -> u64 {
self.bar_start + self.bar_size
}
pub fn summary(&self) -> String {
format!(
"BAR2 {:#x}..{:#x} ({:.1} MiB), allocated {:.1} MiB, peak {:.1} MiB, {} free fragments",
self.bar_start,
self.bar_start + self.bar_size,
self.bar_size as f64 / (1024.0 * 1024.0),
self.allocated as f64 / (1024.0 * 1024.0),
self.peak_allocated as f64 / (1024.0 * 1024.0),
self.free_list.len()
)
}
}
fn align_up(value: u64, alignment: u64) -> u64 {
(value + alignment - 1) & !(alignment - 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allocate_and_free() {
let mut mgr = VramManager::new(0x1000_0000, 64 * 1024 * 1024);
let a = mgr.allocate(4096).expect("first alloc");
assert!(a >= 0x1000_1000);
mgr.free(a, 4096);
assert!(mgr.allocated < 4096 + VRAM_PAGE_SIZE * 2);
}
#[test]
fn reuse_freed() {
let mut mgr = VramManager::new(0x1000_0000, 64 * 1024 * 1024);
let a = mgr.allocate(32768).expect("alloc");
mgr.free(a, 32768);
let b = mgr.allocate(16384).expect("realloc from free list");
assert_eq!(a, b, "reallocation should reuse the same start");
}
#[test]
fn exhausted_returns_error() {
let mut mgr = VramManager::new(0x1000_0000, 4096 * 4);
assert!(mgr.allocate(4096 * 4).is_ok());
assert!(mgr.allocate(4096).is_err());
}
#[test]
fn summary_string() {
let mgr = VramManager::new(0x1000_0000, 8 * 1024 * 1024);
let s = mgr.summary();
assert!(s.contains("BAR2"));
assert!(s.contains("MiB"));
}
}