Files
punktfunk/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/wsi_dxgi.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

520 lines
16 KiB
C++

/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wsi_dxgi.hpp"
#include <windows.h>
namespace Vulkan
{
DXGIInteropSwapchain::~DXGIInteropSwapchain()
{
// Wait-for-idle before teardown.
if (fence)
fence->SetEventOnCompletion(fence_value, nullptr);
if (latency_handle)
CloseHandle(latency_handle);
}
static bool is_running_on_wine()
{
// If we're running in Wine for whatever reason, interop like this is completely useless.
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
return !ntdll || GetProcAddress(ntdll, "wine_get_version");
}
static bool is_running_in_tool(Device &device)
{
auto &ext = device.get_device_features();
if (ext.supports_tooling_info && vkGetPhysicalDeviceToolPropertiesEXT)
{
auto gpu = device.get_physical_device();
uint32_t count = 0;
vkGetPhysicalDeviceToolPropertiesEXT(gpu, &count, nullptr);
Util::SmallVector<VkPhysicalDeviceToolPropertiesEXT> tool_props(count);
for (auto &t : tool_props)
t = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TOOL_PROPERTIES_EXT };
vkGetPhysicalDeviceToolPropertiesEXT(gpu, &count, tool_props.data());
// It's okay for validation to not force this path. We're mostly concerned with RenderDoc, RGP and Nsight.
for (auto &t : tool_props)
if (t.purposes & (VK_TOOL_PURPOSE_PROFILING_BIT | VK_TOOL_PURPOSE_TRACING_BIT))
return true;
}
return false;
}
bool DXGIInteropSwapchain::init_interop_device(Device &vk_device_)
{
vk_device = &vk_device_;
// If we're running in Wine for whatever reason, interop like this is more harmful than good.
if (is_running_on_wine())
return false;
// If we're running in some capture tool, we need to use Vulkan WSI to avoid confusing it.
if (is_running_in_tool(*vk_device))
return false;
if (!vk_device->get_device_features().vk11_props.deviceLUIDValid)
return false;
d3d12_lib = Util::DynamicLibrary("d3d12.dll");
dxgi_lib = Util::DynamicLibrary("dxgi.dll");
if (!d3d12_lib)
{
LOGE("Failed to find d3d12.dll. Ignoring interop device.\n");
return false;
}
if (!dxgi_lib)
{
LOGE("Failed to find dxgi.dll. Ignoring interop device.\n");
return false;
}
auto pfn_CreateDXGIFactory1 =
dxgi_lib.get_symbol<decltype(&CreateDXGIFactory1)>("CreateDXGIFactory1");
auto pfn_D3D12CreateDevice =
d3d12_lib.get_symbol<decltype(&D3D12CreateDevice)>("D3D12CreateDevice");
if (!pfn_CreateDXGIFactory1 || !pfn_D3D12CreateDevice)
{
LOGE("Failed to find entry points.\n");
return false;
}
HRESULT hr;
if (FAILED(hr = pfn_CreateDXGIFactory1(IID_PPV_ARGS(&dxgi_factory))))
{
LOGE("Failed to create DXGI factory, hr #%x.\n", unsigned(hr));
return false;
}
LUID luid = {};
ComPtr<IDXGIAdapter> adapter;
memcpy(&luid, vk_device->get_device_features().vk11_props.deviceLUID, VK_LUID_SIZE);
if (FAILED(hr = dxgi_factory->EnumAdapterByLuid(luid, IID_PPV_ARGS(&adapter))))
{
LOGE("Failed to enumerate DXGI adapter by LUID.\n");
return false;
}
if (FAILED(hr = pfn_D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device))))
{
LOGE("Failed to create D3D12Device, hr #%x.\n", unsigned(hr));
return false;
}
D3D12_COMMAND_QUEUE_DESC queue_desc = {};
queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
if (FAILED(hr = device->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&queue))))
{
LOGE("Failed to create command queue, hr #%x.\n", unsigned(hr));
return false;
}
if (FAILED(hr = device->CreateCommandList1(
0, D3D12_COMMAND_LIST_TYPE_DIRECT, D3D12_COMMAND_LIST_FLAG_NONE, IID_PPV_ARGS(&list))))
{
LOGE("Failed to create command list, hr #%x.\n", unsigned(hr));
return false;
}
if (FAILED(hr = device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&fence))))
{
LOGE("Failed to create shared fence, hr #%x.\n", unsigned(hr));
return false;
}
// Import D3D12 timeline into Vulkan.
// Other way around is not as well-supported.
vk_fence = vk_device->request_semaphore_external(
VK_SEMAPHORE_TYPE_TIMELINE, VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT);
if (!vk_fence)
{
LOGE("Failed to create timeline.\n");
return EXIT_FAILURE;
}
ExternalHandle fence_handle;
fence_handle.semaphore_handle_type = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT;
if (FAILED(device->CreateSharedHandle(fence.Get(), nullptr,
GENERIC_ALL, nullptr, &fence_handle.handle)))
{
LOGE("Failed to create shared fence handle.\n");
return EXIT_FAILURE;
}
if (!vk_fence->import_from_handle(fence_handle))
{
LOGE("Failed to import timeline.\n");
CloseHandle(fence_handle.handle);
return false;
}
return true;
}
VkImage DXGIInteropSwapchain::get_vulkan_image() const
{
return vulkan_backbuffer->get_image();
}
static DXGI_FORMAT convert_vk_format(VkFormat fmt)
{
switch (fmt)
{
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
// D3D12 fails to create SRGB swapchain for some reason.
// We'll import the memory as sRGB however, and it works fine ...
return DXGI_FORMAT_R8G8B8A8_UNORM;
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
return DXGI_FORMAT_B8G8R8A8_UNORM;
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
return DXGI_FORMAT_R10G10B10A2_UNORM;
case VK_FORMAT_R16G16B16A16_SFLOAT:
return DXGI_FORMAT_R16G16B16A16_FLOAT;
default:
return DXGI_FORMAT_UNKNOWN;
}
}
static DXGI_COLOR_SPACE_TYPE convert_vk_color_space(VkColorSpaceKHR colspace)
{
switch (colspace)
{
case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT:
return DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709;
case VK_COLOR_SPACE_HDR10_ST2084_EXT:
return DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR:
return DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
default:
return DXGI_COLOR_SPACE_RESERVED;
}
}
void DXGIInteropSwapchain::reset_backbuffer_state()
{
for (auto &buf : backbuffers)
if (fence)
fence->SetEventOnCompletion(buf.wait_fence_value, nullptr);
backbuffers.clear();
}
bool DXGIInteropSwapchain::setup_per_frame_state(PerFrameState &state, unsigned index)
{
HRESULT hr;
if (FAILED(hr = swapchain->GetBuffer(index, IID_PPV_ARGS(&state.backbuffer))))
{
LOGE("Failed to get backbuffer, hr #%x.\n", unsigned(hr));
return false;
}
if (FAILED(hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(&state.allocator))))
{
LOGE("Failed to create command allocator, hr #%x.\n", unsigned(hr));
return false;
}
return true;
}
bool DXGIInteropSwapchain::init_swapchain(HWND hwnd_, VkSurfaceFormatKHR format,
unsigned width, unsigned height, unsigned count)
{
if (hwnd && hwnd_ != hwnd)
{
reset_backbuffer_state();
swapchain.Reset();
}
hwnd = hwnd_;
DXGI_SWAP_CHAIN_DESC1 desc = {};
desc.Width = width;
desc.Height = height;
desc.BufferCount = count;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
desc.SampleDesc.Count = 1;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.Format = convert_vk_format(format.format);
if (!desc.Format)
return false;
auto color_space = convert_vk_color_space(format.colorSpace);
if (color_space == DXGI_COLOR_SPACE_RESERVED)
return false;
BOOL allow_tear = FALSE;
if (SUCCEEDED(dxgi_factory->CheckFeatureSupport(
DXGI_FEATURE_PRESENT_ALLOW_TEARING,
&allow_tear, sizeof(allow_tear)) && allow_tear))
{
desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;
allow_tearing = true;
}
desc.Flags |= DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
ComPtr<IDXGISwapChain1> swap;
HRESULT hr;
reset_backbuffer_state();
// If we already have a swapchain we can just use ResizeBuffers.
if (!swapchain)
{
if (FAILED(hr = dxgi_factory->CreateSwapChainForHwnd(
queue.Get(), hwnd, &desc, nullptr, nullptr, &swap)))
{
LOGE("Failed to create swapchain, hr #%x.\n", unsigned(hr));
return false;
}
completed_presents = 0;
completed_waits = 0;
if (FAILED(swap.As(&swapchain)))
{
LOGE("Failed to query swapchain interface.\n");
return false;
}
if (latency_handle)
CloseHandle(latency_handle);
latency_handle = swapchain->GetFrameLatencyWaitableObject();
if (!latency_handle)
{
LOGE("Failed to query latency handle.\n");
return false;
}
// Drop semaphore to 0 right away to make code less awkward later.
if (WaitForSingleObject(latency_handle, INFINITE) != WAIT_OBJECT_0)
{
LOGE("Failed to wait for latency object.\n");
return false;
}
}
else
{
if (FAILED(hr = swapchain->ResizeBuffers(count, width, height, desc.Format, desc.Flags)))
{
LOGE("Failed to resize buffers, hr #%x.\n", unsigned(hr));
return false;
}
}
if (FAILED(dxgi_factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES)))
{
LOGE("Failed to make window association.\n");
return false;
}
surface_format = format;
UINT space_support = 0;
if (FAILED(swapchain->CheckColorSpaceSupport(color_space, &space_support)) ||
((space_support & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) == 0))
{
// Fallback to SDR if HDR doesn't pass check.
if (FAILED(swapchain->CheckColorSpaceSupport(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, &space_support)) ||
((space_support & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) == 0))
{
return false;
}
LOGW("HDR10 not supported by DXGI swapchain, falling back to SDR.\n");
surface_format.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
color_space = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
}
if (FAILED(swapchain->SetColorSpace1(color_space)))
{
LOGE("Failed to set color space.\n");
return false;
}
backbuffers.resize(desc.BufferCount);
for (unsigned i = 0; i < desc.BufferCount; i++)
if (!setup_per_frame_state(backbuffers[i], i))
return false;
ExternalHandle imported_image;
imported_image.memory_handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT;
D3D12_RESOURCE_DESC blit_desc = {};
blit_desc.Width = width;
blit_desc.Height = height;
blit_desc.Format = desc.Format;
blit_desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
blit_desc.SampleDesc.Count = 1;
blit_desc.DepthOrArraySize = 1;
blit_desc.MipLevels = 1;
blit_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
blit_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
D3D12_HEAP_PROPERTIES heap_props = {};
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
blit_backbuffer.Reset();
if (FAILED(hr = device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_SHARED, &blit_desc,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&blit_backbuffer))))
{
LOGE("Failed to create blit render target, hr #%x.\n", unsigned(hr));
return false;
}
if (FAILED(hr = device->CreateSharedHandle(blit_backbuffer.Get(), nullptr, GENERIC_ALL, nullptr,
&imported_image.handle)))
{
LOGE("Failed to create shared handle, hr #%x.\n", unsigned(hr));
return false;
}
auto image_info = ImageCreateInfo::render_target(width, height, format.format);
image_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
image_info.misc = IMAGE_MISC_EXTERNAL_MEMORY_BIT;
image_info.external = imported_image;
vulkan_backbuffer = vk_device->create_image(image_info);
if (!vulkan_backbuffer)
{
LOGE("Failed to create shared Vulkan image, hr #%x.\n", unsigned(hr));
return false;
}
vulkan_backbuffer->set_swapchain_layout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
return true;
}
VkSurfaceFormatKHR DXGIInteropSwapchain::get_current_surface_format() const
{
return surface_format;
}
bool DXGIInteropSwapchain::wait_latency(unsigned latency_frames)
{
uint64_t target_wait_count = completed_presents - latency_frames;
if (latency_handle && (target_wait_count & (1ull << 63)) == 0)
{
while (completed_waits < target_wait_count)
{
if (WaitForSingleObject(latency_handle, INFINITE) != WAIT_OBJECT_0)
{
LOGE("Failed to wait for latency object.\n");
return false;
}
completed_waits++;
}
}
return true;
}
bool DXGIInteropSwapchain::acquire(Semaphore &acquire_semaphore)
{
// AMD workaround. Driver freaks out if trying to wait for D3D12 timeline value of 0.
queue->Signal(fence.Get(), ++fence_value);
acquire_semaphore = vk_device->request_timeline_semaphore_as_binary(*vk_fence, fence_value);
return true;
}
bool DXGIInteropSwapchain::present(Vulkan::Semaphore release_semaphore, bool vsync)
{
unsigned index = swapchain->GetCurrentBackBufferIndex();
auto &per_frame = backbuffers[index];
vk_device->add_wait_semaphore(CommandBuffer::Type::Generic, std::move(release_semaphore),
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, true);
auto cmd = vk_device->request_command_buffer();
cmd->release_image_barrier(*vulkan_backbuffer,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_GENERAL,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0);
vk_device->submit(cmd);
auto timeline_signal = vk_device->request_timeline_semaphore_as_binary(*vk_fence, ++fence_value);
vk_device->submit_empty(CommandBuffer::Type::Generic, nullptr, timeline_signal.get());
queue->Wait(fence.Get(), fence_value);
fence->SetEventOnCompletion(per_frame.wait_fence_value, nullptr);
if (FAILED(per_frame.allocator->Reset()))
{
LOGE("Failed to reset command allocator.\n");
return false;
}
list->Reset(per_frame.allocator.Get(), nullptr);
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Transition.pResource = per_frame.backbuffer.Get();
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST;
list->ResourceBarrier(1, &barrier);
D3D12_TEXTURE_COPY_LOCATION dst = {}, src = {};
dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
src.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
dst.pResource = per_frame.backbuffer.Get();
src.pResource = blit_backbuffer.Get();
list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
list->ResourceBarrier(1, &barrier);
if (FAILED(list->Close()))
{
LOGE("Failed to close command list.\n");
return false;
}
ID3D12CommandList *cmdlist = list.Get();
queue->ExecuteCommandLists(1, &cmdlist);
queue->Signal(fence.Get(), ++fence_value);
per_frame.wait_fence_value = fence_value;
HRESULT hr = swapchain->Present(vsync ? 1 : 0, !vsync && allow_tearing ? DXGI_PRESENT_ALLOW_TEARING : 0);
if (FAILED(hr))
{
LOGE("Failed to present, hr #%x.\n", unsigned(hr));
return false;
}
completed_presents++;
return true;
}
}