Files
punktfunk/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c_interop_test.cpp
T
enricobuehler 4c3b11445c feat(host): vendor PyroWave + minimal Granite subset as crates/pyrowave-sys
Phase 0 of design/pyrowave-codec-plan.md — the opt-in wired-LAN ultra-low-
latency codec. Vendored at upstream 509e4f88 (API 0.4.0, Granite 44362775,
volk + vulkan-headers pins in PUNKTFUNK-VENDOR.txt), pruned to the 6.6 MB
the standalone no-renderer build needs; scripts/vendor-pyrowave.sh
reproduces the tree (a pin bump is protocol-affecting, plan §4.2).

build.rs drives the wrapper CMakeLists (static archives incl. a static
C-API lib upstream only ships shared) + bindgen over pyrowave.h; Linux and
Windows only, empty stub elsewhere (Apple gets a native Metal port, §4.7).
Offline-safe by construction: no network, no system lib, vendored Vulkan
headers — same model as the opus dep (flatpak builder has no network).

Phase-0 validation on .21 (RTX 5070 Ti, driver 610.43.03):
- upstream pyrowave-c-test + interop test (incl. dmabuf/DRM-modifier
  Vulkan<->Vulkan) pass, from the pristine AND the pruned tree
- GPU kernel times at ~1.6 bpp noise: encode/decode 0.090/0.042 ms @800p,
  0.146/0.067 @1080p, 0.226/0.103 @1440p, 0.477/0.201 @4K — order of
  magnitude under NVENC's 1-2 ms retrieve, CBR lands within ~100 B of
  target
- cargo test -p pyrowave-sys green (static link + API-version pin check)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:35:10 +02:00

1912 lines
71 KiB
C++

