use std::ffi::{CStr, c_char, c_int, c_void};
use std::fmt;
use std::hash::{BuildHasher, RandomState};
use std::sync::{Mutex, OnceLock, PoisonError};
use std::thread::ThreadId;
use ash::vk;
use wgpu::hal::api::Vulkan;
use wgpu::hal::vulkan::TextureMemory;
const ACQUIRE_TIMEOUT_NS: u64 = 1_000_000_000;
const BUFFER_FORMAT: u32 = ndk_sys::AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM.0;
#[allow(clippy::unnecessary_cast)]
const BUFFER_USAGE_SAMPLED: u64 =
ndk_sys::AHardwareBuffer_UsageFlags::AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE.0 as u64;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum HardwareBufferImportError {
Unsupported(String),
Failed(String),
}
impl fmt::Display for HardwareBufferImportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported(reason) => write!(f, "hardware-buffer import unsupported: {reason}"),
Self::Failed(reason) => write!(f, "hardware-buffer import failed: {reason}"),
}
}
}
impl std::error::Error for HardwareBufferImportError {}
struct BufferApi {
acquire: unsafe extern "C" fn(*mut ndk_sys::AHardwareBuffer),
release: unsafe extern "C" fn(*mut ndk_sys::AHardwareBuffer),
describe:
unsafe extern "C" fn(*const ndk_sys::AHardwareBuffer, *mut ndk_sys::AHardwareBuffer_Desc),
}
unsafe extern "C" {
fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}
#[cfg(target_pointer_width = "64")]
const RTLD_NOW: c_int = 2;
#[cfg(not(target_pointer_width = "64"))]
const RTLD_NOW: c_int = 0;
fn buffer_api() -> Option<&'static BufferApi> {
static API: OnceLock<Option<BufferApi>> = OnceLock::new();
API.get_or_init(|| {
unsafe {
let library = dlopen(c"libandroid.so".as_ptr(), RTLD_NOW);
if library.is_null() {
return None;
}
let symbol = |name: &CStr| {
let address = dlsym(library, name.as_ptr());
(!address.is_null()).then_some(address)
};
Some(BufferApi {
acquire: std::mem::transmute::<
*mut c_void,
unsafe extern "C" fn(*mut ndk_sys::AHardwareBuffer),
>(symbol(c"AHardwareBuffer_acquire")?),
release: std::mem::transmute::<
*mut c_void,
unsafe extern "C" fn(*mut ndk_sys::AHardwareBuffer),
>(symbol(c"AHardwareBuffer_release")?),
describe: std::mem::transmute::<
*mut c_void,
unsafe extern "C" fn(
*const ndk_sys::AHardwareBuffer,
*mut ndk_sys::AHardwareBuffer_Desc,
),
>(symbol(c"AHardwareBuffer_describe")?),
})
}
})
.as_ref()
}
static IMPORT_THREAD: Mutex<Option<(u64, ThreadId)>> = Mutex::new(None);
fn check_import_thread(device: &wgpu::Device) {
if !cfg!(debug_assertions) {
return;
}
static HASHER: OnceLock<RandomState> = OnceLock::new();
let identity = HASHER.get_or_init(RandomState::new).hash_one(device);
let current = std::thread::current().id();
let first = {
let mut recorded = IMPORT_THREAD.lock().unwrap_or_else(PoisonError::into_inner);
match *recorded {
Some((recorded_device, thread)) if recorded_device == identity => thread,
_ => {
*recorded = Some((identity, current));
current
}
}
};
debug_assert_eq!(
first, current,
"import_hardware_buffer ran on {current:?}, but the first import on this device ran on \
{first:?}: the import submits on the raw queue wgpu uses, so every import must run on \
the device's one submitting (render) thread (frust-gpu import::android module docs)"
);
}
fn failed(step: &str, result: vk::Result) -> HardwareBufferImportError {
HardwareBufferImportError::Failed(format!("{step}: {result}"))
}
pub unsafe fn import_hardware_buffer(
device: &wgpu::Device,
buffer: *mut ndk_sys::AHardwareBuffer,
) -> Result<wgpu::Texture, HardwareBufferImportError> {
if buffer.is_null() {
return Err(HardwareBufferImportError::Failed(
"a null AHardwareBuffer".to_string(),
));
}
let api = buffer_api().ok_or_else(|| {
HardwareBufferImportError::Unsupported(
"this Android release has no AHardwareBuffer API (API 26+)".to_string(),
)
})?;
let description = unsafe {
let mut description = std::mem::zeroed::<ndk_sys::AHardwareBuffer_Desc>();
(api.describe)(buffer, &mut description);
description
};
let max_dimension = device.limits().max_texture_dimension_2d;
let (width, height) = check_buffer(&description, max_dimension)?;
let hal_device = unsafe { device.as_hal::<Vulkan>() }.ok_or_else(|| {
HardwareBufferImportError::Unsupported("the device is not a Vulkan device".to_string())
})?;
let enabled = hal_device.enabled_device_extensions();
for required in [
ash::android::external_memory_android_hardware_buffer::NAME,
ash::ext::queue_family_foreign::NAME,
] {
if !enabled.contains(&required) {
return Err(HardwareBufferImportError::Unsupported(format!(
"the device was opened without {required:?}"
)));
}
}
check_import_thread(device);
let raw = hal_device.raw_device().clone();
let instance = hal_device.shared_instance().raw_instance();
let extension =
ash::android::external_memory_android_hardware_buffer::Device::new(instance, &raw);
let mut format_properties = vk::AndroidHardwareBufferFormatPropertiesANDROID::default();
let mut properties =
vk::AndroidHardwareBufferPropertiesANDROID::default().push_next(&mut format_properties);
unsafe {
extension.get_android_hardware_buffer_properties(buffer.cast::<c_void>(), &mut properties)
}
.map_err(|e| failed("vkGetAndroidHardwareBufferPropertiesANDROID", e))?;
let allocation_size = properties.allocation_size;
let memory_type_bits = properties.memory_type_bits;
if format_properties.format != vk::Format::R8G8B8A8_UNORM
|| !format_properties
.format_features
.contains(vk::FormatFeatureFlags::SAMPLED_IMAGE)
{
return Err(HardwareBufferImportError::Failed(format!(
"the buffer maps to {:?} with {:?}, not a sampleable R8G8B8A8_UNORM",
format_properties.format, format_properties.format_features
)));
}
let memory_properties =
unsafe { instance.get_physical_device_memory_properties(hal_device.raw_physical_device()) };
let memory_type = pick_memory_type(memory_type_bits, &memory_properties).ok_or_else(|| {
HardwareBufferImportError::Failed(format!(
"no memory type in the buffer's mask {memory_type_bits:#x}"
))
})?;
let mut partial = Partial {
device: raw.clone(),
image: vk::Image::null(),
memory: vk::DeviceMemory::null(),
};
let mut external = vk::ExternalMemoryImageCreateInfo::default()
.handle_types(vk::ExternalMemoryHandleTypeFlags::ANDROID_HARDWARE_BUFFER_ANDROID);
let image_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk::Format::R8G8B8A8_UNORM)
.extent(vk::Extent3D {
width,
height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::SAMPLED)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.push_next(&mut external);
partial.image =
unsafe { raw.create_image(&image_info, None) }.map_err(|e| failed("vkCreateImage", e))?;
let mut import = vk::ImportAndroidHardwareBufferInfoANDROID::default().buffer(buffer.cast());
let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().image(partial.image);
let allocate_info = vk::MemoryAllocateInfo::default()
.allocation_size(allocation_size)
.memory_type_index(memory_type)
.push_next(&mut import)
.push_next(&mut dedicated);
partial.memory = unsafe { raw.allocate_memory(&allocate_info, None) }
.map_err(|e| failed("vkAllocateMemory (AHardwareBuffer import)", e))?;
unsafe { raw.bind_image_memory(partial.image, partial.memory, 0) }
.map_err(|e| failed("vkBindImageMemory", e))?;
unsafe {
acquire_from_foreign(
&raw,
hal_device.raw_queue(),
hal_device.queue_family_index(),
partial.image,
)
}?;
let (image, memory) = partial.disarm();
unsafe { (api.acquire)(buffer) };
let held = HeldBuffer {
buffer,
release: api.release,
};
let drop_device = raw;
let on_drop: wgpu::hal::DropCallback = Box::new(move || {
unsafe {
drop_device.destroy_image(image, None);
drop_device.free_memory(memory, None);
}
held.release();
});
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let hal_descriptor = wgpu::hal::TextureDescriptor {
label: Some("frust-gpu hardware buffer"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUses::RESOURCE,
memory_flags: wgpu::hal::MemoryFlags::empty(),
view_formats: Vec::new(),
};
let hal_texture = unsafe {
hal_device.texture_from_raw(
image,
&hal_descriptor,
Some(on_drop),
TextureMemory::External,
)
};
drop(hal_device);
let descriptor = wgpu::TextureDescriptor {
label: Some("frust-gpu hardware buffer"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
Ok(unsafe {
device.create_texture_from_hal::<Vulkan>(
hal_texture,
&descriptor,
wgpu::TextureUses::RESOURCE,
)
})
}
fn check_buffer(
description: &ndk_sys::AHardwareBuffer_Desc,
max_dimension: u32,
) -> Result<(u32, u32), HardwareBufferImportError> {
let refuse = |why: String| Err(HardwareBufferImportError::Failed(why));
if description.format != BUFFER_FORMAT {
return refuse(format!(
"AHardwareBuffer format {} is not R8G8B8A8_UNORM",
description.format
));
}
if description.layers != 1 {
return refuse(format!("{} layers, not 1", description.layers));
}
if description.usage & BUFFER_USAGE_SAMPLED == 0 {
return refuse("the buffer lacks GPU_SAMPLED_IMAGE usage".to_string());
}
let (width, height) = (description.width, description.height);
if width == 0 || height == 0 || width > max_dimension || height > max_dimension {
return refuse(format!(
"a {width}x{height} buffer is outside 1..={max_dimension}"
));
}
Ok((width, height))
}
fn pick_memory_type(
type_bits: u32,
properties: &vk::PhysicalDeviceMemoryProperties,
) -> Option<u32> {
let candidates = || {
properties
.memory_types_as_slice()
.iter()
.enumerate()
.filter(move |(index, _)| type_bits & (1 << index) != 0)
};
candidates()
.find(|(_, memory)| {
memory
.property_flags
.contains(vk::MemoryPropertyFlags::DEVICE_LOCAL)
})
.or_else(|| candidates().next())
.map(|(index, _)| index as u32)
}
unsafe fn acquire_from_foreign(
device: &ash::Device,
queue: vk::Queue,
family: u32,
image: vk::Image,
) -> Result<(), HardwareBufferImportError> {
let pool_info = vk::CommandPoolCreateInfo::default()
.flags(vk::CommandPoolCreateFlags::TRANSIENT)
.queue_family_index(family);
let pool = unsafe { device.create_command_pool(&pool_info, None) }
.map_err(|e| failed("vkCreateCommandPool", e))?;
let result = unsafe { record_and_submit(device, queue, family, pool, image) };
unsafe { device.destroy_command_pool(pool, None) };
result
}
unsafe fn record_and_submit(
device: &ash::Device,
queue: vk::Queue,
family: u32,
pool: vk::CommandPool,
image: vk::Image,
) -> Result<(), HardwareBufferImportError> {
let allocate_info = vk::CommandBufferAllocateInfo::default()
.command_pool(pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1);
let command_buffer = unsafe { device.allocate_command_buffers(&allocate_info) }
.map_err(|e| failed("vkAllocateCommandBuffers", e))?[0];
let begin_info =
vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
unsafe { device.begin_command_buffer(command_buffer, &begin_info) }
.map_err(|e| failed("vkBeginCommandBuffer", e))?;
let barrier = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::SHADER_READ)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.src_queue_family_index(vk::QUEUE_FAMILY_FOREIGN_EXT)
.dst_queue_family_index(family)
.image(image)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
unsafe {
device.cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::ALL_COMMANDS,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
);
}
unsafe { device.end_command_buffer(command_buffer) }
.map_err(|e| failed("vkEndCommandBuffer", e))?;
let fence = unsafe { device.create_fence(&vk::FenceCreateInfo::default(), None) }
.map_err(|e| failed("vkCreateFence", e))?;
let command_buffers = [command_buffer];
let submit = vk::SubmitInfo::default().command_buffers(&command_buffers);
let result = unsafe { device.queue_submit(queue, &[submit], fence) }
.map_err(|e| failed("vkQueueSubmit (ownership acquire)", e))
.and_then(|()| {
unsafe { device.wait_for_fences(&[fence], true, ACQUIRE_TIMEOUT_NS) }
.map_err(|e| failed("vkWaitForFences (ownership acquire)", e))
});
if let Err(error) = &result {
let idle = unsafe { device.queue_wait_idle(queue) };
log::warn!(
"frust-gpu: device health: the AHardwareBuffer ownership acquire did not complete \
({error}); waited the queue idle ({idle:?}). A wedged or lost queue, or a wgpu \
submit on another thread, which the import does not support"
);
}
unsafe { device.destroy_fence(fence, None) };
result
}
struct Partial {
device: ash::Device,
image: vk::Image,
memory: vk::DeviceMemory,
}
impl Partial {
fn disarm(mut self) -> (vk::Image, vk::DeviceMemory) {
let parts = (self.image, self.memory);
self.image = vk::Image::null();
self.memory = vk::DeviceMemory::null();
parts
}
}
impl Drop for Partial {
fn drop(&mut self) {
unsafe {
self.device.destroy_image(self.image, None);
self.device.free_memory(self.memory, None);
}
}
}
struct HeldBuffer {
buffer: *mut ndk_sys::AHardwareBuffer,
release: unsafe extern "C" fn(*mut ndk_sys::AHardwareBuffer),
}
unsafe impl Send for HeldBuffer {}
unsafe impl Sync for HeldBuffer {}
impl HeldBuffer {
fn release(self) {
unsafe { (self.release)(self.buffer) };
}
}
#[cfg(test)]
mod tests {
use super::*;
fn description(width: u32, height: u32) -> ndk_sys::AHardwareBuffer_Desc {
ndk_sys::AHardwareBuffer_Desc {
width,
height,
layers: 1,
format: BUFFER_FORMAT,
usage: BUFFER_USAGE_SAMPLED,
stride: width,
rfu0: 0,
rfu1: 0,
}
}
#[test]
fn a_sampled_rgba8_buffer_within_the_limit_is_imported() {
assert_eq!(check_buffer(&description(64, 32), 4096), Ok((64, 32)));
}
#[test]
fn other_layouts_are_refused() {
let rgbx = ndk_sys::AHardwareBuffer_Desc {
format: ndk_sys::AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM.0,
..description(64, 64)
};
assert!(check_buffer(&rgbx, 4096).is_err());
let layered = ndk_sys::AHardwareBuffer_Desc {
layers: 2,
..description(64, 64)
};
assert!(check_buffer(&layered, 4096).is_err());
let unsampled = ndk_sys::AHardwareBuffer_Desc {
usage: 0,
..description(64, 64)
};
assert!(check_buffer(&unsampled, 4096).is_err());
assert!(check_buffer(&description(8192, 64), 4096).is_err());
assert!(check_buffer(&description(0, 64), 4096).is_err());
}
#[test]
fn device_local_memory_is_preferred_within_the_mask() {
let mut properties = vk::PhysicalDeviceMemoryProperties {
memory_type_count: 3,
..Default::default()
};
properties.memory_types[0].property_flags = vk::MemoryPropertyFlags::DEVICE_LOCAL;
properties.memory_types[1].property_flags = vk::MemoryPropertyFlags::HOST_VISIBLE;
properties.memory_types[2].property_flags =
vk::MemoryPropertyFlags::DEVICE_LOCAL | vk::MemoryPropertyFlags::HOST_VISIBLE;
assert_eq!(pick_memory_type(0b110, &properties), Some(2));
assert_eq!(pick_memory_type(0b010, &properties), Some(1));
assert_eq!(pick_memory_type(0, &properties), None);
}
}