use alloc::boxed::Box;
use alloc::vec::Vec;
use core::ffi::c_void;
use ash::vk;
use ndk_sys::AHardwareBuffer;
use waterui_graphics::shared_context::{GpuRuntime, drain_device_before_teardown};
use wgpu_hal::api::Vulkan;
use super::capture_format::{WuiCaptureFormat, capture_buffer_format};
use super::gpu_surface::WuiGpuCaptureFence;
const BUFFER_FORMAT_RGBA_8888: u32 =
ndk_sys::AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM.0;
const BUFFER_FORMAT_RGBA_FP16: u32 =
ndk_sys::AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HardwareBufferDescription {
pub width: u32,
pub height: u32,
pub format: WuiCaptureFormat,
}
#[must_use]
pub unsafe fn describe_hardware_buffer(buffer: *mut AHardwareBuffer) -> HardwareBufferDescription {
assert!(
!buffer.is_null(),
"Android view capture was handed a null AHardwareBuffer"
);
let mut description = ndk_sys::AHardwareBuffer_Desc {
width: 0,
height: 0,
layers: 0,
format: 0,
usage: 0,
stride: 0,
rfu0: 0,
rfu1: 0,
};
unsafe { ndk_sys::AHardwareBuffer_describe(buffer, &raw mut description) };
let format = match description.format {
BUFFER_FORMAT_RGBA_8888 => WuiCaptureFormat::Rgba8Unorm,
BUFFER_FORMAT_RGBA_FP16 => WuiCaptureFormat::Rgba16Float,
other => panic!(
"Android view capture received an AHardwareBuffer in format {other}, which is neither \
RGBA_8888 nor RGBA_FP16"
),
};
HardwareBufferDescription {
width: description.width,
height: description.height,
format,
}
}
#[must_use]
pub unsafe fn hardware_buffer_from_java(
env: *mut c_void,
hardware_buffer: *mut c_void,
) -> *mut AHardwareBuffer {
let buffer =
unsafe { ndk_sys::AHardwareBuffer_fromHardwareBuffer(env.cast(), hardware_buffer.cast()) };
assert!(
!buffer.is_null(),
"Android view capture was handed a HardwareBuffer that is not backed by an AHardwareBuffer"
);
buffer
}
const COLOR_SUBRESOURCE: vk::ImageSubresourceRange = vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
};
const COLOR_LAYERS: vk::ImageSubresourceLayers = vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
mip_level: 0,
base_array_layer: 0,
layer_count: 1,
};
const MAX_CACHED_IMPORTS: usize = 8;
struct ImportedHardwareBuffer {
device: ash::Device,
buffer: *mut AHardwareBuffer,
image: vk::Image,
memory: vk::DeviceMemory,
description: HardwareBufferDescription,
}
impl Drop for ImportedHardwareBuffer {
fn drop(&mut self) {
unsafe {
self.device.destroy_image(self.image, None);
self.device.free_memory(self.memory, None);
ndk_sys::AHardwareBuffer_release(self.buffer);
}
}
}
impl ImportedHardwareBuffer {
unsafe fn import(
device: &wgpu_hal::vulkan::Device,
buffer: *mut AHardwareBuffer,
description: HardwareBufferDescription,
context: &'static str,
) -> Self {
let raw_device = device.raw_device();
let instance = device.shared_instance().raw_instance();
let extension = ash::android::external_memory_android_hardware_buffer::NAME;
assert!(
device.enabled_device_extensions().contains(&extension),
"{context}: the GPU device was opened without {}, so a captured view subtree cannot \
be imported",
extension.to_string_lossy()
);
let external_memory = ash::android::external_memory_android_hardware_buffer::Device::new(
instance, raw_device,
);
let mut format_properties = vk::AndroidHardwareBufferFormatPropertiesANDROID::default();
let (memory_type_bits, allocation_size) = {
let mut properties = vk::AndroidHardwareBufferPropertiesANDROID::default()
.push_next(&mut format_properties);
unsafe {
external_memory.get_android_hardware_buffer_properties(
buffer.cast::<c_void>().cast_const(),
&mut properties,
)
}
.unwrap_or_else(|error| {
panic!("{context}: the driver rejected the captured AHardwareBuffer: {error}")
});
(properties.memory_type_bits, properties.allocation_size)
};
let vk_format = format_properties.format;
let mut external_info = 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)
.extent(vk::Extent3D {
width: description.width,
height: description.height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::TRANSFER_SRC)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.push_next(&mut external_info);
let image = unsafe { raw_device.create_image(&image_info, None) }.unwrap_or_else(|error| {
panic!("{context}: could not create the image for a captured AHardwareBuffer: {error}")
});
let memory_type_index = external_memory_type_index(
instance,
device.raw_physical_device(),
memory_type_bits,
context,
);
let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().image(image);
let mut import_info =
vk::ImportAndroidHardwareBufferInfoANDROID::default().buffer(buffer.cast::<c_void>());
let allocate_info = vk::MemoryAllocateInfo::default()
.allocation_size(allocation_size)
.memory_type_index(memory_type_index)
.push_next(&mut dedicated)
.push_next(&mut import_info);
let memory =
unsafe { raw_device.allocate_memory(&allocate_info, None) }.unwrap_or_else(|error| {
unsafe { raw_device.destroy_image(image, None) };
panic!(
"{context}: could not import the memory of a captured AHardwareBuffer: {error}"
)
});
unsafe { raw_device.bind_image_memory(image, memory, 0) }.unwrap_or_else(|error| {
panic!("{context}: could not bind a captured AHardwareBuffer to its image: {error}")
});
unsafe { ndk_sys::AHardwareBuffer_acquire(buffer) };
tracing::debug!(
context,
width = description.width,
height = description.height,
format = ?description.format,
"imported an Android capture buffer as a Vulkan image"
);
Self {
device: raw_device.clone(),
buffer,
image,
memory,
description,
}
}
}
pub struct HardwareBufferImports {
runtime: GpuRuntime,
imports: Vec<ImportedHardwareBuffer>,
}
impl Drop for HardwareBufferImports {
fn drop(&mut self) {
self.clear();
}
}
impl core::fmt::Debug for HardwareBufferImports {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("HardwareBufferImports")
.field("runtime", &self.runtime)
.field("imports", &self.imports.len())
.finish()
}
}
impl HardwareBufferImports {
#[must_use]
pub const fn new(runtime: GpuRuntime) -> Self {
Self {
runtime,
imports: Vec::new(),
}
}
pub fn clear(&mut self) {
if self.imports.is_empty() {
return;
}
drain_device_before_teardown(&self.runtime.context().device);
self.imports.clear();
}
unsafe fn get_or_import(
&mut self,
device: &wgpu_hal::vulkan::Device,
buffer: *mut AHardwareBuffer,
description: HardwareBufferDescription,
context: &'static str,
) -> vk::Image {
if let Some(index) = self.imports.iter().position(|import| {
core::ptr::eq(import.buffer, buffer) && import.description == description
}) {
let import = self.imports.remove(index);
let image = import.image;
self.imports.push(import);
return image;
}
let stale = self
.imports
.iter()
.position(|import| core::ptr::eq(import.buffer, buffer));
let evicted = stale.or_else(|| (self.imports.len() >= MAX_CACHED_IMPORTS).then_some(0));
if let Some(index) = evicted {
drain_device_before_teardown(&self.runtime.context().device);
drop(self.imports.remove(index));
}
let import =
unsafe { ImportedHardwareBuffer::import(device, buffer, description, context) };
let image = import.image;
self.imports.push(import);
image
}
}
pub unsafe fn copy_hardware_buffer_into_texture(
imports: &mut HardwareBufferImports,
buffer: *mut AHardwareBuffer,
destination: &wgpu::Texture,
context: &'static str,
) -> *mut WuiGpuCaptureFence {
let description = unsafe { describe_hardware_buffer(buffer) };
assert_eq!(
(description.width, description.height),
(destination.width(), destination.height()),
"{context}: the captured buffer is {}x{} but the capture texture is {}x{}",
description.width,
description.height,
destination.width(),
destination.height()
);
assert_eq!(
description.format,
capture_buffer_format(destination.format()),
"{context}: the captured buffer's layout does not match the capture texture's format"
);
let runtime = imports.runtime.clone();
let gpu = runtime.context();
let (raw_device, family_index, source) = {
let hal_device = unsafe { gpu.device.as_hal::<Vulkan>() }.unwrap_or_else(|| {
panic!("{context}: Android view capture requires the Vulkan backend")
});
let source = unsafe { imports.get_or_import(&hal_device, buffer, description, context) };
(
hal_device.raw_device().clone(),
hal_device.queue_family_index(),
source,
)
};
let destination_image = {
let guard = unsafe { destination.as_hal::<Vulkan>() }
.unwrap_or_else(|| panic!("{context}: the capture texture is not a Vulkan texture"));
unsafe { guard.raw_handle() }
};
let mut transition = gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("WaterUI Android Capture Transition"),
});
transition.transition_resources(
core::iter::empty(),
core::iter::once(wgpu::TextureTransition {
texture: destination,
selector: None,
state: wgpu::TextureUses::COPY_DST,
}),
);
let mut encoder = gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("WaterUI Android Capture Copy"),
});
unsafe {
encoder.as_hal_mut::<Vulkan, _, ()>(|hal_encoder| {
let hal_encoder = hal_encoder.unwrap_or_else(|| {
panic!("{context}: the command encoder is not a Vulkan encoder")
});
let command_buffer = hal_encoder.raw_handle();
record_capture_copy(
&raw_device,
command_buffer,
family_index,
source,
destination_image,
description,
);
});
}
let submission = gpu.queue.submit([transition.finish(), encoder.finish()]);
Box::into_raw(Box::new(WuiGpuCaptureFence::new(
gpu.submission_completion_driver(),
submission,
)))
}
fn record_capture_copy(
device: &ash::Device,
command_buffer: vk::CommandBuffer,
family_index: u32,
source: vk::Image,
destination: vk::Image,
description: HardwareBufferDescription,
) {
let acquire = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_READ)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.src_queue_family_index(vk::QUEUE_FAMILY_FOREIGN_EXT)
.dst_queue_family_index(family_index)
.image(source)
.subresource_range(COLOR_SUBRESOURCE);
unsafe {
device.cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[acquire],
);
}
let region = vk::ImageCopy::default()
.src_subresource(COLOR_LAYERS)
.dst_subresource(COLOR_LAYERS)
.extent(vk::Extent3D {
width: description.width,
height: description.height,
depth: 1,
});
unsafe {
device.cmd_copy_image(
command_buffer,
source,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
destination,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[region],
);
}
let release = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::TRANSFER_READ)
.dst_access_mask(vk::AccessFlags::empty())
.old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.new_layout(vk::ImageLayout::GENERAL)
.src_queue_family_index(family_index)
.dst_queue_family_index(vk::QUEUE_FAMILY_FOREIGN_EXT)
.image(source)
.subresource_range(COLOR_SUBRESOURCE);
unsafe {
device.cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
vk::DependencyFlags::empty(),
&[],
&[],
&[release],
);
}
}
fn external_memory_type_index(
instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
memory_type_bits: u32,
context: &'static str,
) -> u32 {
let memory_properties =
unsafe { instance.get_physical_device_memory_properties(physical_device) };
(0..memory_properties.memory_type_count)
.find(|index| memory_type_bits & (1 << index) != 0)
.unwrap_or_else(|| {
panic!(
"{context}: the driver reports no memory type that can back a captured \
AHardwareBuffer"
)
})
}