// Copyright (c) 2026 Hans-Kristian Arntzen
// SPDX-License-Identifier: MIT
#define INITGUID
#include "device.hpp"
#include "context.hpp"
#include "image.hpp"
#include "pyrowave.h"
#include <stdio.h>
#include <cstdlib>
#include <exception>
#include <vector>
#ifdef _WIN32
#include <d3d11_4.h>
#include <d3d12.h>
#include <dxgi.h>
#include "com_ptr.hpp"
#endif
using namespace Vulkan;
#define ASSERT_THAT(x) do { \
if (!(x)) { fprintf(stderr, "Fatal error executing %s at line %d.\n", #x, __LINE__); std::terminate(); } \
} while(false)
#define CHECKED(x) do { \
pyrowave_result _res = x; \
if (_res != PYROWAVE_SUCCESS) { fprintf(stderr, "Got pyrowave result %d while executing %s at line %d.\n", _res, #x, __LINE__); std::terminate(); } \
} while(false)
#define CHECK_HRESULT(x) ASSERT_THAT(SUCCEEDED(x))
static pyrowave_device create_device_from_granite(Device &device)
{
// Verify that we can create a device from UUID/LUID.
VkPhysicalDeviceIDProperties ids = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES };
VkPhysicalDeviceProperties2 props2 = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &ids };
vkGetPhysicalDeviceProperties2(device.get_physical_device(), &props2);
pyrowave_uuid device_uuid, driver_uuid;
pyrowave_luid device_luid;
memcpy(device_uuid.uuid, ids.deviceUUID, VK_UUID_SIZE);
memcpy(driver_uuid.uuid, ids.driverUUID, VK_UUID_SIZE);
memcpy(device_luid.luid, ids.deviceLUID, VK_LUID_SIZE);
pyrowave_device pyro_device;
CHECKED(pyrowave_create_device_by_compat(device.get_gpu_properties().vendorID,
device.get_gpu_properties().deviceID, &device_uuid, &driver_uuid,
ids.deviceLUIDValid ? &device_luid : nullptr, &pyro_device));
return pyro_device;
}
static pyrowave_sync_object create_sync_object_from_timeline(pyrowave_device device, SemaphoreHolder &sem)
{
auto exported_timeline = sem.export_to_handle();
ASSERT_THAT(exported_timeline);
pyrowave_sync_object_create_info sync_info = {};
sync_info.device = device;
sync_info.handle_type = exported_timeline.semaphore_handle_type;
sync_info.semaphore_type = VK_SEMAPHORE_TYPE_TIMELINE;
sync_info.external_handle = (pyrowave_os_handle)exported_timeline.handle;
pyrowave_sync_object imported_timeline;
CHECKED(pyrowave_sync_object_create(&sync_info, &imported_timeline));
return imported_timeline;
}
static pyrowave_sync_object create_sync_object_from_binary(pyrowave_device device, SemaphoreHolder &sem)
{
auto exported_timeline = sem.export_to_handle();
ASSERT_THAT(exported_timeline);
pyrowave_sync_object_create_info sync_info = {};
sync_info.device = device;
sync_info.handle_type = exported_timeline.semaphore_handle_type;
sync_info.semaphore_type = VK_SEMAPHORE_TYPE_BINARY;
sync_info.external_handle = (pyrowave_os_handle)exported_timeline.handle;
sync_info.import_flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT;
pyrowave_sync_object imported_timeline;
CHECKED(pyrowave_sync_object_create(&sync_info, &imported_timeline));
return imported_timeline;
}
static pyrowave_image create_imported_image(pyrowave_device pyro_device, Device &device, Image &img)
{
auto exported = img.export_handle();
ASSERT_THAT(exported);
VkImageCreateInfo image_create_info = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
image_create_info.flags = img.get_create_info().flags;
image_create_info.usage = img.get_create_info().usage;
image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_create_info.format = img.get_create_info().format;
image_create_info.samples = img.get_create_info().samples;
image_create_info.mipLevels = img.get_create_info().levels;
image_create_info.arrayLayers = img.get_create_info().layers;
image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_create_info.imageType = img.get_create_info().type;
image_create_info.extent = { img.get_width(), img.get_height(), img.get_depth() };
pyrowave_image_create_info image_info = {};
image_info.device = pyro_device;
image_info.handle_type = exported.memory_handle_type;
image_info.external_handle = (pyrowave_os_handle)exported.handle;
image_info.image_create_info = &image_create_info;
VkImageDrmFormatModifierExplicitCreateInfoEXT modifier_info =
{ VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT };
VkImageFormatListCreateInfo format_list =
{ VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO };
std::vector<VkSubresourceLayout> drm_plane_layouts;
if (exported.memory_handle_type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT)
{
image_create_info.pNext = &modifier_info;
image_create_info.tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT;
if (img.get_format() == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM)
{
static const VkFormat nv12_formats[] = { VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM };
format_list.viewFormatCount = 2;
format_list.pViewFormats = nv12_formats;
}
else if (img.get_format() == VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM)
{
static const VkFormat yuv420p_formats[] = { VK_FORMAT_R8_UNORM };
format_list.viewFormatCount = 1;
format_list.pViewFormats = yuv420p_formats;
}
else
{
format_list.viewFormatCount = 1;
format_list.pViewFormats = &image_create_info.format;
}
// Query which DRM modifier the implementation picked for us and pass it along.
VkImageDrmFormatModifierPropertiesEXT props = { VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_PROPERTIES_EXT };
device.get_device_table().vkGetImageDrmFormatModifierPropertiesEXT(
device.get_device(), img.get_image(), &props);
modifier_info.drmFormatModifier = props.drmFormatModifier;
VkDrmFormatModifierPropertiesListEXT modifiers = { VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT };
VkFormatProperties3 props3 = { VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3, &modifiers };
device.get_format_properties(image_create_info.format, &props3);
std::vector<VkDrmFormatModifierPropertiesEXT> modifiers_props(modifiers.drmFormatModifierCount);
modifiers.pDrmFormatModifierProperties = modifiers_props.data();
device.get_format_properties(image_create_info.format, &props3);
auto itr = std::find_if(modifiers_props.begin(), modifiers_props.end(), [&](const VkDrmFormatModifierPropertiesEXT &prop)
{
return prop.drmFormatModifier == props.drmFormatModifier;
});
ASSERT_THAT(itr != modifiers_props.end());
uint32_t num_memory_planes = itr->drmFormatModifierPlaneCount;
drm_plane_layouts.resize(num_memory_planes);
// DRM format modifiers have per-plane stride information that needs to be queried ...
for (uint32_t i = 0; i < num_memory_planes; i++)
{
VkImageSubresource subresource = {
VkImageAspectFlags(VK_IMAGE_ASPECT_MEMORY_PLANE_0_BIT_EXT << i),
0, 0
};
device.get_device_table().vkGetImageSubresourceLayout(device.get_device(),
img.get_image(), &subresource, &drm_plane_layouts[i]);
// On import, these must be cleared to be valid.
drm_plane_layouts[i].size = 0;
drm_plane_layouts[i].depthPitch = 0;
if (image_create_info.arrayLayers == 1)
drm_plane_layouts[i].arrayPitch = 0;
}
// Pass it along to import.
modifier_info.drmFormatModifierPlaneCount = num_memory_planes;
modifier_info.pPlaneLayouts = drm_plane_layouts.data();
modifier_info.pNext = &format_list;
}
pyrowave_image imported_image;
CHECKED(pyrowave_image_create(&image_info, &imported_image));
return imported_image;
}
static uint8_t mirror(int v)
{
v &= 511;
if (v > 255)
v = 511 - v;
ASSERT_THAT(v >= 0 && v <= 255);
return uint8_t(v);
}
static ImageHandle create_exportable_test_image(Device &device, VkExternalMemoryHandleTypeFlagBits handle_type,
VkFormat format)
{
auto info = ImageCreateInfo::immutable_2d_image(1280, 720, format);
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
info.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT;
info.misc = IMAGE_MISC_NO_DEFAULT_VIEWS_BIT | IMAGE_MISC_EXTERNAL_MEMORY_BIT;
info.layout = ImageLayout::General;
info.initial_layout = VK_IMAGE_LAYOUT_GENERAL;
info.external.memory_handle_type = handle_type;
// DRM format modifiers require explicit cast list when MUTABLE is used.
VkImageFormatListCreateInfo format_list = { VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO };
VkImageDrmFormatModifierListCreateInfoEXT allowed_modifiers_info =
{ VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_LIST_CREATE_INFO_EXT };
std::vector<uint64_t> allowed_modifiers;
if (handle_type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT)
{
// Query all modifiers potentially supported by a format.
VkDrmFormatModifierPropertiesListEXT modifiers = { VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT };
VkFormatProperties3 props3 = { VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3, &modifiers };
device.get_format_properties(info.format, &props3);
std::vector<VkDrmFormatModifierPropertiesEXT> modifiers_props(modifiers.drmFormatModifierCount);
modifiers.pDrmFormatModifierProperties = modifiers_props.data();
device.get_format_properties(info.format, &props3);
if (format == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM)
{
static const VkFormat nv12_formats[] = { VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM };
format_list.viewFormatCount = 2;
format_list.pViewFormats = nv12_formats;
}
else if (format == VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM)
{
static const VkFormat yuv420p_formats[] = { VK_FORMAT_R8_UNORM };
format_list.viewFormatCount = 1;
format_list.pViewFormats = yuv420p_formats;
}
else
{
format_list.viewFormatCount = 1;
format_list.pViewFormats = &format;
}
for (uint32_t i = 0; i < modifiers.drmFormatModifierCount; i++)
{
auto &mod = modifiers.pDrmFormatModifierProperties[i];
VkPhysicalDeviceImageDrmFormatModifierInfoEXT modinfo =
{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_DRM_FORMAT_MODIFIER_INFO_EXT, &format_list };
modinfo.drmFormatModifier = mod.drmFormatModifier;
VkImageFormatProperties2 props2 = { VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2 };
// For DRM modifiers, it's a bit involved.
// Step 1 is to query which modifiers are supported for a given image.
if (device.get_image_format_properties(info.format, info.type,
VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
info.usage, info.flags, &modinfo, &props2) &&
info.width <= props2.imageFormatProperties.maxExtent.width &&
info.height <= props2.imageFormatProperties.maxExtent.height)
{
allowed_modifiers.push_back(mod.drmFormatModifier);
}
}
ASSERT_THAT(!allowed_modifiers.empty());
// Implementation picks one of the allowed modifiers.
allowed_modifiers_info.drmFormatModifierCount = allowed_modifiers.size();
allowed_modifiers_info.pDrmFormatModifiers = allowed_modifiers.data();
allowed_modifiers_info.pNext = &format_list;
info.pnext = &allowed_modifiers_info;
}
auto img = device.create_image(info);
if (format == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM)
{
auto cmd = device.request_command_buffer();
auto *luma = static_cast<uint8_t *>(cmd->update_image(*img, {}, { 1280, 720, 1 }, 1280, 720, { VK_IMAGE_ASPECT_PLANE_0_BIT, 0, 0, 1 }));
auto *chroma = static_cast<uint16_t *>(cmd->update_image(*img, {}, { 640, 360, 1 }, 640, 360, { VK_IMAGE_ASPECT_PLANE_1_BIT, 0, 0, 1 }));
for (int y = 0; y < 720; y++)
for (int x = 0; x < 1280; x++)
luma[y * 1280 + x] = mirror(y * 3 + 5 * x);
for (int y = 0; y < 360; y++)
for (int x = 0; x < 640; x++)
chroma[y * 640 + x] = (uint16_t(mirror(y * 7 + 5 * x)) << 8) | mirror(y + 3 * x);
device.submit(cmd);
}
return img;
}
static constexpr size_t BitstreamSize = 1000000;
static void send_image_to_encoder(pyrowave_image pyro_image,
pyrowave_sync_object pyro_sync_acquire, uint64_t acquire_value,
pyrowave_sync_object pyro_sync_release, uint64_t release_value,
pyrowave_encoder encoder)
{
pyrowave_gpu_external_reference ref = { pyro_image, VK_QUEUE_FAMILY_EXTERNAL };
pyrowave_gpu_sync_operation acquire = {};
pyrowave_gpu_sync_operation release = {};
pyrowave_gpu_buffers buffers = {};
pyrowave_rate_control rate_control = { BitstreamSize };
CHECKED(pyrowave_image_get_image_view(pyro_image,
VK_IMAGE_ASPECT_PLANE_0_BIT, VK_IMAGE_USAGE_SAMPLED_BIT, &buffers.planes[0]));
CHECKED(pyrowave_image_get_image_view(pyro_image,
VK_IMAGE_ASPECT_PLANE_1_BIT, VK_IMAGE_USAGE_SAMPLED_BIT, &buffers.planes[1]));
CHECKED(pyrowave_image_get_image_view(pyro_image,
VK_IMAGE_ASPECT_PLANE_2_BIT, VK_IMAGE_USAGE_SAMPLED_BIT, &buffers.planes[2]));
acquire.num_images = 1;
acquire.images = &ref;
acquire.sync.semaphore = pyrowave_sync_object_get_semaphore(pyro_sync_acquire);
acquire.sync.value = acquire_value;
release.num_images = 1;
release.images = &ref;
release.sync.semaphore = pyrowave_sync_object_get_semaphore(pyro_sync_release);
release.sync.value = release_value;
CHECKED(pyrowave_encoder_encode_gpu_synchronous(encoder, &acquire, &release, &buffers, &rate_control));
}
static void send_granite_image_to_encoder(Device &device, Image &granite_image, pyrowave_image pyro_image,
SemaphoreHolder &granite_sync, pyrowave_sync_object pyro_sync_acquire, uint64_t acquire_value,
pyrowave_sync_object pyro_sync_release, uint64_t release_value,
pyrowave_encoder encoder)
{
auto cmd = device.request_command_buffer();
cmd->release_image_barrier(granite_image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL,
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_QUEUE_FAMILY_EXTERNAL);
device.submit(cmd);
if (acquire_value)
{
auto signal = device.request_timeline_semaphore_as_binary(granite_sync, acquire_value);
device.submit_empty(CommandBuffer::Type::Generic, nullptr, signal.get());
}
#ifdef VULKAN_DEBUG
// VVL doesn't quite understand it when a different device increments the shared timeline.
// It thinks that we're doing a rewind, but that's not the case.
// Only observed on Windows for some reason, but avoids a dumb false positive.
device.wait_idle();
#endif
send_image_to_encoder(pyro_image, pyro_sync_acquire, acquire_value,
pyro_sync_release, release_value, encoder);
// Verify that the release value was signaled properly in finite time.
VkSemaphoreWaitInfo wait_info = { VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO };
wait_info.pSemaphores = &granite_sync.get_semaphore();
wait_info.semaphoreCount = 1;
wait_info.pValues = &release_value;
auto vr = device.get_device_table().vkWaitSemaphores(device.get_device(), &wait_info, 1000000000);
ASSERT_THAT(vr == VK_SUCCESS);
}
static void decode_image(pyrowave_image *pyro_image, bool multiplane,
pyrowave_sync_object pyro_sync, uint64_t release_value,
pyrowave_decoder decoder)
{
// Discard the image(s).
pyrowave_gpu_external_reference acquire_ref[3] = {};
pyrowave_gpu_external_reference release_ref[3] = {};
pyrowave_gpu_sync_operation acquire = {};
pyrowave_gpu_sync_operation release = {};
pyrowave_gpu_buffers buffers = {};
for (int plane = 0; plane < (multiplane ? 1 : 3); plane++)
{
// Discard the output.
acquire_ref[plane] = { pyro_image[plane], VK_QUEUE_FAMILY_IGNORED };
// Release back to other device.
release_ref[plane] = { pyro_image[plane], VK_QUEUE_FAMILY_EXTERNAL };
}
acquire.num_images = multiplane ? 1 : 3;
acquire.images = acquire_ref;
release.num_images = multiplane ? 1 : 3;
release.images = release_ref;
release.sync.semaphore = pyrowave_sync_object_get_semaphore(pyro_sync);
release.sync.value = release_value;
CHECKED(pyrowave_image_get_image_view(pyro_image[0],
VK_IMAGE_ASPECT_PLANE_0_BIT, VK_IMAGE_USAGE_STORAGE_BIT, &buffers.planes[0]));
CHECKED(pyrowave_image_get_image_view(pyro_image[multiplane ? 0 : 1],
VK_IMAGE_ASPECT_PLANE_1_BIT, VK_IMAGE_USAGE_STORAGE_BIT, &buffers.planes[1]));
CHECKED(pyrowave_image_get_image_view(pyro_image[multiplane ? 0 : 2],
VK_IMAGE_ASPECT_PLANE_2_BIT, VK_IMAGE_USAGE_STORAGE_BIT, &buffers.planes[2]));
CHECKED(pyrowave_decoder_decode_gpu_buffer(decoder, &acquire, &release, &buffers));
}
static void send_payload_to_decoder(pyrowave_encoder encoder, pyrowave_decoder decoder)
{
size_t num_packets;
CHECKED(pyrowave_encoder_compute_num_packets(encoder, BitstreamSize, &num_packets));
ASSERT_THAT(num_packets == 1);
pyrowave_packet packet;
std::unique_ptr<uint8_t[]> bitstream(new uint8_t[BitstreamSize]);
CHECKED(pyrowave_encoder_packetize(encoder, &packet, BitstreamSize, &num_packets, bitstream.get(), BitstreamSize));
CHECKED(pyrowave_decoder_push_packet(decoder, bitstream.get() + packet.offset, packet.size));
ASSERT_THAT(pyrowave_decoder_decode_is_ready(decoder, false));
}
static void validate_mirror_buffer(const uint8_t *ptr, int width, int height, int row_stride, int dx, int dy)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int reference = mirror(x * dx + y * dy);
int value = ptr[y * row_stride + x];
int d = std::abs(reference - value);
ASSERT_THAT(d <= 1);
}
}
}
static void validate_mirror_buffer(Device &device, Buffer &buf, int width, int height, int dx, int dy)
{
auto *ptr = static_cast<const uint8_t *>(device.map_host_buffer(buf, MEMORY_ACCESS_READ_BIT));
validate_mirror_buffer(ptr, width, height, width, dx, dy);
}
static void validate_granite_image(Device &device, Image &img, SemaphoreHolder &sem, uint64_t acquire_value)
{
auto wait_sem = device.request_timeline_semaphore_as_binary(sem, acquire_value);
wait_sem->signal_external();
device.add_wait_semaphore(CommandBuffer::Type::Generic, std::move(wait_sem), VK_PIPELINE_STAGE_2_COPY_BIT, true);
auto cmd = device.request_command_buffer();
BufferCreateInfo bufinfo = {};
bufinfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
bufinfo.size = img.get_width() * img.get_height();
bufinfo.domain = BufferDomain::CachedHost;
auto y = device.create_buffer(bufinfo);
bufinfo.size /= 4;
auto cb = device.create_buffer(bufinfo);
auto cr = device.create_buffer(bufinfo);
cmd->acquire_image_barrier(img, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL,
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_EXTERNAL);
cmd->copy_image_to_buffer(*y, img, 0, {}, { img.get_width(), img.get_height(), 1 }, 1280, 720,
{ VK_IMAGE_ASPECT_PLANE_0_BIT, 0, 0, 1 });
cmd->copy_image_to_buffer(*cb, img, 0, {}, { img.get_width() / 2, img.get_height() / 2, 1 }, 640, 360,
{ VK_IMAGE_ASPECT_PLANE_1_BIT, 0, 0, 1 });
cmd->copy_image_to_buffer(*cr, img, 0, {}, { img.get_width() / 2, img.get_height() / 2, 1 }, 640, 360,
{ VK_IMAGE_ASPECT_PLANE_2_BIT, 0, 0, 1 });
cmd->barrier(VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_WRITE_BIT);
Fence fence;
device.submit(cmd, &fence);
fence->wait();
validate_mirror_buffer(device, *y, 1280, 720, 5, 3);
validate_mirror_buffer(device, *cb, 640, 360, 3, 1);
validate_mirror_buffer(device, *cr, 640, 360, 5, 7);
}
static void test_direct_interop()
{
ASSERT_THAT(Context::init_loader(nullptr));
Context ctx;
ctx.set_num_thread_indices(1);
ctx.set_system_handles({});
VkApplicationInfo app_info = { VK_STRUCTURE_TYPE_APPLICATION_INFO };
app_info.apiVersion = VK_API_VERSION_1_3;
app_info.pApplicationName = "pyrowave-c-test";
app_info.pEngineName = "Granite";
ctx.set_application_info(&app_info);
ASSERT_THAT(ctx.init_instance_and_device(nullptr, 0, nullptr, 0));
Device device;
device.set_context(ctx);
// Fill in a proxy instance create info.
VkInstanceCreateInfo instance_create_info = { VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
instance_create_info.enabledExtensionCount = device.get_device_features().num_instance_extensions;
instance_create_info.ppEnabledExtensionNames = device.get_device_features().instance_extensions;
instance_create_info.pApplicationInfo = &ctx.get_application_info();
// Fill in a proxy device create info.
VkDeviceCreateInfo device_create_info = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
VkDeviceQueueCreateInfo queue_info = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
queue_info.queueFamilyIndex = device.get_queue_info().family_indices[QUEUE_INDEX_GRAPHICS];
queue_info.queueCount = 1;
device_create_info.pNext = ctx.get_enabled_device_features().pdf2;
device_create_info.enabledExtensionCount = ctx.get_enabled_device_features().num_device_extensions;
device_create_info.ppEnabledExtensionNames = ctx.get_enabled_device_features().device_extensions;
device_create_info.queueCreateInfoCount = 1;
device_create_info.pQueueCreateInfos = &queue_info;
// Hand over a concrete VkQueue we want implementation to use.
pyrowave_device_create_queue_info device_queue_info = {};
device_queue_info.familyIndex = queue_info.queueFamilyIndex;
device_queue_info.index = 0;
device_queue_info.queue = device.get_queue_info().queues[QUEUE_INDEX_GRAPHICS];
pyrowave_device_create_info info = {};
info.GetInstanceProcAddr = vkGetInstanceProcAddr;
info.instance = ctx.get_instance();
info.physical_device = ctx.get_gpu();
info.device = ctx.get_device();
info.device_create_info = &device_create_info;
info.instance_create_info = &instance_create_info;
info.queue_info_count = 1;
info.queue_info = &device_queue_info;
info.userdata = &device;
info.queue_lock_callback = [](void *userdata) { static_cast<Device *>(userdata)->external_queue_lock(); };
info.queue_unlock_callback = [](void *userdata) { static_cast<Device *>(userdata)->external_queue_unlock(); };
pyrowave_encoder encoder;
pyrowave_decoder decoder;
pyrowave_device pyro_device;
CHECKED(pyrowave_create_device(&info, &pyro_device));
pyrowave_encoder_create_info encoder_info = {};
encoder_info.device = pyro_device;
encoder_info.chroma = PYROWAVE_CHROMA_SUBSAMPLING_444;
encoder_info.width = 64;
encoder_info.height = 64;
CHECKED(pyrowave_encoder_create(&encoder_info, &encoder));
pyrowave_decoder_create_info decoder_info = {};
decoder_info.device = pyro_device;
decoder_info.chroma = PYROWAVE_CHROMA_SUBSAMPLING_444;
decoder_info.width = 64;
decoder_info.height = 64;
CHECKED(pyrowave_decoder_create(&decoder_info, &decoder));
uint8_t plane_data[3][64][64];
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
plane_data[0][y][x] = y + x + 1;
plane_data[1][y][x] = y + x + 2;
plane_data[2][y][x] = y + x + 3;
}
}
auto image_info = ImageCreateInfo::immutable_2d_image(64, 64, VK_FORMAT_R8_UNORM);
image_info.layers = 3;
image_info.initial_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
ImageInitialData initial_data[3] = { { plane_data[0] }, { plane_data[1] }, { plane_data[2] } };
auto input_image = device.create_image(image_info, initial_data);
ASSERT_THAT(input_image);
image_info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
image_info.initial_layout = VK_IMAGE_LAYOUT_GENERAL;
auto output_image = device.create_image(image_info);
ASSERT_THAT(output_image);
pyrowave_rate_control rate_control = { 100000 };
pyrowave_gpu_buffers gpu_buffers = {};
for (int i = 0; i < 3; i++)
{
gpu_buffers.planes[i].image = input_image->get_image();
gpu_buffers.planes[i].width = 64;
gpu_buffers.planes[i].height = 64;
gpu_buffers.planes[i].image_format = VK_FORMAT_R8_UNORM;
gpu_buffers.planes[i].view_format = VK_FORMAT_R8_UNORM;
gpu_buffers.planes[i].layer = i;
gpu_buffers.planes[i].layout = input_image->get_layout(VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL);
gpu_buffers.planes[i].mip_level = 0;
gpu_buffers.planes[i].aspect = VK_IMAGE_ASPECT_COLOR_BIT;
}
auto cmd = device.request_command_buffer();
// Encode to provided cmd.
// Redirect commands here.
pyrowave_device_set_command_buffer(pyro_device, cmd->get_command_buffer());
CHECKED(pyrowave_encoder_encode_gpu_synchronous(encoder, nullptr, nullptr, &gpu_buffers, &rate_control));
pyrowave_device_set_command_buffer(pyro_device, VK_NULL_HANDLE);
// Wait on CPU before we call packetization.
Fence fence;
device.submit(cmd, &fence);
fence->wait();
size_t num_packets;
CHECKED(pyrowave_encoder_compute_num_packets(encoder, rate_control.maximum_bitstream_size, &num_packets));
ASSERT_THAT(num_packets == 1);
std::unique_ptr<uint8_t[]> bitstream(new uint8_t[rate_control.maximum_bitstream_size]);
pyrowave_packet packet;
CHECKED(pyrowave_encoder_packetize(encoder, &packet, rate_control.maximum_bitstream_size, &num_packets,
bitstream.get(), rate_control.maximum_bitstream_size));
CHECKED(pyrowave_decoder_push_packet(decoder, bitstream.get() + packet.offset, packet.size));
ASSERT_THAT(pyrowave_decoder_decode_is_ready(decoder, false));
for (auto &plane : gpu_buffers.planes)
{
plane.image = output_image->get_image();
plane.layout = VK_IMAGE_LAYOUT_GENERAL;
}
cmd = device.request_command_buffer();
// Redirect commands here.
pyrowave_device_set_command_buffer(pyro_device, cmd->get_command_buffer());
CHECKED(pyrowave_decoder_decode_gpu_buffer(decoder, nullptr, nullptr, &gpu_buffers));
pyrowave_device_set_command_buffer(pyro_device, VK_NULL_HANDLE);
cmd->image_barrier(*output_image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
BufferCreateInfo bufinfo = {};
bufinfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
bufinfo.size = 64 * 64 * 3;
bufinfo.domain = BufferDomain::CachedHost;
auto readback_buffer = device.create_buffer(bufinfo);
cmd->copy_image_to_buffer(*readback_buffer, *output_image, 0, {}, { 64, 64, 1 }, 0, 0,
{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 3 });
cmd->barrier(VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT);
fence.reset();
device.submit(cmd, &fence);
fence->wait();
auto *readback_ptr = static_cast<const uint8_t *>(device.map_host_buffer(*readback_buffer, MEMORY_ACCESS_READ_BIT));
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
int y_delta = std::abs(readback_ptr[0 * 64 * 64 + y * 64 + x] - (y + x + 1));
int cb_delta = std::abs(readback_ptr[1 * 64 * 64 + y * 64 + x] - (y + x + 2));
int cr_delta = std::abs(readback_ptr[2 * 64 * 64 + y * 64 + x] - (y + x + 3));
ASSERT_THAT(y_delta <= 1);
ASSERT_THAT(cb_delta <= 1);
ASSERT_THAT(cr_delta <= 1);
}
}
pyrowave_encoder_destroy(encoder);
pyrowave_decoder_destroy(decoder);
pyrowave_device_destroy(pyro_device);
}
// Most basic interop scenario, OPAQUE_FD for everything.
static void test_opaque_interop(bool win32_kmt)
{
ASSERT_THAT(Context::init_loader(nullptr));
Context ctx;
ctx.set_num_thread_indices(1);
ctx.set_system_handles({});
ASSERT_THAT(ctx.init_instance_and_device(nullptr, 0, nullptr, 0));
Device device;
device.set_context(ctx);
pyrowave_device pyro_device = create_device_from_granite(device);
auto semaphore_type = win32_kmt ?
VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT : ExternalHandle::get_opaque_semaphore_handle_type();
auto memory_type = win32_kmt ?
VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT : ExternalHandle::get_opaque_memory_handle_type();
// No impl seems to support OPAQUE_KMT timelines.
auto timeline_sem = device.request_semaphore_external(VK_SEMAPHORE_TYPE_TIMELINE, ExternalHandle::get_opaque_semaphore_handle_type());
ASSERT_THAT(timeline_sem);
pyrowave_sync_object imported_timeline = create_sync_object_from_timeline(pyro_device, *timeline_sem);
auto exportable_nv12_image = create_exportable_test_image(
device, memory_type,
VK_FORMAT_G8_B8R8_2PLANE_420_UNORM);
auto exportable_yuv420p_image = create_exportable_test_image(
device, memory_type,
VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM);
pyrowave_image imported_nv12_image = create_imported_image(pyro_device, device, *exportable_nv12_image);
pyrowave_image imported_yuv420p_image = create_imported_image(pyro_device, device, *exportable_yuv420p_image);
auto binary_sem = device.request_semaphore_external(VK_SEMAPHORE_TYPE_BINARY, semaphore_type);
ASSERT_THAT(binary_sem);
device.submit_empty(CommandBuffer::Type::Generic, nullptr, binary_sem.get());
pyrowave_sync_object imported_binary = create_sync_object_from_binary(pyro_device, *binary_sem);
pyrowave_encoder_create_info encoder_info = {};
encoder_info.device = pyro_device;
encoder_info.width = 1280;
encoder_info.height = 720;
encoder_info.chroma = PYROWAVE_CHROMA_SUBSAMPLING_420;
pyrowave_encoder encoder;
CHECKED(pyrowave_encoder_create(&encoder_info, &encoder));
// Test the two ways we can send a sync payload, binary + temporary or persistent timeline.
// Verify it doesn't explode.
send_granite_image_to_encoder(device, *exportable_nv12_image, imported_nv12_image,
*timeline_sem, imported_timeline, 1,
imported_timeline, 2,
encoder);
send_granite_image_to_encoder(device, *exportable_nv12_image, imported_nv12_image,
*timeline_sem, imported_binary, 0,
imported_timeline, 3,
encoder);
// Pyrowave (or rather, Granite) API destroys objects in a deferred way.
pyrowave_sync_object_destroy(imported_binary);
pyrowave_decoder_create_info decoder_info = {};
decoder_info.device = pyro_device;
decoder_info.width = 1280;
decoder_info.height = 720;
decoder_info.chroma = PYROWAVE_CHROMA_SUBSAMPLING_420;
pyrowave_decoder decoder;
CHECKED(pyrowave_decoder_create(&decoder_info, &decoder));
send_payload_to_decoder(encoder, decoder);
decode_image(&imported_yuv420p_image, true, imported_timeline, 4, decoder);
validate_granite_image(device, *exportable_yuv420p_image, *timeline_sem, 4);
pyrowave_sync_object_destroy(imported_timeline);
pyrowave_image_destroy(imported_nv12_image);
pyrowave_image_destroy(imported_yuv420p_image);
pyrowave_encoder_destroy(encoder);
pyrowave_decoder_destroy(decoder);
pyrowave_device_destroy(pyro_device);
}
static void test_drm_modifier_interop()
{
ASSERT_THAT(Context::init_loader(nullptr));
Context ctx;
ctx.set_num_thread_indices(1);
ctx.set_system_handles({});
ASSERT_THAT(ctx.init_instance_and_device(nullptr, 0, nullptr, 0));
Device device;
device.set_context(ctx);
if (!device.get_device_features().supports_drm_modifiers)
{
printf("Device does not support DRM modifiers, skipping test ...\n");
return;
}
pyrowave_device pyro_device = create_device_from_granite(device);
auto timeline_sem = device.request_semaphore_external(VK_SEMAPHORE_TYPE_TIMELINE,
ExternalHandle::get_opaque_semaphore_handle_type());
ASSERT_THAT(timeline_sem);
pyrowave_sync_object imported_timeline = create_sync_object_from_timeline(pyro_device, *timeline_sem);
auto exportable_nv12_image = create_exportable_test_image(
device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
VK_FORMAT_G8_B8R8_2PLANE_420_UNORM);
auto exportable_yuv420p_image = create_exportable_test_image(
device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM);
pyrowave_image imported_nv12_image = create_imported_image(pyro_device, device, *exportable_nv12_image);
pyrowave_image imported_yuv420p_image = create_imported_image(pyro_device, device, *exportable_yuv420p_image);
pyrowave_encoder_create_info encoder_info = {};
encoder_info.device = pyro_device;
encoder_info.width = 1280;
encoder_info.height = 720;
encoder_info.chroma = PYROWAVE_CHROMA_SUBSAMPLING_420;
pyrowave_encoder encoder;
CHECKED(pyrowave_encoder_create(&encoder_info, &encoder));
// Test the two ways we can send a sync payload, binary + temporary or persistent timeline.
// Verify it doesn't explode.
send_granite_image_to_encoder(device, *exportable_nv12_image, imported_nv12_image,
*timeline_sem, imported_timeline, 1,
imported_timeline, 2,
encoder);
pyrowave_decoder_create_info decoder_info = {};
decoder_info.device = pyro_device;
decoder_info.width = 1280;
decoder_info.height = 720;
decoder_info.chroma = PYROWAVE_CHROMA_SUBSAMPLING_420;
pyrowave_decoder decoder;
CHECKED(pyrowave_decoder_create(&decoder_info, &decoder));
send_payload_to_decoder(encoder, decoder);
decode_image(&imported_yuv420p_image, true, imported_timeline, 3, decoder);
validate_granite_image(device, *exportable_yuv420p_image, *timeline_sem, 3);
pyrowave_sync_object_destroy(imported_timeline);
pyrowave_image_destroy(imported_nv12_image);
pyrowave_image_destroy(imported_yuv420p_image);
pyrowave_encoder_destroy(encoder);
pyrowave_decoder_destroy(decoder);
pyrowave_device_destroy(pyro_device);
}
#ifdef _WIN32
static VkFormat convert_dxgi_format(DXGI_FORMAT format)
{
switch (format)
{
case DXGI_FORMAT_NV12: return VK_FORMAT_G8_B8R8_2PLANE_420_UNORM;
case DXGI_FORMAT_R8_UNORM: return VK_FORMAT_R8_UNORM;
default: ASSERT_THAT(0 && "augment as needed");
}
return VK_FORMAT_UNDEFINED;
}
static pyrowave_image create_pyrowave_image_from_d3d12(pyrowave_device pyro_device, ID3D12Device *device, ID3D12Resource *resource, bool storage)
{
HANDLE shared_handle;
CHECK_HRESULT(device->CreateSharedHandle(resource, nullptr, GENERIC_ALL, nullptr, &shared_handle));
auto desc = resource->GetDesc();
VkImageCreateInfo image_create_info = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
image_create_info.imageType = VK_IMAGE_TYPE_2D;
image_create_info.extent = { uint32_t(desc.Width), desc.Height, 1u };
image_create_info.mipLevels = desc.MipLevels;
// MUTABLE is needed since we will take plane views. Extended usage since the base planar format doesn't support STORAGE.
image_create_info.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
// The usage flags don't matter that much.
image_create_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
if (storage)
image_create_info.usage |= VK_IMAGE_USAGE_STORAGE_BIT;
image_create_info.format = convert_dxgi_format(desc.Format);
image_create_info.samples = static_cast<VkSampleCountFlagBits>(desc.SampleDesc.Count);
image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_create_info.arrayLayers = desc.DepthOrArraySize;
pyrowave_image_create_info info = {};
info.device = pyro_device;
info.external_handle = (pyrowave_os_handle)shared_handle;
info.handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT;
info.image_create_info = &image_create_info;
pyrowave_image image;
CHECKED(pyrowave_image_create(&info, &image));
return image;
}
static pyrowave_image create_pyrowave_image_from_d3d11(pyrowave_device pyro_device, ID3D11Texture2D *resource, bool kmt)
{
HANDLE shared_handle;
ComPtr<IDXGIResource1> res;
resource->QueryInterface(IID_IDXGIResource1, res.ppv());
if (kmt)
{
CHECK_HRESULT(res->GetSharedHandle(&shared_handle));
}
else
{
CHECK_HRESULT(res->CreateSharedHandle(nullptr, GENERIC_ALL, nullptr, &shared_handle));
}
D3D11_TEXTURE2D_DESC desc;
resource->GetDesc(&desc);
VkImageCreateInfo image_create_info = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
image_create_info.imageType = VK_IMAGE_TYPE_2D;
image_create_info.extent = { uint32_t(desc.Width), desc.Height, 1u };
image_create_info.mipLevels = desc.MipLevels;
// MUTABLE is needed since we will take plane views. Extended usage since the base planar format doesn't support STORAGE.
image_create_info.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
// The usage flags don't matter that much.
image_create_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
image_create_info.format = convert_dxgi_format(desc.Format);
image_create_info.samples = static_cast<VkSampleCountFlagBits>(desc.SampleDesc.Count);
image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_create_info.arrayLayers = desc.ArraySize;
pyrowave_image_create_info info = {};
info.device = pyro_device;
info.external_handle = (pyrowave_os_handle)shared_handle;
info.handle_type = kmt ? VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_KMT_BIT : VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT;
info.image_create_info = &image_create_info;
pyrowave_image image;
CHECKED(pyrowave_image_create(&info, &image));
return image;
}
static pyrowave_sync_object create_pyrowave_sync_from_d3d12_handle(pyrowave_device device, HANDLE handle)
{
pyrowave_sync_object_create_info info = {};
info.device = device;
info.external_handle = (pyrowave_os_handle)handle;
// D3D11 fence is aliased with D3D12 fence, it's the same thing in Windows 10+.
info.handle_type = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT;
// Technically it's BINARY in the spec since this sharing API predated timeline, but it gets fixed up by Granite as needed.
info.semaphore_type = VK_SEMAPHORE_TYPE_TIMELINE;
pyrowave_sync_object sync;
CHECKED(pyrowave_sync_object_create(&info, &sync));
return sync;
}
static void upload_mirror_image(ID3D12Device *device, ID3D12GraphicsCommandList *list, ID3D12Resource *resource,
UINT subresource, ComPtr<ID3D12Resource> &staging,
int dx0, int dy0, int dx1, int dy1)
{
auto desc = resource->GetDesc();
UINT64 total_bytes = 0;
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint;
device->GetCopyableFootprints(&desc, subresource, 1, 0, &footprint, nullptr, nullptr, &total_bytes);
ASSERT_THAT(total_bytes != UINT64_MAX);
D3D12_RESOURCE_DESC staging_desc = {};
staging_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
staging_desc.Width = total_bytes;
staging_desc.Height = 1;
staging_desc.DepthOrArraySize = 1;
staging_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
staging_desc.MipLevels = 1;
staging_desc.SampleDesc.Count = 1;
D3D12_HEAP_PROPERTIES heap_props = {};
heap_props.Type = D3D12_HEAP_TYPE_UPLOAD;
CHECK_HRESULT(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, &staging_desc,
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_ID3D12Resource, staging.ppv()));
uint8_t *ptr;
CHECK_HRESULT(staging->Map(0, nullptr, reinterpret_cast<void **>(&ptr)));
for (int y = 0; y < int(footprint.Footprint.Height); y++)
{
for (int x = 0; x < int(footprint.Footprint.Width); x++)
{
if (subresource == 0)
{
ptr[x] = mirror(y * dy0 + x * dx0);
}
else
{
ptr[2 * x + 0] = mirror(y * dy0 + x * dx0);
ptr[2 * x + 1] = mirror(y * dy1 + x * dx1);
}
}
ptr += footprint.Footprint.RowPitch;
}
staging->Unmap(0, nullptr);
D3D12_TEXTURE_COPY_LOCATION dst = {}, src = {};
dst.pResource = resource;
dst.SubresourceIndex = subresource;
dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
src.PlacedFootprint = footprint;
src.pResource = staging.get();
src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
}
static ComPtr<ID3D12Resource> readback_image(ID3D12Device *device, ID3D12GraphicsCommandList *list, ID3D12Resource *resource, UINT subresource, UINT64 *row_pitch)
{
auto desc = resource->GetDesc();
UINT64 total_bytes = 0;
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint;
device->GetCopyableFootprints(&desc, subresource, 1, 0, &footprint, nullptr, nullptr, &total_bytes);
ASSERT_THAT(total_bytes != UINT64_MAX);
D3D12_RESOURCE_DESC staging_desc = {};
staging_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
staging_desc.Width = total_bytes;
staging_desc.Height = 1;
staging_desc.DepthOrArraySize = 1;
staging_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
staging_desc.MipLevels = 1;
staging_desc.SampleDesc.Count = 1;
D3D12_HEAP_PROPERTIES heap_props = {};
heap_props.Type = D3D12_HEAP_TYPE_READBACK;
ComPtr<ID3D12Resource> staging;
CHECK_HRESULT(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, &staging_desc,
D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_ID3D12Resource, staging.ppv()));
D3D12_TEXTURE_COPY_LOCATION dst = {}, src = {};
src.pResource = resource;
src.SubresourceIndex = subresource;
src.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
dst.PlacedFootprint = footprint;
dst.pResource = staging.get();
dst.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
*row_pitch = footprint.Footprint.RowPitch;
return staging;
}
static void validate_d3d12_image(ID3D12Resource *readback, int width, int height, int row_stride, int dx, int dy)
{
uint8_t *ptr;
CHECK_HRESULT(readback->Map(0, nullptr, reinterpret_cast<void **>(&ptr)));
validate_mirror_buffer(ptr, width, height, row_stride, dx, dy);
readback->Unmap(0, nullptr);
}
static void test_nv12_interop()
{
// NV12 layout on NVIDIA is bizarre and requires us to hack around it when we import it in Vulkan.
// Smoke-test for debugging any interop issues.
ComPtr<ID3D12Device> device;
ComPtr<ID3D12CommandQueue> queue;
ComPtr<ID3D12Fence> fence;
ComPtr<ID3D12GraphicsCommandList> list;
ComPtr<ID3D12CommandAllocator> allocator;
uint64_t timeline = 0;
CHECK_HRESULT(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_ID3D12Device, device.ppv()));
CHECK_HRESULT(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_ID3D12CommandAllocator, allocator.ppv()));
CHECK_HRESULT(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.get(), nullptr, IID_ID3D12GraphicsCommandList, list.ppv()));
// Base API create command list starts a new command list, which we usually need to close right away ...
CHECK_HRESULT(list->Close());
D3D12_COMMAND_QUEUE_DESC queue_desc = {};
queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
CHECK_HRESULT(device->CreateCommandQueue(&queue_desc, IID_ID3D12CommandQueue, queue.ppv()));
CHECK_HRESULT(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_ID3D12Fence, fence.ppv()));
LUID luid = device->GetAdapterLuid();
static_assert(sizeof(luid) == sizeof(pyrowave_luid), "LUID struct size does not match.");
Context context;
Device dev;
ASSERT_THAT(Context::init_loader(nullptr));
context.set_num_thread_indices(1);
context.set_system_handles({});
// Just enable video extensions so that we can use video image usage, but don't bother creating queues for it, etc.
ASSERT_THAT(context.init_instance(nullptr, 0, CONTEXT_CREATION_ENABLE_VIDEO_FEATURE_ONLY_BIT));
uint32_t count;
ASSERT_THAT(vkEnumeratePhysicalDevices(context.get_instance(), &count, nullptr) == VK_SUCCESS);
std::vector<VkPhysicalDevice> gpus(count);
ASSERT_THAT(vkEnumeratePhysicalDevices(context.get_instance(), &count, gpus.data()) == VK_SUCCESS);
VkPhysicalDevice selected_gpu = VK_NULL_HANDLE;
for (auto &gpu : gpus)
{
VkPhysicalDeviceProperties props = {};
vkGetPhysicalDeviceProperties(gpu, &props);
// Is this even possible these days?
if (props.apiVersion < VK_API_VERSION_1_2)
continue;
VkPhysicalDeviceIDProperties ids = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES };
VkPhysicalDeviceProperties2 props2 = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &ids };
vkGetPhysicalDeviceProperties2(gpu, &props2);
if (!ids.deviceLUIDValid)
continue;
if (memcmp(&luid, ids.deviceLUID, VK_LUID_SIZE) != 0)
continue;
ASSERT_THAT(context.init_device(gpu, VK_NULL_HANDLE, nullptr, 0, CONTEXT_CREATION_ENABLE_VIDEO_FEATURE_ONLY_BIT));
selected_gpu = gpu;
}
ASSERT_THAT(selected_gpu);
dev.set_context(context);
// NV Windows is quite broken here and no matter what we do,
// it will only work for very specific resource sizes it seems, even with the video usage hacks ...
constexpr uint32_t Width = 1024;
constexpr uint32_t Height = 1024;
D3D12_RESOURCE_DESC resource_desc = {};
resource_desc.Width = Width;
resource_desc.Height = Height;
resource_desc.DepthOrArraySize = 1;
resource_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
resource_desc.SampleDesc.Count = 1;
resource_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
resource_desc.MipLevels = 1;
D3D12_HEAP_PROPERTIES heap_props = {};
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
ComPtr<ID3D12Resource> nv12;
resource_desc.Format = DXGI_FORMAT_NV12;
CHECK_HRESULT(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_SHARED, &resource_desc,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_ID3D12Resource, nv12.ppv()));
// Upload frame to NV12 image async.
list->Reset(allocator.get(), nullptr);
ComPtr<ID3D12Resource> staging_buffers[2]; // Verify that sync works somewhat so defer destroy these.
upload_mirror_image(device.get(), list.get(), nv12.get(), 0, staging_buffers[0], 5, 3, 0, 0);
upload_mirror_image(device.get(), list.get(), nv12.get(), 1, staging_buffers[1], 3, 1, 5, 7);
list->Close();
ID3D12CommandList *lists[] = { list.get() };
queue->ExecuteCommandLists(1, lists);
queue->Signal(fence.get(), ++timeline);
fence->SetEventOnCompletion(timeline, nullptr);
allocator->Reset();
list->Reset(allocator.get(), nullptr);
ComPtr<ID3D12Resource> readback_buffer;
UINT64 row_pitch = {};
readback_buffer = readback_image(device.get(), list.get(), nv12.get(), 1, &row_pitch);
list->Close();
queue->ExecuteCommandLists(1, lists);
queue->Signal(fence.get(), ++timeline);
fence->SetEventOnCompletion(timeline, nullptr);
auto img_info = ImageCreateInfo::immutable_2d_image(Width, Height, VK_FORMAT_G8_B8R8_2PLANE_420_UNORM);
img_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_VIDEO_ENCODE_SRC_BIT_KHR;
img_info.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT | VK_IMAGE_CREATE_VIDEO_PROFILE_INDEPENDENT_BIT_KHR;
img_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
img_info.misc = IMAGE_MISC_EXTERNAL_MEMORY_BIT | IMAGE_MISC_NO_DEFAULT_VIEWS_BIT;
CHECK_HRESULT(device->CreateSharedHandle(nv12.get(), nullptr, GENERIC_ALL, nullptr, &img_info.external.handle));
img_info.external.memory_handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT;
auto img = dev.create_image(img_info);
ASSERT_THAT(img);
BufferCreateInfo bufinfo = {};
bufinfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
bufinfo.domain = BufferDomain::CachedHost;
bufinfo.size = Width * Height / 2;
auto buffer = dev.create_buffer(bufinfo);
auto cmd = dev.request_command_buffer();
cmd->acquire_image_barrier(*img, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
cmd->copy_image_to_buffer(*buffer, *img, 0, {}, { Width / 2, Height / 2, 1 }, Width / 2, Height / 2, { VK_IMAGE_ASPECT_PLANE_1_BIT, 0, 0, 1 });
cmd->barrier(VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT);
Fence sync;
dev.submit(cmd, &sync);
sync->wait();
uint8_t *d3d12_ptr;
CHECK_HRESULT(readback_buffer->Map(0, nullptr, (void **)&d3d12_ptr));
const auto *ptr = static_cast<uint8_t *>(dev.map_host_buffer(*buffer, MEMORY_ACCESS_READ_BIT));
for (uint32_t y = 0; y < Height / 2; y++)
{
for (uint32_t x = 0; x < Width / 2; x++)
{
uint32_t pix = y * Width / 2 + x;
ASSERT_THAT(ptr[2 * pix + 0] == d3d12_ptr[y * row_pitch + 2 * x + 0]);
ASSERT_THAT(ptr[2 * pix + 1] == d3d12_ptr[y * row_pitch + 2 * x + 1]);
}
}
readback_buffer->Unmap(0, nullptr);
}
static void test_d3d12_interop_allocation_stress(ID3D12Device *device, pyrowave_device pyro_device)
{
D3D12_RESOURCE_DESC resource_desc = {};
resource_desc.Width = 4096;
resource_desc.Height = 4096;
resource_desc.DepthOrArraySize = 1;
resource_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
resource_desc.SampleDesc.Count = 1;
resource_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
resource_desc.MipLevels = 1;
D3D12_HEAP_PROPERTIES heap_props = {};
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
for (int i = 0; i < 10000; i++)
{
ComPtr<ID3D12Resource> img;
resource_desc.Format = DXGI_FORMAT_R8_UNORM;
CHECK_HRESULT(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_SHARED, &resource_desc,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_ID3D12Resource, img.ppv()));
pyrowave_image pyro_img = create_pyrowave_image_from_d3d12(pyro_device, device, img.get(), true);
// Check to see if destruction order matters.
if (i % 2)
{
pyrowave_image_destroy(pyro_img);
img = {};
}
else
{
img = {};
pyrowave_image_destroy(pyro_img);
}
}
for (int i = 0; i < 10000; i++)
{
// Sharing D3D12 to Vulkan is well supported. Other way around, not so much.
ComPtr<ID3D12Fence> fence;
CHECK_HRESULT(device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_ID3D12Fence, fence.ppv()));
HANDLE fence_handle;
CHECK_HRESULT(device->CreateSharedHandle(fence.get(), nullptr, GENERIC_ALL, nullptr, &fence_handle));
pyrowave_sync_object pyro_sync = create_pyrowave_sync_from_d3d12_handle(pyro_device, fence_handle);
// Check to see if destruction order matters.
if (i % 2)
{
pyrowave_sync_object_destroy(pyro_sync);
fence = {};
}
else
{
fence = {};
pyrowave_sync_object_destroy(pyro_sync);
}
}
}
static void test_d3d11_interop()
{
uint32_t index = 0;
if (const char *env = getenv("ADAPTER"))
index = strtoul(env, nullptr, 0);
bool validate = false;
if (const char *env = getenv("VALIDATE"))
validate = strtoul(env, nullptr, 0) != 0;
ComPtr<IDXGIFactory1> factory;
ComPtr<IDXGIAdapter> adapter;
ComPtr<ID3D11Device> device;
ComPtr<ID3D11Device5> device5;
ComPtr<ID3D11DeviceContext> context;
ComPtr<ID3D11DeviceContext4> context4;
ComPtr<ID3D11Fence> fence;
CHECK_HRESULT(CreateDXGIFactory1(IID_IDXGIFactory, factory.ppv()));
CHECK_HRESULT(factory->EnumAdapters(index, (IDXGIAdapter **)adapter.ppv()));
HRESULT hr = D3D11CreateDevice(adapter.get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, validate ? D3D11_CREATE_DEVICE_DEBUG : 0, nullptr, 0, D3D11_SDK_VERSION,
(ID3D11Device **)device.ppv(), nullptr, (ID3D11DeviceContext **)context.ppv());
ASSERT_THAT(SUCCEEDED(hr));
CHECK_HRESULT(device->QueryInterface(IID_ID3D11Device5, device5.ppv()));
DXGI_ADAPTER_DESC adapter_desc;
CHECK_HRESULT(adapter->GetDesc(&adapter_desc));
LUID luid = adapter_desc.AdapterLuid;
static_assert(sizeof(luid) == sizeof(pyrowave_luid), "LUID struct size does not match.");
CHECK_HRESULT(context->QueryInterface(IID_ID3D11DeviceContext4, context4.ppv()));
pyrowave_device pyro_device;
CHECKED(pyrowave_create_device_by_compat(0, 0, nullptr, nullptr,
reinterpret_cast<pyrowave_luid *>(&luid), &pyro_device));
for (int i = 0; i < 10000; i++)
{
ComPtr<ID3D11Texture2D> tex;
D3D11_TEXTURE2D_DESC desc = {};
desc.Format = DXGI_FORMAT_R8_UNORM;
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED | D3D11_RESOURCE_MISC_SHARED_NTHANDLE;
desc.SampleDesc.Count = 1;
desc.Width = 4096;
desc.Height = 4096;
desc.ArraySize = 1;
desc.MipLevels = 1;
CHECK_HRESULT(device5->CreateTexture2D(&desc, nullptr, (ID3D11Texture2D **)tex.ppv()));
pyrowave_image pyro_img = create_pyrowave_image_from_d3d11(pyro_device, tex.get(), false);
// Check to see if destruction order matters.
if (i % 2)
{
pyrowave_image_destroy(pyro_img);
fence = {};
}
else
{
fence = {};
pyrowave_image_destroy(pyro_img);
}
// Flushing seems to be load bearing here or GPU memory usage baloons out of control.
context->Flush();
}
for (int i = 0; i < 10000; i++)
{
ComPtr<ID3D11Texture2D> tex;
D3D11_TEXTURE2D_DESC desc = {};
desc.Format = DXGI_FORMAT_R8_UNORM;
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
desc.SampleDesc.Count = 1;
desc.Width = 4096;
desc.Height = 4096;
desc.ArraySize = 1;
desc.MipLevels = 1;
CHECK_HRESULT(device5->CreateTexture2D(&desc, nullptr, (ID3D11Texture2D **)tex.ppv()));
pyrowave_image pyro_img = create_pyrowave_image_from_d3d11(pyro_device, tex.get(), true);
// Check to see if destruction order matters.
if (i % 2)
{
pyrowave_image_destroy(pyro_img);
fence = {};
}
else
{
fence = {};
pyrowave_image_destroy(pyro_img);
}
// Flushing seems to be load bearing here or GPU memory usage baloons out of control.
context->Flush();
}
for (int i = 0; i < 10000; i++)
{
// Sharing D3D11 to Vulkan is well supported. Other way around, not so much.
ComPtr<ID3D11Fence> share_fence;
CHECK_HRESULT(device5->CreateFence(0, D3D11_FENCE_FLAG_SHARED, IID_ID3D11Fence, share_fence.ppv()));
HANDLE fence_handle;
CHECK_HRESULT(share_fence->CreateSharedHandle(nullptr, GENERIC_ALL, nullptr, &fence_handle));
context4->Signal(share_fence.get(), 1);
share_fence = {};
pyrowave_sync_object pyro_sync = create_pyrowave_sync_from_d3d12_handle(pyro_device, fence_handle);
// Check to see if destruction order matters.
if (i % 2)
{
pyrowave_sync_object_destroy(pyro_sync);
fence = {};
}
else
{
fence = {};
pyrowave_sync_object_destroy(pyro_sync);
}
context->Flush();
}
pyrowave_device_destroy(pyro_device);
}
static void test_d3d12_interop()
{
ComPtr<ID3D12Device> device;
ComPtr<ID3D12CommandQueue> queue;
ComPtr<ID3D12Fence> fence;
ComPtr<ID3D12GraphicsCommandList> list;
ComPtr<ID3D12CommandAllocator> allocator;
uint64_t timeline = 0;
uint32_t index = 0;
if (const char *env = getenv("ADAPTER"))
index = strtoul(env, nullptr, 0);
ComPtr<IDXGIFactory1> factory;
ComPtr<IDXGIAdapter> adapter;
CHECK_HRESULT(CreateDXGIFactory1(IID_IDXGIFactory, factory.ppv()));
CHECK_HRESULT(factory->EnumAdapters(index, (IDXGIAdapter **)adapter.ppv()));
CHECK_HRESULT(D3D12CreateDevice(adapter.get(), D3D_FEATURE_LEVEL_11_0, IID_ID3D12Device, device.ppv()));
CHECK_HRESULT(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_ID3D12CommandAllocator, allocator.ppv()));
CHECK_HRESULT(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.get(), nullptr, IID_ID3D12GraphicsCommandList, list.ppv()));
// Base API create command list starts a new command list, which we usually need to close right away ...
CHECK_HRESULT(list->Close());
D3D12_COMMAND_QUEUE_DESC queue_desc = {};
queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
CHECK_HRESULT(device->CreateCommandQueue(&queue_desc, IID_ID3D12CommandQueue, queue.ppv()));
// Sharing D3D12 to Vulkan is well supported. Other way around, not so much.
CHECK_HRESULT(device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_ID3D12Fence, fence.ppv()));
LUID luid = device->GetAdapterLuid();
static_assert(sizeof(luid) == sizeof(pyrowave_luid), "LUID struct size does not match.");
pyrowave_device pyro_device;
CHECKED(pyrowave_create_device_by_compat(0, 0, nullptr, nullptr,
reinterpret_cast<pyrowave_luid *>(&luid), &pyro_device));
test_d3d12_interop_allocation_stress(device.get(), pyro_device);
// NV Windows is quite broken here and no matter what we do, it will only work for very specific resource sizes it seems ...
constexpr uint32_t Width = 1024;
constexpr uint32_t Height = 1024;
D3D12_RESOURCE_DESC resource_desc = {};
resource_desc.Width = Width;
resource_desc.Height = Height;
resource_desc.DepthOrArraySize = 1;
resource_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
resource_desc.SampleDesc.Count = 1;
resource_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
resource_desc.MipLevels = 1;
D3D12_HEAP_PROPERTIES heap_props = {};
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
ComPtr<ID3D12Resource> nv12, yuv420p[3];
resource_desc.Format = DXGI_FORMAT_NV12;
CHECK_HRESULT(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_SHARED, &resource_desc,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_ID3D12Resource, nv12.ppv()));
// DXGI doesn't have proper 3-plane format. Just test the path with 3x R8 images.
// Also adds some test coverage to that path.
resource_desc.Format = DXGI_FORMAT_R8_UNORM;
CHECK_HRESULT(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_SHARED, &resource_desc,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_ID3D12Resource, yuv420p[0].ppv()));
resource_desc.Width /= 2;
resource_desc.Height /= 2;
for (int plane = 1; plane < 3; plane++)
{
CHECK_HRESULT(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_SHARED, &resource_desc,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_ID3D12Resource, yuv420p[plane].ppv()));
}
pyrowave_image pyro_nv12 = create_pyrowave_image_from_d3d12(pyro_device, device.get(), nv12.get(), false);
pyrowave_image pyro_yuv420p[3] = {};
for (int plane = 0; plane < 3; plane++)
pyro_yuv420p[plane] = create_pyrowave_image_from_d3d12(pyro_device, device.get(), yuv420p[plane].get(), true);
HANDLE fence_handle;
CHECK_HRESULT(device->CreateSharedHandle(fence.get(), nullptr, GENERIC_ALL, nullptr, &fence_handle));
pyrowave_sync_object pyro_sync = create_pyrowave_sync_from_d3d12_handle(pyro_device, fence_handle);
pyrowave_encoder_create_info encoder_info = {};
encoder_info.device = pyro_device;
encoder_info.width = Width;
encoder_info.height = Height;
pyrowave_encoder encoder;
CHECKED(pyrowave_encoder_create(&encoder_info, &encoder));
pyrowave_decoder_create_info decoder_info = {};
decoder_info.device = pyro_device;
decoder_info.width = Width;
decoder_info.height = Height;
pyrowave_decoder decoder;
CHECKED(pyrowave_decoder_create(&decoder_info, &decoder));
// Upload frame to NV12 image async.
list->Reset(allocator.get(), nullptr);
ComPtr<ID3D12Resource> staging_buffers[2]; // Verify that sync works somewhat so defer destroy these.
upload_mirror_image(device.get(), list.get(), nv12.get(), 0, staging_buffers[0], 5, 3, 0, 0);
upload_mirror_image(device.get(), list.get(), nv12.get(), 1, staging_buffers[1], 3, 1, 5, 7);
list->Close();
// Submit to D3D12 queue and signal the shared timeline.
ID3D12CommandList *lists[] = { list.get() };
queue->ExecuteCommandLists(1, lists);
queue->Signal(fence.get(), ++timeline);
send_image_to_encoder(pyro_nv12, pyro_sync, timeline, pyro_sync, timeline + 1, encoder);
timeline++;
// This is a blocking call since we read back the payload on CPU (hopefully on a different machine).
send_payload_to_decoder(encoder, decoder);
// Decode and send image back to D3D12.
decode_image(pyro_yuv420p, false, pyro_sync, ++timeline, decoder);
queue->Wait(fence.get(), timeline);
allocator->Reset();
list->Reset(allocator.get(), nullptr);
ComPtr<ID3D12Resource> readback_buffers[3];
UINT64 row_pitch[3] = {};
for (int plane = 0; plane < 3; plane++)
readback_buffers[plane] = readback_image(device.get(), list.get(), yuv420p[plane].get(), 0, &row_pitch[plane]);
CHECK_HRESULT(list->Close());
queue->ExecuteCommandLists(1, lists);
// Wait for device to go idle.
queue->Signal(fence.get(), ++timeline);
// null event handle blocks on CPU directly.
fence->SetEventOnCompletion(timeline, nullptr);
validate_d3d12_image(readback_buffers[0].get(), Width, Height, row_pitch[0], 5, 3);
validate_d3d12_image(readback_buffers[1].get(), Width / 2, Height / 2, row_pitch[1], 3, 1);
validate_d3d12_image(readback_buffers[2].get(), Width / 2, Height / 2, row_pitch[2], 5, 7);
pyrowave_image_destroy(pyro_nv12);
for (auto &img : pyro_yuv420p)
pyrowave_image_destroy(img);
pyrowave_sync_object_destroy(pyro_sync);
pyrowave_decoder_destroy(decoder);
pyrowave_encoder_destroy(encoder);
pyrowave_device_destroy(pyro_device);
}
struct SharedBlock
{
HANDLE texture[3];
HANDLE fence;
HANDLE sem;
LUID luid;
uint32_t width, height;
uint8_t payload[1024 * 1024];
uint32_t payload_size;
uint64_t wait_value;
uint64_t signal_value;
bool dead;
};
static void test_d3d11_cross_process_encode()
{
ComPtr<IDXGIFactory1> factory;
ComPtr<IDXGIAdapter> adapter;
ComPtr<ID3D11Device> device;
ComPtr<ID3D11Device5> device5;
ComPtr<ID3D11DeviceContext> context;
ComPtr<ID3D11DeviceContext4> context4;
ComPtr<ID3D11Fence> fence;
uint32_t index = 0;
if (const char *env = getenv("ADAPTER"))
index = strtoul(env, nullptr, 0);
CHECK_HRESULT(CreateDXGIFactory1(IID_IDXGIFactory, factory.ppv()));
CHECK_HRESULT(factory->EnumAdapters(index, (IDXGIAdapter **)adapter.ppv()));
HRESULT hr = D3D11CreateDevice(adapter.get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION,
(ID3D11Device **)device.ppv(), nullptr, (ID3D11DeviceContext **)context.ppv());
ASSERT_THAT(SUCCEEDED(hr));
CHECK_HRESULT(device->QueryInterface(IID_ID3D11Device5, device5.ppv()));
DXGI_ADAPTER_DESC adapter_desc;
CHECK_HRESULT(adapter->GetDesc(&adapter_desc));
LUID luid = adapter_desc.AdapterLuid;
static_assert(sizeof(luid) == sizeof(pyrowave_luid), "LUID struct size does not match.");
HANDLE mapping = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(SharedBlock), "PyroFlingTestDummy");
ASSERT_THAT(mapping != INVALID_HANDLE_VALUE && mapping != nullptr);
auto *shared = static_cast<SharedBlock *>(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedBlock)));
ASSERT_THAT(shared);
memset(shared, 0, sizeof(*shared));
shared->luid = luid;
shared->width = 1280;
shared->height = 720;
CHECK_HRESULT(context->QueryInterface(IID_ID3D11DeviceContext4, context4.ppv()));
char self[4096];
DWORD ret = GetModuleFileNameA(GetModuleHandle(nullptr), self, sizeof(self));
self[ret] = '\0';
strcat(self, " --child");
HANDLE job_handle = CreateJobObjectA(nullptr, nullptr);
ASSERT_THAT(job_handle);
// Kill all child processes if the parent dies.
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {};
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
ASSERT_THAT(SetInformationJobObject(job_handle, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli)));
STARTUPINFOA si = {};
si.cb = sizeof(STARTUPINFOA);
PROCESS_INFORMATION pi;
ASSERT_THAT(CreateProcessA(nullptr, self, nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED,
nullptr, nullptr, &si, &pi));
ASSERT_THAT(AssignProcessToJobObject(job_handle, pi.hProcess));
HANDLE semaphore = CreateSemaphoreA(nullptr, 0, 1, "PyroFlingSemDummy");
ASSERT_THAT(semaphore);
shared->wait_value = 1;
shared->signal_value = 2;
ResumeThread(pi.hThread);
pyrowave_device pyro_device;
CHECKED(pyrowave_create_default_device(&pyro_device));
pyrowave_decoder decoder;
pyrowave_decoder_create_info decoder_info = {};
decoder_info.device = pyro_device;
decoder_info.width = 1280;
decoder_info.height = 720;
CHECKED(pyrowave_decoder_create(&decoder_info, &decoder));
std::unique_ptr<uint8_t[]> y_data(new uint8_t[1280 * 720]);
std::unique_ptr<uint8_t[]> c_data(new uint8_t[640 * 360]);
std::unique_ptr<uint8_t[]> decoded_y_data(new uint8_t[1280 * 720]);
std::unique_ptr<uint8_t[]> decoded_cb_data(new uint8_t[640 * 360]);
std::unique_ptr<uint8_t[]> decoded_cr_data(new uint8_t[640 * 360]);
ComPtr<ID3D11Texture2D> tex[3];
// Do encoding work.
for (int i = 0; i < 1000; i++)
{
// Create new handles every so often.
if (i % 10 == 0)
{
D3D11_TEXTURE2D_DESC desc = {};
desc.Format = DXGI_FORMAT_R8_UNORM;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED | D3D11_RESOURCE_MISC_SHARED_NTHANDLE;
desc.SampleDesc.Count = 1;
desc.Width = 1280;
desc.Height = 720;
desc.ArraySize = 1;
desc.MipLevels = 1;
CHECK_HRESULT(device5->CreateTexture2D(&desc, nullptr, (ID3D11Texture2D **)tex[0].ppv()));
desc.Width /= 2;
desc.Height /= 2;
CHECK_HRESULT(device5->CreateTexture2D(&desc, nullptr, (ID3D11Texture2D **)tex[1].ppv()));
CHECK_HRESULT(device5->CreateTexture2D(&desc, nullptr, (ID3D11Texture2D **)tex[2].ppv()));
for (int j = 0; j < 3; j++)
{
ComPtr<IDXGIResource1> res;
tex[j]->QueryInterface(IID_IDXGIResource1, res.ppv());
HANDLE shared_handle;
CHECK_HRESULT(res->CreateSharedHandle(nullptr, GENERIC_ALL, nullptr, &shared_handle));
ASSERT_THAT(DuplicateHandle(GetCurrentProcess(), shared_handle, pi.hProcess,
&shared->texture[j], GENERIC_ALL, FALSE, DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS));
}
CHECK_HRESULT(device5->CreateFence(0, D3D11_FENCE_FLAG_SHARED, IID_ID3D11Fence, fence.ppv()));
HANDLE shared_handle;
CHECK_HRESULT(fence->CreateSharedHandle(nullptr, GENERIC_ALL, nullptr, &shared_handle));
ASSERT_THAT(DuplicateHandle(GetCurrentProcess(), shared_handle, pi.hProcess,
&shared->fence, GENERIC_ALL, FALSE, DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS));
}
D3D11_BOX box = {};
box.right = 1280;
box.bottom = 720;
box.back = 1;
memset(y_data.get(), i & 0xff, 1280 * 720);
context->UpdateSubresource(tex[0].get(), 0, &box, y_data.get(), 1280, 1280 * 720);
box.right = 640;
box.bottom = 360;
memset(c_data.get(), (i + 1) & 0xff, 640 * 360);
context->UpdateSubresource(tex[1].get(), 0, &box, c_data.get(), 640, 640 * 360);
memset(c_data.get(), (i + 2) & 0xff, 640 * 360);
context->UpdateSubresource(tex[2].get(), 0, &box, c_data.get(), 640, 640 * 360);
context4->Signal(fence.get(), shared->wait_value);
ASSERT_THAT(ReleaseSemaphore(semaphore, 1, nullptr));
// Wait until encoder is done.
CHECK_HRESULT(fence->SetEventOnCompletion(shared->signal_value, nullptr));
ASSERT_THAT(shared->payload_size);
pyrowave_decoder_clear(decoder);
CHECKED(pyrowave_decoder_push_packet(decoder, shared->payload, shared->payload_size));
ASSERT_THAT(pyrowave_decoder_decode_is_ready(decoder, false));
pyrowave_cpu_buffer cpu_buffers = {};
cpu_buffers.data[0] = decoded_y_data.get();
cpu_buffers.data[1] = decoded_cb_data.get();
cpu_buffers.data[2] = decoded_cr_data.get();
cpu_buffers.width = 1280;
cpu_buffers.height = 720;
cpu_buffers.format = PYROWAVE_CPU_BUFFER_FORMAT_YUV420P;
cpu_buffers.plane_size_in_bytes[0] = 1280 * 720;
cpu_buffers.plane_size_in_bytes[1] = 640 * 360;
cpu_buffers.plane_size_in_bytes[2] = 640 * 360;
cpu_buffers.row_stride_in_bytes[0] = 1280;
cpu_buffers.row_stride_in_bytes[1] = 640;
cpu_buffers.row_stride_in_bytes[2] = 640;
CHECKED(pyrowave_decoder_decode_cpu_buffer_synchronous(decoder, &cpu_buffers));
for (int pix = 0; pix < 1280 * 720; pix++)
ASSERT_THAT(decoded_y_data[pix] == (i & 0xff));
for (int pix = 0; pix < 640 * 360; pix++)
ASSERT_THAT(decoded_cb_data[pix] == ((i + 1) & 0xff));
for (int pix = 0; pix < 640 * 360; pix++)
ASSERT_THAT(decoded_cr_data[pix] == ((i + 2) & 0xff));
shared->wait_value += 2;
shared->signal_value += 2;
LOGI("Running cross-process frame %u / 1000 ...\n", i);
}
CloseHandle(pi.hThread);
shared->dead = true;
ASSERT_THAT(ReleaseSemaphore(semaphore, 1, nullptr));
ASSERT_THAT(WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_OBJECT_0);
ASSERT_THAT(!shared->dead);
CloseHandle(pi.hProcess);
CloseHandle(semaphore);
CloseHandle(job_handle);
pyrowave_decoder_destroy(decoder);
pyrowave_device_destroy(pyro_device);
}
static void test_child_interop()
{
//__debugbreak();
// Open shared handles.
HANDLE semaphore = CreateSemaphoreA(nullptr, 0, 1, "PyroFlingSemDummy");
ASSERT_THAT(semaphore);
HANDLE mapping = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(SharedBlock), "PyroFlingTestDummy");
ASSERT_THAT(mapping != INVALID_HANDLE_VALUE && mapping != nullptr);
auto *shared = static_cast<SharedBlock *>(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedBlock)));
ASSERT_THAT(shared);
pyrowave_device device;
CHECKED(pyrowave_create_device_by_compat(0, 0, nullptr, nullptr, reinterpret_cast<const pyrowave_luid *>(&shared->luid), &device));
pyrowave_image img[3] = {};
pyrowave_sync_object sync = {};
pyrowave_encoder encoder = {};
pyrowave_encoder_create_info encoder_info = {};
encoder_info.device = device;
encoder_info.width = shared->width;
encoder_info.height = shared->height;
encoder_info.chroma = PYROWAVE_CHROMA_SUBSAMPLING_420;
CHECKED(pyrowave_encoder_create(&encoder_info, &encoder));
pyrowave_gpu_external_reference refs[3];
// Encode loop
for (;;)
{
WaitForSingleObject(semaphore, INFINITE);
if (shared->dead)
break;
for (int i = 0; i < 3; i++)
{
if (shared->texture[i])
{
if (img[i])
pyrowave_image_destroy(img[i]);
VkImageCreateInfo image_info = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
image_info.extent = { shared->width / (i ? 2 : 1), shared->height / (i ? 2 : 1), 1 };
image_info.format = VK_FORMAT_R8_UNORM;
image_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.arrayLayers = 1;
image_info.mipLevels = 1;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.imageType = VK_IMAGE_TYPE_2D;
pyrowave_image_create_info info = {};
info.device = device;
info.handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT;
info.external_handle = (pyrowave_os_handle)shared->texture[i];
info.image_create_info = &image_info;
CHECKED(pyrowave_image_create(&info, &img[i]));
refs[i] = { img[i], VK_QUEUE_FAMILY_EXTERNAL };
shared->texture[i] = nullptr;
}
}
if (shared->fence)
{
if (sync)
pyrowave_sync_object_destroy(sync);
pyrowave_sync_object_create_info sync_info = {};
sync_info.device = device;
sync_info.external_handle = (pyrowave_os_handle)shared->fence;
sync_info.handle_type = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT;
sync_info.semaphore_type = VK_SEMAPHORE_TYPE_TIMELINE;
CHECKED(pyrowave_sync_object_create(&sync_info, &sync));
shared->fence = nullptr;
}
pyrowave_rate_control rate_control = { sizeof(shared->payload) };
pyrowave_gpu_buffers buffers = {};
pyrowave_gpu_sync_operation acquire = {};
pyrowave_gpu_sync_operation release = {};
acquire.sync.semaphore = pyrowave_sync_object_get_semaphore(sync);
acquire.sync.value = shared->wait_value;
acquire.num_images = 3;
acquire.images = refs;
release.num_images = 3;
release.images = refs;
for (int i = 0; i < 3; i++)
CHECKED(pyrowave_image_get_image_view(img[i], VkImageAspectFlagBits(VK_IMAGE_ASPECT_PLANE_0_BIT << i), VK_IMAGE_USAGE_SAMPLED_BIT, &buffers.planes[i]));
CHECKED(pyrowave_encoder_encode_gpu_synchronous(encoder, &acquire, &release, &buffers, &rate_control));
pyrowave_packet packet;
size_t out_packets = 1;
CHECKED(pyrowave_encoder_packetize(encoder, &packet, sizeof(shared->payload), &out_packets, shared->payload, sizeof(shared->payload)));
shared->payload_size = packet.size;
// API sanity check.
CHECKED(pyrowave_sync_object_cpu_wait(sync, shared->wait_value, UINT64_MAX));
// Signal on CPU.
CHECKED(pyrowave_sync_object_cpu_signal(sync, shared->signal_value));
}
//__debugbreak();
shared->dead = false;
pyrowave_sync_object_destroy(sync);
for (auto &i : img)
pyrowave_image_destroy(i);
pyrowave_encoder_destroy(encoder);
pyrowave_device_destroy(device);
}
#endif
int main(int argc, char **argv)
{
#ifdef _WIN32
if (getenv("VALIDATE"))
{
ComPtr<ID3D12Debug> debug;
if (SUCCEEDED(D3D12GetDebugInterface(IID_ID3D12Debug, debug.ppv())))
debug->EnableDebugLayer();
}
if (argc == 2 && strcmp(argv[1], "--child") == 0)
{
test_child_interop();
return EXIT_SUCCESS;
}
test_d3d11_cross_process_encode();
printf("Running NV12 interop test ...\n");
test_nv12_interop();
printf("Running D3D11 interop test ...\n");
test_d3d11_interop();
printf("Running D3D12 interop test ...\n");
test_d3d12_interop();
#else
(void)argc;
(void)argv;
#endif
printf("Running Vulkan <-> Vulkan interop test with direct device share ...\n");
test_direct_interop();
printf("Running opaque Vulkan <-> Vulkan interop test ...\n");
test_opaque_interop(false);
#ifdef _WIN32
printf("Running opaque Vulkan <-> Vulkan interop test (KMT handles) ...\n");
test_opaque_interop(true);
#endif
printf("Running drm modifier Vulkan <-> Vulkan interop test ...\n");
test_drm_modifier_interop();
printf("Interop tests passed!\n");
}