// 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");
}
