diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/media/NativeMediaCodec.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/media/NativeMediaCodec.java index 70bd5f775d..0dad04f573 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/media/NativeMediaCodec.java +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/media/NativeMediaCodec.java @@ -4,6 +4,9 @@ package org.yuzu.yuzu_emu.media; import android.media.Image; +import android.media.ImageReader; +import android.view.Surface; +import android.hardware.HardwareBuffer; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; @@ -31,8 +34,32 @@ public class NativeMediaCodec { } final int id = nextId.getAndIncrement(); decoders.put(id, codec); - // Request YUV_420_888 output (Image) if available + // Use ImageReader-backed Surface so the codec can produce AHardwareBuffer-backed images + ImageReader imageReader = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + imageReader = ImageReader.newInstance(width, height, android.graphics.ImageFormat.YUV_420_888, 4); + final ImageReader ir = imageReader; + ir.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + Image image = null; + try { + image = reader.acquireLatestImage(); + if (image == null) return; + long pts = image.getTimestamp(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + HardwareBuffer hb = image.getHardwareBuffer(); + if (hb != null) { + onHardwareBufferAvailable(id, hb, pts); + } + } + } catch (Throwable t) { + Log.w(TAG, "onImageAvailable failed: " + t); + } finally { + if (image != null) image.close(); + } + } + }, null); codec.setCallback(new MediaCodec.Callback() { private final int decoderId = id; @@ -43,17 +70,14 @@ public class NativeMediaCodec { @Override public void onOutputBufferAvailable(MediaCodec mc, int index, MediaCodec.BufferInfo info) { + // We configure the codec to output to the ImageReader surface; the ImageReader listener + // will handle delivering AHardwareBuffer-backed images to native code. Still release the + // output buffer if requested by codec (some codecs may not use getOutputImage when a + // Surface is provided). try { - Image image = mc.getOutputImage(index); - if (image != null) { - byte[] data = ImageToNV12(image); - onFrameDecoded(decoderId, data, image.getWidth(), image.getHeight(), info.presentationTimeUs); - image.close(); - } - } catch (Throwable t) { - Log.w(TAG, "onOutputBufferAvailable failed: " + t); - } finally { try { mc.releaseOutputBuffer(index, false); } catch (Throwable ignored) {} + } catch (Throwable t) { + Log.w(TAG, "onOutputBufferAvailable release failed: " + t); } } @@ -69,7 +93,13 @@ public class NativeMediaCodec { }); } - codec.configure(format, null, null, 0); + // Configure codec to output to the ImageReader surface when possible to enable GPU-backed buffers. + if (imageReader != null) { + Surface surf = imageReader.getSurface(); + codec.configure(format, surf, null, 0); + } else { + codec.configure(format, null, null, 0); + } codec.start(); return id; } catch (Exception e) { @@ -78,6 +108,9 @@ public class NativeMediaCodec { } } + // Called from Java when an AHardwareBuffer-backed image is available (API26+). + private static native void onHardwareBufferAvailable(int decoderId, Object hardwareBuffer, long pts); + private static byte[] ImageToNV12(Image image) { // Convert YUV_420_888 to NV12 (Y plane, interleaved UV) final Image.Plane[] planes = image.getPlanes(); diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index d6e9864282..d6aaaacf9c 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -333,6 +333,7 @@ target_link_options(video_core PRIVATE ${FFmpeg_LDFLAGS}) if(ANDROID) target_sources(video_core PRIVATE host1x/ffmpeg/mediacodec_bridge_android.cpp) + target_sources(video_core PRIVATE vulkan_common/ahardwarebuffer_vulkan.cpp) endif() add_dependencies(video_core host_shaders) diff --git a/src/video_core/host1x/ffmpeg/mediacodec_bridge.h b/src/video_core/host1x/ffmpeg/mediacodec_bridge.h index fcda90b511..2acace7b65 100644 --- a/src/video_core/host1x/ffmpeg/mediacodec_bridge.h +++ b/src/video_core/host1x/ffmpeg/mediacodec_bridge.h @@ -7,6 +7,10 @@ #include #include +#ifdef __ANDROID__ +#include +#endif + namespace FFmpeg::MediaCodecBridge { bool IsAvailable(); @@ -22,4 +26,18 @@ bool SendPacket(int id, const uint8_t* data, size_t size, int64_t pts); // Pop a decoded NV12 frame. Returns std::nullopt if none available. On success, fills width,height,pts std::optional> PopDecodedFrame(int id, int& width, int& height, int64_t& pts); +#ifdef __ANDROID__ +// Pop a decoded AHardwareBuffer (zero-copy path). Returns std::nullopt if none available. +std::optional PopDecodedHardwareBuffer(int id, int& width, int& height, int64_t& pts); +#endif + +#ifdef __ANDROID__ +// Enqueue an AHardwareBuffer for presentation; PresentManager will consume these. +void EnqueueHardwareBufferForPresent(AHardwareBuffer* ahb, int width, int height, int64_t pts); + +// Try to pop an AHardwareBuffer for presentation. Returns true if a buffer was popped. +bool TryPopHardwareBufferForPresent(AHardwareBuffer** out_ahb, int& out_width, int& out_height, int64_t& out_pts); +#endif +} // namespace FFmpeg::MediaCodecBridge + } // namespace FFmpeg::MediaCodecBridge diff --git a/src/video_core/host1x/ffmpeg/mediacodec_bridge_android.cpp b/src/video_core/host1x/ffmpeg/mediacodec_bridge_android.cpp index cb06fa0e08..7bd2e555dd 100644 --- a/src/video_core/host1x/ffmpeg/mediacodec_bridge_android.cpp +++ b/src/video_core/host1x/ffmpeg/mediacodec_bridge_android.cpp @@ -15,6 +15,9 @@ #include #include "common/android/id_cache.h" #include "common/logging/log.h" +#include +#include +#include namespace FFmpeg::MediaCodecBridge { @@ -31,11 +34,45 @@ struct DecoderState { int height = 0; int64_t pts = 0; bool has_frame = false; + // Queue of AHardwareBuffer pointers for zero-copy path. + std::deque hw_queue; + std::deque hw_pts_queue; }; static std::mutex s_global_mtx; static std::map> s_decoders; +#ifdef __ANDROID__ +// Global queue for AHardwareBuffer presentation (producer: mediacodec, consumer: PresentManager) +static std::mutex s_present_queue_mtx; +static std::deque s_present_queue_ahb; +static std::deque s_present_queue_w; +static std::deque s_present_queue_h; +static std::deque s_present_queue_pts; + +void EnqueueHardwareBufferForPresent(AHardwareBuffer* ahb, int width, int height, int64_t pts) { + std::lock_guard lock(s_present_queue_mtx); + s_present_queue_ahb.push_back(ahb); + s_present_queue_w.push_back(width); + s_present_queue_h.push_back(height); + s_present_queue_pts.push_back(pts); +} + +bool TryPopHardwareBufferForPresent(AHardwareBuffer** out_ahb, int& out_width, int& out_height, int64_t& out_pts) { + std::lock_guard lock(s_present_queue_mtx); + if (s_present_queue_ahb.empty()) return false; + *out_ahb = s_present_queue_ahb.front(); + out_width = s_present_queue_w.front(); + out_height = s_present_queue_h.front(); + out_pts = s_present_queue_pts.front(); + s_present_queue_ahb.pop_front(); + s_present_queue_w.pop_front(); + s_present_queue_h.pop_front(); + s_present_queue_pts.pop_front(); + return true; +} +#endif + extern "C" JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_media_NativeMediaCodec_onFrameDecoded( JNIEnv* env, jclass, jint decoderId, jbyteArray data, jint width, jint height, jlong pts) { std::lock_guard lock(s_global_mtx); @@ -51,6 +88,32 @@ extern "C" JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_media_NativeMediaCodec st->has_frame = true; } +extern "C" JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_media_NativeMediaCodec_onHardwareBufferAvailable( + JNIEnv* env, jclass, jint decoderId, jobject hardwareBuffer, jlong pts) { + if (hardwareBuffer == nullptr) return; + std::lock_guard lock(s_global_mtx); + auto it = s_decoders.find(decoderId); + if (it == s_decoders.end()) return; + auto& st = it->second; + + // Convert Java HardwareBuffer to AHardwareBuffer* + AHardwareBuffer* ahb = AHardwareBuffer_fromHardwareBuffer(env, hardwareBuffer); + if (!ahb) return; + // Acquire to extend lifetime + AHardwareBuffer_acquire(ahb); + + // Optionally get description to set width/height for caller convenience + AHardwareBuffer_Desc desc; + AHardwareBuffer_describe(ahb, &desc); + + st->hw_queue.push_back(ahb); + st->hw_pts_queue.push_back(static_cast(pts)); + LOG_DEBUG(Render_Vulkan, "Enqueued AHardwareBuffer for decoder {} size={}x{} pts={}", decoderId, desc.width, desc.height, (long long)pts); + st->has_frame = true; + // Also enqueue for presentation (global present queue). PresentManager will consume these. + EnqueueHardwareBufferForPresent(ahb, static_cast(desc.width), static_cast(desc.height), static_cast(pts)); +} + bool IsAvailable() { // We assume the bridge is available if the Java class can be found. auto env = Common::Android::GetEnvForThread(); @@ -116,4 +179,24 @@ std::optional> PopDecodedFrame(int id, int& width, int& hei } // namespace FFmpeg::MediaCodecBridge +#ifdef __ANDROID__ +std::optional FFmpeg::MediaCodecBridge::PopDecodedHardwareBuffer(int id, int& width, int& height, int64_t& pts) { + std::lock_guard lock(s_global_mtx); + auto it = s_decoders.find(id); + if (it == s_decoders.end()) return std::nullopt; + auto& st = it->second; + if (st->hw_queue.empty()) return std::nullopt; + AHardwareBuffer* ahb = st->hw_queue.front(); + st->hw_queue.pop_front(); + pts = st->hw_pts_queue.front(); + st->hw_pts_queue.pop_front(); + // Fill width/height from the buffer description + AHardwareBuffer_Desc desc; + AHardwareBuffer_describe(ahb, &desc); + width = static_cast(desc.width); + height = static_cast(desc.height); + return std::optional{ahb}; +} +#endif + #endif // __ANDROID__ diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index 0b29ad1389..c2d12f23d1 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/settings.h" +#include "common/logging/log.h" #include "common/thread.h" #include "core/frontend/emu_window.h" #include "video_core/renderer_vulkan/vk_present_manager.h" @@ -13,6 +14,10 @@ #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_surface.h" #include "video_core/vulkan_common/vulkan_wrapper.h" +#ifdef __ANDROID__ +#include "video_core/vulkan_common/ahardwarebuffer_vulkan.h" +#include "video_core/host1x/ffmpeg/mediacodec_bridge.h" +#endif namespace Vulkan { @@ -170,6 +175,23 @@ Frame* PresentManager::GetRenderFrame() { frame->present_done.Wait(); frame->present_done.Reset(); +#ifdef ANDROID + // Free any imported AHardwareBuffer-backed resources from the previous submission. + VkDevice vkdev = static_cast(*device.GetLogical()); + for (VkImage img : frame->imported_images) { + vkDestroyImage(vkdev, img, nullptr); + } + for (VkDeviceMemory mem : frame->imported_mem) { + vkFreeMemory(vkdev, mem, nullptr); + } + for (AHardwareBuffer* ahb : frame->imported_ahb) { + AHardwareBuffer_release(ahb); + } + frame->imported_images.clear(); + frame->imported_mem.clear(); + frame->imported_ahb.clear(); +#endif + return frame; } @@ -366,6 +388,12 @@ void PresentManager::CopyToSwapchainImpl(Frame* frame) { .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, .pInheritanceInfo = nullptr, }); + // If Android MediaCodec produced an AHardwareBuffer-backed frame, import and blit it directly to + // the swapchain image. This implements a prototype zero-copy path: the mediacodec bridge enqueues + // AHardwareBuffer handles which are then imported here and copied into the swapchain image. + // We'll attempt to consume an AHardwareBuffer-backed frame after the swapchain image + // has been transitioned to TRANSFER_DST_OPTIMAL (below). This ensures correct ordering + // and avoids doing copies before layout transitions. const VkImage image{swapchain.CurrentImage()}; const VkExtent2D extent = swapchain.GetExtent(); @@ -449,15 +477,78 @@ void PresentManager::CopyToSwapchainImpl(Frame* frame) { cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, {}, {}, {}, pre_barriers); - if (blit_supported) { - cmdbuf.BlitImage(*frame->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - MakeImageBlit(frame->width, frame->height, extent.width, extent.height), - VK_FILTER_LINEAR); - } else { - cmdbuf.CopyImage(*frame->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - MakeImageCopy(frame->width, frame->height, extent.width, extent.height)); +#ifdef __ANDROID__ + // Try to pop a hardware buffer produced by MediaCodec. If available and extension enabled, + // import it and copy it into the swapchain image. Imported resources are tracked on the frame + // for cleanup after presentation. + AHardwareBuffer* ahb = nullptr; + int ahb_w = 0, ahb_h = 0; + int64_t ahb_pts = 0; + bool used_imported = false; + const bool ahb_ext_supported = device.IsAndroidHardwareBufferExternalMemorySupported(); + if (!ahb_ext_supported) { + // Drain and release any queued buffers to avoid leaks if producer is still enqueuing. + int drained = 0; + while (FFmpeg::MediaCodecBridge::TryPopHardwareBufferForPresent(&ahb, ahb_w, ahb_h, ahb_pts)) { + AHardwareBuffer_release(ahb); + ++drained; + } + if (drained > 0) { + LOG_INFO(Render_Vulkan, "Released {} queued AHardwareBuffer(s) (extension unsupported)", drained); + } + } else if (FFmpeg::MediaCodecBridge::TryPopHardwareBufferForPresent(&ahb, ahb_w, ahb_h, ahb_pts)) { + VkDevice vkdev = static_cast(*device.GetLogical()); + VkPhysicalDevice vkphysical = static_cast(*device.GetPhysical()); + VkImage imported_image = VK_NULL_HANDLE; + VkDeviceMemory imported_mem = VK_NULL_HANDLE; + VkFormat imported_fmt = VK_FORMAT_UNDEFINED; + if (::ImportAHardwareBufferToVk(device.GetDispatchLoader(), vkdev, vkphysical, ahb, &imported_image, &imported_mem, &imported_fmt)) { + // Track imported resources on the frame so they can be freed after present + frame->imported_images.push_back(imported_image); + frame->imported_mem.push_back(imported_mem); + frame->imported_ahb.push_back(ahb); + LOG_INFO(Render_Vulkan, "Imported AHardwareBuffer {}x{} fmt={} pts={} into Vulkan image", ahb_w, ahb_h, imported_fmt, (long long)ahb_pts); + + // Transition imported image to TRANSFER_SRC_OPTIMAL + VkImageMemoryBarrier barrier_src{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = imported_image, + .subresourceRange{.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1}}; + + const std::array src_barriers{barrier_src}; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, {}, {}, {}, src_barriers); + + // Copy imported image -> swapchain image + VkImageCopy copy_region = MakeImageCopy(static_cast(ahb_w), static_cast(ahb_h), extent.width, extent.height); + cmdbuf.CopyImage(imported_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_region); + used_imported = true; + } else { + LOG_WARNING(Render_Vulkan, "AHardwareBuffer import failed; releasing buffer ({}x{} pts={})", ahb_w, ahb_h, (long long)ahb_pts); + AHardwareBuffer_release(ahb); + } + } +#endif + + if (!used_imported) { + if (blit_supported) { + cmdbuf.BlitImage(*frame->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + MakeImageBlit(frame->width, frame->height, extent.width, extent.height), + VK_FILTER_LINEAR); + } else { + cmdbuf.CopyImage(*frame->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + MakeImageCopy(frame->width, frame->height, extent.width, extent.height)); + } + if (ahb_ext_supported) { + LOG_DEBUG(Render_Vulkan, "No AHardwareBuffer available; used standard frame copy"); + } } cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, {}, diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h index 5820280602..23f8961195 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.h +++ b/src/video_core/renderer_vulkan/vk_present_manager.h @@ -6,11 +6,15 @@ #include #include #include +#include #include "common/common_types.h" #include "common/polyfill_thread.h" #include "video_core/vulkan_common/vulkan_memory_allocator.h" #include "video_core/vulkan_common/vulkan_wrapper.h" +#ifdef ANDROID +#include +#endif struct VkSurfaceKHR_T; @@ -33,6 +37,12 @@ struct Frame { vk::CommandBuffer cmdbuf; vk::Semaphore render_ready; vk::Fence present_done; +#ifdef ANDROID + // Imported resources for a submitted frame that must be freed after present is done. + std::vector imported_images; + std::vector imported_mem; + std::vector imported_ahb; +#endif }; class PresentManager { diff --git a/src/video_core/vulkan_common/ahardwarebuffer_vulkan.cpp b/src/video_core/vulkan_common/ahardwarebuffer_vulkan.cpp new file mode 100644 index 0000000000..2095486b23 --- /dev/null +++ b/src/video_core/vulkan_common/ahardwarebuffer_vulkan.cpp @@ -0,0 +1,116 @@ +#ifdef __ANDROID__ + +#include "ahardwarebuffer_vulkan.h" +#include +#include +#include +#include + +namespace { + +static uint32_t FindMemoryTypeIndex(VkPhysicalDevice physicalDevice, uint32_t typeBits, + VkMemoryPropertyFlags properties) { + VkPhysicalDeviceMemoryProperties memProps; + vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProps); + for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) { + if ((typeBits & (1u << i)) && (memProps.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + // Fallback: return first matching bit + for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) { + if (typeBits & (1u << i)) return i; + } + return 0; +} + +} // anonymous + +static bool ImportAHardwareBufferInternal(PFN_vkGetAndroidHardwareBufferPropertiesANDROID get_props, + VkDevice device, VkPhysicalDevice physicalDevice, AHardwareBuffer* ahb, + VkImage* outImage, VkDeviceMemory* outMemory, VkFormat* outFormat) { + if (!device || !physicalDevice || !ahb || !outImage || !outMemory || !get_props) return false; + + VkAndroidHardwareBufferPropertiesANDROID ahb_props{}; + ahb_props.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID; + VkResult r = get_props(device, ahb, &ahb_props); + if (r != VK_SUCCESS) return false; + + // Determine VkFormat to use + VkFormat fmt = static_cast(ahb_props.format); + if (outFormat) *outFormat = fmt; + + // Create image with external memory + VkExternalMemoryImageCreateInfo external_info{}; + external_info.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; + external_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID; + + VkImageCreateInfo image_info{}; + image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + image_info.pNext = &external_info; + image_info.imageType = VK_IMAGE_TYPE_2D; + image_info.format = fmt; + image_info.extent.width = ahb_props.allocationSize ? 0 : 0; // placeholder; we'll query via AHardwareBuffer_describe + + AHardwareBuffer_Desc desc; + AHardwareBuffer_describe(ahb, &desc); + image_info.extent.width = desc.width; + image_info.extent.height = desc.height; + image_info.extent.depth = 1; + image_info.mipLevels = 1; + image_info.arrayLayers = 1; + image_info.samples = VK_SAMPLE_COUNT_1_BIT; + image_info.tiling = VK_IMAGE_TILING_OPTIMAL; + // Use typical usage bits; adjust as needed by consumer + image_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + r = vkCreateImage(device, &image_info, nullptr, outImage); + if (r != VK_SUCCESS) return false; + + VkMemoryRequirements memReq; + vkGetImageMemoryRequirements(device, *outImage, &memReq); + + // Prepare import struct + VkImportAndroidHardwareBufferInfoANDROID import_info{}; + import_info.sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID; + import_info.buffer = ahb; + + VkMemoryAllocateInfo alloc_info{}; + alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc_info.pNext = &import_info; + alloc_info.allocationSize = memReq.size; + alloc_info.memoryTypeIndex = FindMemoryTypeIndex(physicalDevice, memReq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + r = vkAllocateMemory(device, &alloc_info, nullptr, outMemory); + if (r != VK_SUCCESS) { + vkDestroyImage(device, *outImage, nullptr); + return false; + } + + r = vkBindImageMemory(device, *outImage, *outMemory, 0); + if (r != VK_SUCCESS) { + vkDestroyImage(device, *outImage, nullptr); + vkFreeMemory(device, *outMemory, nullptr); + return false; + } + + return true; +} + +bool ImportAHardwareBufferToVk(VkDevice device, VkPhysicalDevice physicalDevice, AHardwareBuffer* ahb, + VkImage* outImage, VkDeviceMemory* outMemory, VkFormat* outFormat) { + PFN_vkGetAndroidHardwareBufferPropertiesANDROID fpGetAHBProps = + (PFN_vkGetAndroidHardwareBufferPropertiesANDROID)vkGetDeviceProcAddr(device, "vkGetAndroidHardwareBufferPropertiesANDROID"); + return ImportAHardwareBufferInternal(fpGetAHBProps, device, physicalDevice, ahb, outImage, outMemory, outFormat); +} + +bool ImportAHardwareBufferToVk(const Vulkan::vk::DeviceDispatch& dld, + VkDevice device, VkPhysicalDevice physicalDevice, AHardwareBuffer* ahb, + VkImage* outImage, VkDeviceMemory* outMemory, VkFormat* outFormat) { + return ImportAHardwareBufferInternal(dld.vkGetAndroidHardwareBufferPropertiesANDROID, device, physicalDevice, ahb, + outImage, outMemory, outFormat); +} + +#endif // __ANDROID__ diff --git a/src/video_core/vulkan_common/ahardwarebuffer_vulkan.h b/src/video_core/vulkan_common/ahardwarebuffer_vulkan.h new file mode 100644 index 0000000000..72f5c29a90 --- /dev/null +++ b/src/video_core/vulkan_common/ahardwarebuffer_vulkan.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef __ANDROID__ + +#include +#include +#include "video_core/vulkan_common/vulkan_wrapper.h" + +// Import an AHardwareBuffer into Vulkan as a VkImage with bound memory. +// On success returns true and fills outImage/outMemory/outFormat. Caller is responsible +// for destroying the VkImage and freeing the VkDeviceMemory when done. The AHardwareBuffer +// reference is not released by this function; caller should call AHardwareBuffer_release when +// done with the AHardwareBuffer. + +bool ImportAHardwareBufferToVk(VkDevice device, VkPhysicalDevice physicalDevice, AHardwareBuffer* ahb, + VkImage* outImage, VkDeviceMemory* outMemory, VkFormat* outFormat); + +// Overload that uses the dispatch loader for extension calls. +bool ImportAHardwareBufferToVk(const Vulkan::vk::DeviceDispatch& dld, + VkDevice device, VkPhysicalDevice physicalDevice, AHardwareBuffer* ahb, + VkImage* outImage, VkDeviceMemory* outMemory, VkFormat* outFormat); + +#endif // __ANDROID__ diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index af88c89f68..1e320acc49 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1110,6 +1110,23 @@ bool Device::GetSuitability(bool requires_swapchain) { CHECK_EXTENSION(VK_KHR_SWAPCHAIN_EXTENSION_NAME); } +#ifdef ANDROID + // Prefer enabling VK_ANDROID_external_memory_android_hardware_buffer when available so + // MediaCodec AHardwareBuffer frames can be imported with zero-copy. + if (supported_extensions.contains(VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME)) { + if (!loaded_extensions.contains(VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME)) { + loaded_extensions.insert(VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME); + extensions.external_memory_android_hardware_buffer = true; + LOG_INFO(Render_Vulkan, "Enabled VK_ANDROID_external_memory_android_hardware_buffer for AHardwareBuffer import path"); + } + else { + LOG_INFO(Render_Vulkan, "VK_ANDROID_external_memory_android_hardware_buffer already enabled"); + } + } else { + LOG_INFO(Render_Vulkan, "Device does not advertise VK_ANDROID_external_memory_android_hardware_buffer; falling back to CPU decode copy path"); + } +#endif + #undef LOG_EXTENSION #undef CHECK_EXTENSION diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index a751bc685a..edcddd2d50 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -91,6 +91,7 @@ VK_DEFINE_HANDLE(VmaAllocator) EXTENSION(EXT, DESCRIPTOR_INDEXING, descriptor_indexing) \ EXTENSION(EXT, FILTER_CUBIC, filter_cubic) \ EXTENSION(QCOM, FILTER_CUBIC_WEIGHTS, filter_cubic_weights) \ + EXTENSION(ANDROID, EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER, external_memory_android_hardware_buffer) \ EXTENSION(KHR, MAINTENANCE_1, maintenance1) \ EXTENSION(KHR, MAINTENANCE_2, maintenance2) \ EXTENSION(KHR, MAINTENANCE_3, maintenance3) \ @@ -514,6 +515,11 @@ public: return extensions.image_format_list || instance_version >= VK_API_VERSION_1_2; } + /// Returns true if VK_ANDROID_external_memory_android_hardware_buffer is enabled. + bool IsAndroidHardwareBufferExternalMemorySupported() const { + return extensions.external_memory_android_hardware_buffer; + } + /// Returns true if the device supports VK_EXT_primitive_topology_list_restart. bool IsTopologyListPrimitiveRestartSupported() const { return features.primitive_topology_list_restart.primitiveTopologyListRestart; diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp index b77d01711a..ed7e36d87b 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.cpp +++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp @@ -223,6 +223,9 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkGetPipelineExecutablePropertiesKHR); X(vkGetPipelineExecutableStatisticsKHR); X(vkGetSemaphoreCounterValue); + // Android AHardwareBuffer external memory extension (present on Android when enabled) + X(vkGetAndroidHardwareBufferPropertiesANDROID); + X(vkGetMemoryAndroidHardwareBufferANDROID); X(vkMapMemory); X(vkQueueSubmit); X(vkResetFences); diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 39396b3279..bf564beda7 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.h +++ b/src/video_core/vulkan_common/vulkan_wrapper.h @@ -323,6 +323,9 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkGetPipelineExecutableStatisticsKHR vkGetPipelineExecutableStatisticsKHR{}; PFN_vkGetQueryPoolResults vkGetQueryPoolResults{}; PFN_vkGetSemaphoreCounterValue vkGetSemaphoreCounterValue{}; + // Android hardware buffer external memory extension functions + PFN_vkGetAndroidHardwareBufferPropertiesANDROID vkGetAndroidHardwareBufferPropertiesANDROID{}; + PFN_vkGetMemoryAndroidHardwareBufferANDROID vkGetMemoryAndroidHardwareBufferANDROID{}; PFN_vkMapMemory vkMapMemory{}; PFN_vkQueueSubmit vkQueueSubmit{}; PFN_vkResetFences vkResetFences{};