Submit the overlay through a persistent Vulkan texture

This commit is contained in:
baketnk committed 2026-09-24 14:12:46 -04:00
1 parent 8417212ed2
commit 4c043c9962
13 files changed
+750 -26

No files matched your search

+12 -2
View File
@@ -56,6 +56,7 @@ if(FRAMEYAP_NATIVE)
message(FATAL_ERROR "Native POC supports Linux only")
endif()
find_package(PkgConfig REQUIRED)
find_package(Vulkan REQUIRED)
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3>=3.2)
# SDL's .pc can inject its producer-prefix RPATH. Let CMake manage build
# RPATH and install only our relative path, never a developer environment.
@@ -83,10 +84,10 @@ if(FRAMEYAP_NATIVE)
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method.c"
COMMAND "${WAYLAND_SCANNER}" private-code "${PROTOCOL}" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method.c"
DEPENDS "${PROTOCOL}" VERBATIM)
target_sources(frameyap PRIVATE src/runtime.cpp src/overlay.cpp src/audio.cpp src/text_input.cpp
target_sources(frameyap PRIVATE src/runtime.cpp src/overlay.cpp src/overlay_texture.cpp src/audio.cpp src/text_input.cpp
"${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method.c" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-client.h")
target_include_directories(frameyap PRIVATE "${OPENVR_INCLUDE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(frameyap PRIVATE frameyap_worker frameyap_mount frameyap_panel PkgConfig::SDL3 PkgConfig::WAYLAND "${OPENVR_LIBRARY}")
target_link_libraries(frameyap PRIVATE frameyap_worker frameyap_mount frameyap_panel PkgConfig::SDL3 PkgConfig::WAYLAND Vulkan::Vulkan "${OPENVR_LIBRARY}")
target_compile_definitions(frameyap PRIVATE FRAMEYAP_NATIVE=1)
set_target_properties(frameyap PROPERTIES INSTALL_RPATH "$ORIGIN/../lib")
endif()
@@ -134,6 +135,15 @@ if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
add_test(NAME frameyap.gestures COMMAND frameyap_gestures_test)
find_package(Python3 3.10 COMPONENTS Interpreter)
if(FRAMEYAP_NATIVE)
add_executable(frameyap_overlay_texture_test tests/overlay_texture_test.cpp src/overlay_texture.cpp)
target_include_directories(frameyap_overlay_texture_test PRIVATE src "${OPENVR_INCLUDE_DIR}" "${Vulkan_INCLUDE_DIRS}")
target_compile_options(frameyap_overlay_texture_test PRIVATE -UNDEBUG)
# Test-provided Vulkan symbols; no GPU, loader or OpenVR initialization.
add_test(NAME frameyap.overlay_texture COMMAND frameyap_overlay_texture_test)
add_executable(frameyap_texture_check EXCLUDE_FROM_ALL tests/overlay_texture_gpu_check.cpp src/overlay_texture.cpp)
target_include_directories(frameyap_texture_check PRIVATE src "${OPENVR_INCLUDE_DIR}")
target_link_libraries(frameyap_texture_check PRIVATE Vulkan::Vulkan)
# Explicit build and --run only; never register a hardware check with CTest.
pkg_check_modules(WAYLAND_SERVER IMPORTED_TARGET wayland-server)
if(WAYLAND_SERVER_FOUND)
find_package(Threads REQUIRED)
+4 -3
View File
@@ -89,9 +89,10 @@ panel. A small overlay-specific RAII owner in this repository should manage Open
handles, input manifest and shutdown. No dependency on external application
libraries, assets, build trees or Python environments.
Start with one small RGBA panel updated only on UI changes and a bounded recording
indicator cadence. `SetOverlayRaw` is the simplest proof route; measure upload
cost before selecting a persistent Vulkan `SetOverlayTexture` path. No stereo
Use one small RGBA panel updated only on UI changes and a bounded recording
indicator cadence. The native implementation uploads the CPU-rasterized panel to
a persistent Vulkan image and uses `SetOverlayTexture`; the initial
`SetOverlayRaw` proof path has been replaced. No stereo
eye targets or per-eye scene rendering. Keep tracking in compositor transforms,
not an application-rendered hand-pose animation loop. Do not promise a particular
GPU cost until measured.
+39 -9
View File
@@ -9,7 +9,8 @@ normal build or test.
## Rendering and controls
Explicit development dependencies: Valve OpenVR SDK v2.15.6 and FreeType 2.
Explicit development dependencies: Valve OpenVR SDK v2.15.6, Vulkan headers/loader
and FreeType 2. The native runtime needs a compatible system Vulkan driver.
Configure/build must not fetch them. The default font is the bundled Inconsolata
Regular, also used by kouseki; its OFL and extraction provenance are included in
[third-party notes](third-party.md). `--font FILE` overrides the JSON selection.
@@ -18,19 +19,35 @@ Sans face if present. Glyph coverage depends on the selected face; full CJK
coverage is not claimed.
`src/panel_surface.*` renders **one 1000×680 RGBA canvas** for review, settings,
status and controls. One OpenVR handle receives it with `SetOverlayRaw`; tabs do
not create extra overlays or render targets. The rounded mint-to-blue perimeter,
status and controls. `src/overlay_texture.*` uploads this CPU canvas into one
persistent Vulkan RGBA8 image and submits it with `SetOverlayTexture`. The image,
staging allocation and command buffer are reused; tabs do not create extra
overlays or render targets. The rounded mint-to-blue perimeter,
shallow curved accent, and dark cards borrow kouseki's VR visual language. Rounded
preview, status and control surfaces use independently rasterized antialiased edges
and restrained baked neon halos rather than GPU bloom. The recording indicator and
selected controls remain distinguishable by their labels, not color alone. Rounded
control hit areas exclude their clipped corners.
Rendering/uploads occur only for changed content, page or settings; laser hover
and button down/up are hit-tested without a raw-texture upload, to reduce
compositor flicker reported during hover. Static frames are reused.
and button down/up are hit-tested without an upload. Static frames are reused.
The caller may call `draw(Panel)` at 10 ms intervals. Tracking transforms do
not require repainting the canvas.
The Vulkan instance/device enable the extensions requested by the running
SteamVR runtime and use its selected physical device and a graphics queue.
There is no desktop window, swapchain, SDL video dependency or raw-upload
fallback. Updates wait for the dedicated queue's previous upload and OpenVR
transfer before reusing staging memory. Image barriers finish in
`TRANSFER_SRC_OPTIMAL`, as required by
[OpenVR's Vulkan contract](https://github.com/ValveSoftware/openvr/wiki/Vulkan).
The queue is used on the overlay thread; GPU resources outlive `VR_Shutdown`.
The device selection, texture description and persistent panel-upload patterns
were compared with kouseki's `openvr_session.cpp` and `vulkan_renderer.cpp` at
`738569f4c41ff4c8fc9edd5bfff9c861957ea39e`; FrameYap owns this implementation.
GPU setup/submission errors stop startup or the run with an explicit error.
This replaces the raw-upload rendering path; headset flicker acceptance still
requires an on-device comparison.
The complete transcript preview is paginated by glyph width and four-line
height; Previous and Next navigate it without changing the source transcript.
Long status/detail messages show a prefix with a visible truncation marker.
@@ -144,10 +161,23 @@ recentring and readability still require a separately authorized headset check.
The opt-in native `--check-controls` probe logs pointer counters and action
callbacks to the terminal rather than repainting them on the panel. Its canvas
stays static for Record/Cancel/Insert/Enter clicks so those clicks can be checked
without diagnostic `SetOverlayRaw` traffic. Switching tabs or mount still updates
the visible panel. This isolates click-induced compositor flicker from full raw
texture replacements; it does not establish that ordinary state-changing UI
updates are flicker-free.
without diagnostic texture uploads. Switching tabs or mount still updates
the visible panel. Diagnostics identify `renderer=Vulkan` and count
`textureUploads`; raw/file `ImageLoaded` events are not GPU upload completions.
The native CTest suite tests persistent image reuse, queued transfer ordering,
coherent/noncoherent staging memory and error cleanup against Vulkan fakes;
it does not initialize the Vulkan loader, a GPU or OpenVR for that test.
An optional offscreen check exercises the real Vulkan backend with eight
synthetic RGBA patterns, verifying exact readback and image reuse. It needs a
GPU/driver, is excluded from normal builds and CTest, and requires `--run`:
```sh
cmake --build build-native --target frameyap_texture_check
./build-native/frameyap_texture_check --run
```
It does not initialize OpenVR or establish compositor/headset acceptance.
`assets/actions.json` names six actions: left/right grip, PTT, cancel, insert,
Enter. `bindings_frame_controller.json` maps right X click to hold-to-talk PTT;
+3 -2
View File
@@ -25,8 +25,9 @@ For the current **external-runtime** POC, `scripts/stage-native-poc.py --help`
documents explicit inputs. It invokes `cmake --install` on an existing native build,
copies SDL/OpenVR and an explicitly licensed font, and retains notices. It does
not build, download, run the app, or copy a proprietary ASR runtime. The native
POC relies on Frame's system Wayland, FreeType, libstdc++ and glibc; audit `ldd`
on the installed binary. SDL/OpenVR resolve inside its own `lib/`, not a producer
POC relies on Frame's system Vulkan loader/driver, Wayland, FreeType, libstdc++
and glibc; audit `ldd` on the installed binary.
SDL/OpenVR resolve inside its own `lib/`, not a producer
prefix. ARM64/glibc packaging is not a claim of compatibility with arbitrary Linux.
```sh
+1
View File
@@ -71,6 +71,7 @@ separately scoped and independently licensed implementation—not a silent model
## Developer native build
Requirements: Linux, CMake/C++20, SDL3 >=3.2, Wayland client + scanner, FreeType,
Vulkan headers/loader (plus the system GPU driver at runtime),
and a deliberately provisioned standalone OpenVR **v2.15.6** SDK. No CMake fetches.
```sh
+2 -2
View File
@@ -2,7 +2,7 @@
"""Stage a native-only POC from an explicit native build and licensed files.
No downloads, compiler invocation, proprietary ASR runtime, registration or launch.
System Wayland/FreeType/libstdc++/glibc remain platform prerequisites.
System Vulkan loader/driver, Wayland/FreeType/libstdc++/glibc remain platform prerequisites.
"""
import argparse
from pathlib import Path
@@ -41,7 +41,7 @@ def main():
shutil.copyfile(args.sdl_library, dest / "lib/libSDL3.so.0", follow_symlinks=True)
shutil.copyfile(args.font, dest / "fonts/font.ttf")
notices = ["FrameYap native-only POC. No ASR runtime or model is included.\n",
"Original FrameYap code: MIT. System Wayland/FreeType/libstdc++/glibc are not bundled.\n",
"Original FrameYap code: MIT. System Vulkan/Wayland/FreeType/libstdc++/glibc are not bundled.\n",
"Bundled libraries: Valve OpenVR and unmodified SDL3; font license included below.\n",
"This package does not grant any rights to kestrel-kernels or provide a functioning ASR environment.\n"]
for label, file in (("FrameYap", root / "LICENSE"), ("OpenVR", args.openvr_license),
+1 -1
View File
@@ -122,7 +122,7 @@ int main(int argc, char** argv) {
auto pointer = overlay.pointer_status();
if (pointer != pointer_status) { pointer_status = pointer; std::cout << pointer_status << std::endl; }
// Keep the canvas fixed for action clicks: otherwise the
// changing counters cause SetOverlayRaw on every down/up.
// changing counters would upload a texture on every down/up.
// Tab/placement changes still redraw the correct controls.
overlay.draw(check_panel);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
+40 -5
View File
@@ -1,4 +1,5 @@
#include "overlay.hpp"
#include "overlay_texture.hpp"
#include "gestures.hpp"
#include "laser_setting.hpp"
#include "panel_surface.hpp"
@@ -11,6 +12,7 @@
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
@@ -41,6 +43,19 @@ std::filesystem::path absolute_file(const std::filesystem::path& p) {
if (!std::filesystem::is_regular_file(result)) throw std::runtime_error("Missing file: " + result.string());
return result;
}
template<class Query> std::vector<std::string> vulkan_extensions(Query query) {
const auto size = query(nullptr, 0);
if (!size) return {};
if (size > 65536) throw std::runtime_error("OpenVR Vulkan extension list is too large");
std::vector<char> buffer(size, '\0');
const auto written = query(buffer.data(), size);
if (!written || written > size || buffer[written - 1] != '\0')
throw std::runtime_error("OpenVR Vulkan extension list changed or is invalid");
std::istringstream words(std::string(buffer.data(), written - 1));
std::vector<std::string> result;
for (std::string name; words >> name;) result.push_back(std::move(name));
return result;
}
} // namespace
struct Overlay::Impl {
@@ -48,6 +63,7 @@ struct Overlay::Impl {
vr::IVROverlay* overlay = nullptr;
vr::IVRInput* input = nullptr;
vr::VROverlayHandle_t handle = vr::k_ulOverlayHandleInvalid;
std::unique_ptr<OverlayTexture> gpu_texture;
vr::VRActionSetHandle_t action_set = vr::k_ulInvalidActionSetHandle;
std::array<vr::VRActionHandle_t, 6> actions{};
std::filesystem::path settings_path;
@@ -71,7 +87,7 @@ struct Overlay::Impl {
bool persist_mount = true;
vr::EVRInputError action_update_error = vr::VRInputError_None;
unsigned pointer_downs = 0, pointer_ups = 0, pointer_actions = 0, pointer_resets = 0;
unsigned raw_uploads = 0, show_calls = 0, hide_calls = 0;
unsigned texture_uploads = 0, show_calls = 0, hide_calls = 0;
unsigned overlay_shown_events = 0, overlay_hidden_events = 0, image_loaded_events = 0, image_failed_events = 0;
unsigned overlay_focus_events = 0, global_focus_events = 0, input_focus_captured_events = 0;
std::string last_pointer_event = "none";
@@ -95,6 +111,21 @@ struct Overlay::Impl {
overlay = vr::VROverlay();
input = vr::VRInput();
if (!overlay || !input) throw std::runtime_error("OpenVR overlay/input interface unavailable");
auto* compositor = vr::VRCompositor();
if (!compositor) throw std::runtime_error("OpenVR Vulkan compositor interface unavailable");
const auto extensions = vulkan_extensions([&](char* out, uint32_t size) {
return compositor->GetVulkanInstanceExtensionsRequired(out, size);
});
gpu_texture = std::make_unique<OverlayTexture>(W, H, extensions,
[&](VkInstance instance) {
uint64_t physical = 0;
system->GetOutputDevice(&physical, vr::TextureType_Vulkan, instance);
return reinterpret_cast<VkPhysicalDevice>(physical);
}, [&](VkPhysicalDevice physical) {
return vulkan_extensions([&](char* out, uint32_t size) {
return compositor->GetVulkanDeviceExtensionsRequired(physical, out, size);
});
});
// The manifest launches a shell launcher, then execs this binary.
// Associate manually launched instances with our registered app key too.
auto* applications = vr::VRApplications();
@@ -136,6 +167,9 @@ struct Overlay::Impl {
handle = vr::k_ulOverlayHandleInvalid;
}
if (system) { vr::VR_Shutdown(); system = nullptr; overlay = nullptr; input = nullptr; }
// OpenVR retains client-side resources for submitted Vulkan images.
// Its shutdown must finish before their device/instance are destroyed.
gpu_texture.reset();
}
void visibility() {
const bool wanted = placed && has_texture;
@@ -198,9 +232,10 @@ struct Overlay::Impl {
void draw(const Panel& p) {
panel = p;
if (surface.render(p)) {
// OpenVR's API takes void*, but does not modify the submitted RGBA bytes.
overlay_check(overlay->SetOverlayRaw(handle, const_cast<unsigned char*>(surface.pixels().data()), W, H, 4), overlay, "SetOverlayRaw");
++raw_uploads;
gpu_texture->upload(surface.pixels());
auto texture = gpu_texture->texture();
overlay_check(overlay->SetOverlayTexture(handle, &texture), overlay, "SetOverlayTexture (Vulkan)");
++texture_uploads;
has_texture = true;
}
visibility();
@@ -399,7 +434,7 @@ std::string Overlay::pointer_status() const {
return "Pointer down=" + std::to_string(impl_->pointer_downs) + " up=" + std::to_string(impl_->pointer_ups) +
" hits=" + std::to_string(impl_->pointer_actions) + " resets=" + std::to_string(impl_->pointer_resets) +
" last=" + impl_->last_pointer_event +
"\nOverlay raw=" + std::to_string(impl_->raw_uploads) +
"\nOverlay renderer=Vulkan textureUploads=" + std::to_string(impl_->texture_uploads) +
" showCalls=" + std::to_string(impl_->show_calls) + " hideCalls=" + std::to_string(impl_->hide_calls) +
" shownEvents=" + std::to_string(impl_->overlay_shown_events) +
" hiddenEvents=" + std::to_string(impl_->overlay_hidden_events) +
+240
View File
@@ -0,0 +1,240 @@
#include "overlay_texture.hpp"
#include <bit>
#include <cstring>
#include <limits>
#include <stdexcept>
namespace frameyap {
namespace {
void check(VkResult result, const char* operation) {
if (result != VK_SUCCESS)
throw std::runtime_error(std::string("Overlay Vulkan ") + operation + ": VkResult=" +
std::to_string(result));
}
std::vector<const char*> names(std::span<const std::string> extensions) {
std::vector<const char*> result;
for (const auto& extension : extensions) result.push_back(extension.c_str());
return result;
}
constexpr VkFormat format = VK_FORMAT_R8G8B8A8_UNORM;
constexpr VkImageSubresourceRange color_range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
} // namespace
struct OverlayTexture::Impl {
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice physical = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory image_memory = VK_NULL_HANDLE, staging_memory = VK_NULL_HANDLE;
VkBuffer staging = VK_NULL_HANDLE;
VkCommandPool pool = VK_NULL_HANDLE;
VkCommandBuffer commands = VK_NULL_HANDLE;
VkPhysicalDeviceMemoryProperties memory{};
vr::VRVulkanTextureData_t description{};
void* mapped = nullptr;
size_t bytes = 0;
uint32_t width = 0, height = 0;
bool coherent = false, uploaded = false;
~Impl() {
// OpenVR must already have released its client-side Vulkan resources.
// On device loss, waiting can fail; owned handles still need destruction.
if (device) vkDeviceWaitIdle(device);
if (mapped) vkUnmapMemory(device, staging_memory);
if (pool) vkDestroyCommandPool(device, pool, nullptr);
if (staging) vkDestroyBuffer(device, staging, nullptr);
if (staging_memory) vkFreeMemory(device, staging_memory, nullptr);
if (image) vkDestroyImage(device, image, nullptr);
if (image_memory) vkFreeMemory(device, image_memory, nullptr);
if (device) vkDestroyDevice(device, nullptr);
if (instance) vkDestroyInstance(instance, nullptr);
}
uint32_t memory_type(uint32_t bits, VkMemoryPropertyFlags required,
VkMemoryPropertyFlags preferred) const {
for (auto flags : {required | preferred, required})
for (uint32_t i = 0; i < memory.memoryTypeCount; ++i)
if ((bits & (1u << i)) && (memory.memoryTypes[i].propertyFlags & flags) == flags)
return i;
throw std::runtime_error("Overlay Vulkan: no compatible memory type");
}
void init(uint32_t w, uint32_t h, std::span<const std::string> instance_extensions,
const SelectDevice& select_device, const DeviceExtensions& device_extensions) {
if (!w || !h || uint64_t(w) * h > std::numeric_limits<size_t>::max() / 4)
throw std::runtime_error("Overlay Vulkan: invalid RGBA dimensions");
width = w; height = h; bytes = size_t(w) * h * 4;
VkApplicationInfo app{};
app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app.pApplicationName = "FrameYap";
app.apiVersion = VK_API_VERSION_1_0;
auto instance_names = names(instance_extensions);
VkInstanceCreateInfo instance_info{};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.pApplicationInfo = &app;
instance_info.enabledExtensionCount = uint32_t(instance_names.size());
instance_info.ppEnabledExtensionNames = instance_names.data();
check(vkCreateInstance(&instance_info, nullptr, &instance), "vkCreateInstance (OpenVR extensions)");
physical = select_device(instance);
if (!physical) throw std::runtime_error("Overlay Vulkan: SteamVR did not select a physical device");
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(physical, &properties);
if (w > properties.limits.maxImageDimension2D || h > properties.limits.maxImageDimension2D)
throw std::runtime_error("Overlay Vulkan: panel exceeds device image dimensions");
uint32_t family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physical, &family_count, nullptr);
std::vector<VkQueueFamilyProperties> families(family_count);
vkGetPhysicalDeviceQueueFamilyProperties(physical, &family_count, families.data());
uint32_t family = 0;
while (family < family_count &&
(!(families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) || !families[family].queueCount)) ++family;
if (family == family_count) throw std::runtime_error("Overlay Vulkan: no graphics queue");
const float priority = 1.0f;
VkDeviceQueueCreateInfo queue_info{};
queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info.queueFamilyIndex = family;
queue_info.queueCount = 1;
queue_info.pQueuePriorities = &priority;
const auto extensions = device_extensions(physical);
auto device_names = names(extensions);
VkDeviceCreateInfo device_info{};
device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
device_info.queueCreateInfoCount = 1;
device_info.pQueueCreateInfos = &queue_info;
device_info.enabledExtensionCount = uint32_t(device_names.size());
device_info.ppEnabledExtensionNames = device_names.data();
check(vkCreateDevice(physical, &device_info, nullptr, &device), "vkCreateDevice (OpenVR extensions)");
vkGetDeviceQueue(device, family, 0, &queue);
vkGetPhysicalDeviceMemoryProperties(physical, &memory);
VkImageCreateInfo image_info{};
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.format = format;
image_info.extent = {width, height, 1};
image_info.mipLevels = 1;
image_info.arrayLayers = 1;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
check(vkCreateImage(device, &image_info, nullptr, &image), "vkCreateImage");
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(device, image, &requirements);
VkMemoryAllocateInfo allocation{};
allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0);
check(vkAllocateMemory(device, &allocation, nullptr, &image_memory), "vkAllocateMemory (image)");
check(vkBindImageMemory(device, image, image_memory, 0), "vkBindImageMemory");
VkBufferCreateInfo buffer_info{};
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buffer_info.size = bytes;
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateBuffer(device, &buffer_info, nullptr, &staging), "vkCreateBuffer");
vkGetBufferMemoryRequirements(device, staging, &requirements);
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
coherent = memory.memoryTypes[allocation.memoryTypeIndex].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
check(vkAllocateMemory(device, &allocation, nullptr, &staging_memory), "vkAllocateMemory (staging)");
check(vkBindBufferMemory(device, staging, staging_memory, 0), "vkBindBufferMemory");
// Map the allocation, including any padding, so whole-allocation flushes
// satisfy nonCoherentAtomSize even when the last pixel is unaligned.
check(vkMapMemory(device, staging_memory, 0, VK_WHOLE_SIZE, 0, &mapped), "vkMapMemory");
VkCommandPoolCreateInfo pool_info{};
pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
pool_info.queueFamilyIndex = family;
pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
check(vkCreateCommandPool(device, &pool_info, nullptr, &pool), "vkCreateCommandPool");
VkCommandBufferAllocateInfo command_info{};
command_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
command_info.commandPool = pool;
command_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
command_info.commandBufferCount = 1;
check(vkAllocateCommandBuffers(device, &command_info, &commands), "vkAllocateCommandBuffers");
description.m_nImage = std::bit_cast<uint64_t>(image);
description.m_pDevice = device;
description.m_pPhysicalDevice = physical;
description.m_pInstance = instance;
description.m_pQueue = queue;
description.m_nQueueFamilyIndex = family;
description.m_nWidth = width;
description.m_nHeight = height;
description.m_nFormat = format;
description.m_nSampleCount = 1;
}
void upload(std::span<const unsigned char> rgba) {
if (rgba.size() != bytes) throw std::runtime_error("Overlay Vulkan: wrong RGBA upload size");
// Includes work enqueued by the previous SetOverlayTexture call, not
// just our upload. Reuse the staging bytes and commands only after it
// finishes. This dedicated queue is idle between content changes.
check(vkQueueWaitIdle(queue), "vkQueueWaitIdle");
std::memcpy(mapped, rgba.data(), bytes);
if (!coherent) {
VkMappedMemoryRange range{};
range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
range.memory = staging_memory;
range.size = VK_WHOLE_SIZE;
check(vkFlushMappedMemoryRanges(device, 1, &range), "vkFlushMappedMemoryRanges");
}
check(vkResetCommandBuffer(commands, 0), "vkResetCommandBuffer");
VkCommandBufferBeginInfo begin{};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
check(vkBeginCommandBuffer(commands, &begin), "vkBeginCommandBuffer");
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.srcAccessMask = uploaded ? VK_ACCESS_TRANSFER_READ_BIT : 0;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.oldLayout = uploaded ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = color_range;
vkCmdPipelineBarrier(commands, uploaded ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {width, height, 1};
vkCmdCopyBufferToImage(commands, staging, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 1, &barrier);
check(vkEndCommandBuffer(commands), "vkEndCommandBuffer");
VkSubmitInfo submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.commandBufferCount = 1;
submit.pCommandBuffers = &commands;
check(vkQueueSubmit(queue, 1, &submit, VK_NULL_HANDLE), "vkQueueSubmit");
// SteamVR enqueues its transfer on this same queue after this upload,
// with the image in the layout required by OpenVR's Vulkan contract.
uploaded = true;
}
};
OverlayTexture::OverlayTexture(uint32_t width, uint32_t height,
std::span<const std::string> instance_extensions,
const SelectDevice& select_device, const DeviceExtensions& device_extensions)
: impl_(std::make_unique<Impl>()) {
impl_->init(width, height, instance_extensions, select_device, device_extensions);
}
OverlayTexture::~OverlayTexture() = default;
void OverlayTexture::upload(std::span<const unsigned char> rgba) { impl_->upload(rgba); }
vr::Texture_t OverlayTexture::texture() {
if (!impl_->uploaded) throw std::runtime_error("Overlay Vulkan: texture has not been uploaded");
return {&impl_->description, vr::TextureType_Vulkan, vr::ColorSpace_Gamma};
}
} // namespace frameyap
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <vulkan/vulkan.h>
#include <openvr.h>
#include <functional>
#include <memory>
#include <span>
#include <string>
#include <vector>
namespace frameyap {
// One persistent RGBA image, staging allocation and graphics queue. When used
// with OpenVR, construct after initialization; destroy AFTER VR_Shutdown. Upload
// and SetOverlayTexture must run on the same thread (OpenVR uses our queue).
class OverlayTexture {
public:
using SelectDevice = std::function<VkPhysicalDevice(VkInstance)>;
using DeviceExtensions = std::function<std::vector<std::string>(VkPhysicalDevice)>;
OverlayTexture(uint32_t width, uint32_t height,
std::span<const std::string> instance_extensions,
const SelectDevice& select_device, const DeviceExtensions& device_extensions);
~OverlayTexture();
OverlayTexture(const OverlayTexture&) = delete;
OverlayTexture& operator=(const OverlayTexture&) = delete;
void upload(std::span<const unsigned char> rgba);
vr::Texture_t texture(); // valid after upload; descriptor lives with this object
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace frameyap
+2 -2
View File
@@ -335,8 +335,8 @@ bool PanelSurface::render(const Panel& p) { return impl_->render(p); }
const std::vector<unsigned char>& PanelSurface::pixels() const { return impl_->pixels; }
bool PanelSurface::available(UiAction a) const { return impl_->available(a); }
void PanelSurface::pointer_move(unsigned, float, float) {
// Hit-test on down/up only. SetOverlayRaw can flicker in SteamVR when each
// laser hover frame causes another full RGBA upload.
// Hit-test on down/up only; laser movement does not change panel content
// and does not require a GPU texture upload.
}
void PanelSurface::pointer_down(unsigned cursor, float x, float y) {
if (cursor >= impl_->pressed.size()) return;
+134
View File
@@ -0,0 +1,134 @@
// Explicit offscreen Vulkan check. Not registered with CTest: --run opts into
// GPU access. No OpenVR initialization, desktop surface, microphone or input.
#include "overlay_texture.hpp"
#include <bit>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string_view>
namespace {
void check(VkResult result, const char* operation) {
if (result != VK_SUCCESS) throw std::runtime_error(std::string(operation) + ": " + std::to_string(result));
}
struct Readback {
VkDevice device{};
VkBuffer buffer{};
VkDeviceMemory memory{};
VkCommandPool pool{};
void* mapped = nullptr;
~Readback() {
if (device) vkDeviceWaitIdle(device);
if (mapped) vkUnmapMemory(device, memory);
if (pool) vkDestroyCommandPool(device, pool, nullptr);
if (buffer) vkDestroyBuffer(device, buffer, nullptr);
if (memory) vkFreeMemory(device, memory, nullptr);
}
void verify(const vr::VRVulkanTextureData_t& texture, std::span<const unsigned char> expected) {
device = texture.m_pDevice;
VkBufferCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
info.size = expected.size();
info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateBuffer(device, &info, nullptr, &buffer), "create readback buffer");
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device, buffer, &requirements);
VkPhysicalDeviceMemoryProperties types{};
vkGetPhysicalDeviceMemoryProperties(texture.m_pPhysicalDevice, &types);
uint32_t type = 0;
while (type < types.memoryTypeCount && (!(requirements.memoryTypeBits & (1u << type)) ||
!(types.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT))) ++type;
if (type == types.memoryTypeCount) throw std::runtime_error("No host-visible readback memory");
VkMemoryAllocateInfo allocation{};
allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = type;
check(vkAllocateMemory(device, &allocation, nullptr, &memory), "allocate readback memory");
check(vkBindBufferMemory(device, buffer, memory, 0), "bind readback memory");
VkCommandPoolCreateInfo pool_info{};
pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
pool_info.queueFamilyIndex = texture.m_nQueueFamilyIndex;
check(vkCreateCommandPool(device, &pool_info, nullptr, &pool), "create readback command pool");
VkCommandBufferAllocateInfo command_info{};
command_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
command_info.commandPool = pool;
command_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
command_info.commandBufferCount = 1;
VkCommandBuffer command{};
check(vkAllocateCommandBuffers(device, &command_info, &command), "allocate readback commands");
VkCommandBufferBeginInfo begin{};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
check(vkBeginCommandBuffer(command, &begin), "begin readback");
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {texture.m_nWidth, texture.m_nHeight, 1};
vkCmdCopyImageToBuffer(command, std::bit_cast<VkImage>(texture.m_nImage),
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buffer, 1, &copy);
VkBufferMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.buffer = buffer;
barrier.size = VK_WHOLE_SIZE;
vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
0, 0, nullptr, 1, &barrier, 0, nullptr);
check(vkEndCommandBuffer(command), "end readback");
VkSubmitInfo submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.commandBufferCount = 1;
submit.pCommandBuffers = &command;
check(vkQueueSubmit(texture.m_pQueue, 1, &submit, VK_NULL_HANDLE), "submit readback");
check(vkQueueWaitIdle(texture.m_pQueue), "wait for readback");
check(vkMapMemory(device, memory, 0, VK_WHOLE_SIZE, 0, &mapped), "map readback");
if (!(types.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) {
VkMappedMemoryRange range{};
range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
range.memory = memory;
range.size = VK_WHOLE_SIZE;
check(vkInvalidateMappedMemoryRanges(device, 1, &range), "invalidate readback");
}
if (std::memcmp(mapped, expected.data(), expected.size()))
throw std::runtime_error("RGBA readback differs from uploaded pixels");
}
};
} // namespace
int main(int argc, char** argv) {
if (argc != 2 || std::string_view(argv[1]) != "--run") {
std::cout << "Use --run for an offscreen GPU upload/readback check. No OpenVR, audio or input.\n";
return argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--help") ? 0 : 2;
}
try {
frameyap::OverlayTexture texture(1000, 680, {}, [](VkInstance instance) {
uint32_t count = 0;
check(vkEnumeratePhysicalDevices(instance, &count, nullptr), "enumerate GPUs");
if (!count) throw std::runtime_error("No Vulkan GPU");
std::vector<VkPhysicalDevice> devices(count);
check(vkEnumeratePhysicalDevices(instance, &count, devices.data()), "enumerate GPUs");
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(devices.front(), &properties);
std::cout << "Vulkan device: " << properties.deviceName << '\n';
return devices.front();
}, [](VkPhysicalDevice) { return std::vector<std::string>{}; });
std::vector<unsigned char> pixels(1000 * 680 * 4);
uint64_t image = 0;
for (unsigned frame = 0; frame < 8; ++frame) {
for (size_t i = 0; i < pixels.size(); ++i) pixels[i] = (i * 17 + (i / 4000) * 31 + frame * 67) & 255;
texture.upload(pixels);
const auto submitted = texture.texture();
const auto& description = *static_cast<vr::VRVulkanTextureData_t*>(submitted.handle);
if (frame && description.m_nImage != image) throw std::runtime_error("Texture was recreated");
image = description.m_nImage;
Readback readback;
readback.verify(description, pixels);
}
std::cout << "8 exact 1000x680 RGBA readbacks; one persistent Vulkan image.\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n'; return 1;
}
}
+240
View File
@@ -0,0 +1,240 @@
// Link against these Vulkan fakes, never the loader. CTest must not touch a GPU
// or initialize OpenVR. Model queued transfers so reuse before completion fails.
#include "overlay_texture.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cassert>
#include <cstring>
#include <map>
#include <set>
#include <stdexcept>
namespace {
template<class T> T handle(uintptr_t value) { return std::bit_cast<T>(value); }
std::set<uintptr_t> live;
uintptr_t next_handle = 10;
unsigned calls = 0, fail_at = 0, images = 0, submits = 0, flushes = 0;
bool coherent = true;
std::map<VkDeviceMemory, std::vector<unsigned char>> allocations;
VkDeviceMemory image_memory{}, buffer_memory{};
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
std::vector<std::function<void()>> recorded, pending;
std::vector<std::vector<unsigned char>> observed;
VkResult result() { return ++calls == fail_at ? VK_ERROR_OUT_OF_DEVICE_MEMORY : VK_SUCCESS; }
template<class T> VkResult create(T* out) {
auto status = result();
if (status == VK_SUCCESS) { *out = handle<T>(next_handle++); live.insert(std::bit_cast<uintptr_t>(*out)); }
return status;
}
template<class T> void destroy(T resource) { assert(live.erase(std::bit_cast<uintptr_t>(resource)) == 1); }
void complete() { for (auto& work : pending) work(); pending.clear(); }
void reset() {
assert(live.empty() && allocations.empty() && pending.empty());
calls = images = submits = flushes = 0;
recorded.clear(); observed.clear(); layout = VK_IMAGE_LAYOUT_UNDEFINED;
}
const std::vector<std::string> instance_extensions{"VK_KHR_external_memory_capabilities"};
auto select_device = [](VkInstance instance) {
assert(live.contains(std::bit_cast<uintptr_t>(instance)));
return handle<VkPhysicalDevice>(2);
};
auto device_extensions = [](VkPhysicalDevice physical) {
assert(physical == handle<VkPhysicalDevice>(2));
return std::vector<std::string>{"VK_KHR_external_memory"};
};
void consume(frameyap::OverlayTexture& texture) {
const auto t = texture.texture();
assert(t.eType == vr::TextureType_Vulkan && t.eColorSpace == vr::ColorSpace_Gamma);
const auto& data = *static_cast<vr::VRVulkanTextureData_t*>(t.handle);
assert(data.m_nWidth == 2 && data.m_nHeight == 2 && data.m_nSampleCount == 1);
assert(data.m_nFormat == VK_FORMAT_R8G8B8A8_UNORM && data.m_nQueueFamilyIndex == 1);
assert(data.m_pPhysicalDevice == handle<VkPhysicalDevice>(2));
assert(data.m_pQueue == handle<VkQueue>(3));
assert(live.contains(data.m_nImage));
// Emulate SetOverlayTexture enqueuing a read AFTER our upload, on our queue.
pending.push_back([] {
assert(layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
auto& bytes = allocations.at(image_memory);
observed.emplace_back(bytes.begin(), bytes.begin() + 16);
});
}
template<class F> void throws(F f) {
bool threw = false;
try { f(); } catch (const std::runtime_error&) { threw = true; }
assert(threw);
}
} // namespace
extern "C" {
VKAPI_ATTR VkResult VKAPI_CALL vkCreateInstance(const VkInstanceCreateInfo* info, const VkAllocationCallbacks*, VkInstance* out) {
assert(info->enabledExtensionCount == 1);
assert(std::string(info->ppEnabledExtensionNames[0]) == instance_extensions[0]);
return create(out);
}
VKAPI_ATTR void VKAPI_CALL vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks*) {
assert(live.size() == 1); destroy(instance);
}
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceProperties(VkPhysicalDevice, VkPhysicalDeviceProperties* properties) {
properties->limits.maxImageDimension2D = 4096;
}
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceQueueFamilyProperties(VkPhysicalDevice, uint32_t* count, VkQueueFamilyProperties* out) {
if (!out) { *count = 2; return; }
assert(*count == 2);
out[0] = {}; out[0].queueFlags = VK_QUEUE_TRANSFER_BIT; out[0].queueCount = 1;
out[1] = {}; out[1].queueFlags = VK_QUEUE_GRAPHICS_BIT; out[1].queueCount = 1;
}
VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(VkPhysicalDevice physical, const VkDeviceCreateInfo* info,
const VkAllocationCallbacks*, VkDevice* out) {
assert(physical == handle<VkPhysicalDevice>(2));
assert(info->queueCreateInfoCount == 1 && info->pQueueCreateInfos->queueFamilyIndex == 1);
assert(info->enabledExtensionCount == 1);
assert(std::string(info->ppEnabledExtensionNames[0]) == "VK_KHR_external_memory");
return create(out);
}
VKAPI_ATTR void VKAPI_CALL vkDestroyDevice(VkDevice device, const VkAllocationCallbacks*) {
assert(live.size() == 2 && allocations.empty() && pending.empty()); destroy(device);
}
VKAPI_ATTR VkResult VKAPI_CALL vkDeviceWaitIdle(VkDevice) { complete(); return VK_SUCCESS; }
VKAPI_ATTR void VKAPI_CALL vkGetDeviceQueue(VkDevice, uint32_t family, uint32_t index, VkQueue* queue) {
assert(family == 1 && index == 0); *queue = handle<VkQueue>(3);
}
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties* memory) {
memory->memoryTypeCount = 2;
memory->memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
memory->memoryTypes[1].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
(coherent ? VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : 0);
}
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* out) {
assert(info->format == VK_FORMAT_R8G8B8A8_UNORM);
assert(info->extent.width == 2 && info->extent.height == 2 && info->extent.depth == 1);
assert(info->mipLevels == 1 && info->arrayLayers == 1 && info->samples == VK_SAMPLE_COUNT_1_BIT);
assert(info->tiling == VK_IMAGE_TILING_OPTIMAL);
assert(info->usage == (VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT));
++images; return create(out);
}
VKAPI_ATTR void VKAPI_CALL vkDestroyImage(VkDevice, VkImage image, const VkAllocationCallbacks*) { destroy(image); }
VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements(VkDevice, VkImage, VkMemoryRequirements* requirements) {
*requirements = {256, 256, 1};
}
VKAPI_ATTR VkResult VKAPI_CALL vkAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* out) {
auto status = create(out);
if (status == VK_SUCCESS) allocations[*out].resize(info->allocationSize);
return status;
}
VKAPI_ATTR void VKAPI_CALL vkFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) {
assert(allocations.erase(memory) == 1); destroy(memory);
}
VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory(VkDevice, VkImage, VkDeviceMemory memory, VkDeviceSize) {
image_memory = memory; return result();
}
VKAPI_ATTR VkResult VKAPI_CALL vkCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* out) {
assert(info->size == 16 && info->usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT); return create(out);
}
VKAPI_ATTR void VKAPI_CALL vkDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { destroy(buffer); }
VKAPI_ATTR void VKAPI_CALL vkGetBufferMemoryRequirements(VkDevice, VkBuffer, VkMemoryRequirements* requirements) {
*requirements = {256, 256, 2};
}
VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory memory, VkDeviceSize) {
buffer_memory = memory; return result();
}
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(VkDevice, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize size,
VkMemoryMapFlags, void** out) {
assert(offset == 0 && size == VK_WHOLE_SIZE);
auto status = result();
if (status == VK_SUCCESS) *out = allocations.at(memory).data();
return status;
}
VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(VkDevice, VkDeviceMemory memory) { assert(allocations.contains(memory)); }
VKAPI_ATTR VkResult VKAPI_CALL vkFlushMappedMemoryRanges(VkDevice, uint32_t count, const VkMappedMemoryRange* range) {
assert(!coherent && count == 1 && range->offset == 0 && range->size == VK_WHOLE_SIZE);
assert(range->memory == buffer_memory); ++flushes; return result();
}
VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool(VkDevice, const VkCommandPoolCreateInfo* info, const VkAllocationCallbacks*, VkCommandPool* out) {
assert(info->queueFamilyIndex == 1); return create(out);
}
VKAPI_ATTR void VKAPI_CALL vkDestroyCommandPool(VkDevice, VkCommandPool pool, const VkAllocationCallbacks*) { destroy(pool); }
VKAPI_ATTR VkResult VKAPI_CALL vkAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* out) {
auto status = result(); if (status == VK_SUCCESS) *out = handle<VkCommandBuffer>(4); return status;
}
VKAPI_ATTR VkResult VKAPI_CALL vkQueueWaitIdle(VkQueue queue) {
assert(queue == handle<VkQueue>(3)); auto status = result(); if (status == VK_SUCCESS) complete(); return status;
}
VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(VkCommandBuffer, VkCommandBufferResetFlags) {
assert(pending.empty()); recorded.clear(); return result();
}
VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return result(); }
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags,
VkDependencyFlags, uint32_t, const VkMemoryBarrier*, uint32_t, const VkBufferMemoryBarrier*,
uint32_t count, const VkImageMemoryBarrier* barriers) {
assert(count == 1);
const auto b = barriers[0];
assert(b.srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED && b.dstQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED);
assert(b.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT);
assert(b.subresourceRange.levelCount == 1 && b.subresourceRange.layerCount == 1);
recorded.push_back([b] { assert(layout == b.oldLayout); layout = b.newLayout; });
}
VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage(VkCommandBuffer, VkBuffer, VkImage, VkImageLayout target,
uint32_t count, const VkBufferImageCopy* copy) {
assert(count == 1 && target == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
assert(copy->bufferRowLength == 0 && copy->bufferImageHeight == 0 && copy->bufferOffset == 0);
assert(copy->imageExtent.width == 2 && copy->imageExtent.height == 2 && copy->imageExtent.depth == 1);
recorded.push_back([] {
assert(layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
std::copy_n(allocations.at(buffer_memory).begin(), 16, allocations.at(image_memory).begin());
});
}
VKAPI_ATTR VkResult VKAPI_CALL vkEndCommandBuffer(VkCommandBuffer) { return result(); }
VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(VkQueue queue, uint32_t count, const VkSubmitInfo* submit, VkFence) {
assert(queue == handle<VkQueue>(3) && count == 1 && submit->commandBufferCount == 1);
auto status = result();
if (status == VK_SUCCESS) { pending.insert(pending.end(), recorded.begin(), recorded.end()); ++submits; }
return status;
}
} // extern C
int main() {
const std::array<unsigned char, 16> first{255,0,0,255, 0,255,0,128, 0,0,255,0, 255,255,255,255};
const std::array<unsigned char, 16> second{0,0,0,0, 4,5,6,7, 8,9,10,11, 12,13,14,15};
for (bool use_coherent : {true, false}) {
reset(); coherent = use_coherent;
{
frameyap::OverlayTexture texture(2, 2, instance_extensions, select_device, device_extensions);
throws([&] { texture.texture(); });
throws([&] { texture.upload(std::span(first).first(15)); });
texture.upload(first);
const auto descriptor = texture.texture();
const auto image = static_cast<vr::VRVulkanTextureData_t*>(descriptor.handle)->m_nImage;
consume(texture);
texture.upload(second); // must drain first read before replacing staging bytes
assert(observed.size() == 1 && std::ranges::equal(observed[0], first));
assert(texture.texture().handle == descriptor.handle);
assert(static_cast<vr::VRVulkanTextureData_t*>(descriptor.handle)->m_nImage == image);
consume(texture);
assert(images == 1 && submits == 2 && flushes == (coherent ? 0u : 2u));
}
assert(observed.size() == 2 && std::ranges::equal(observed[1], second));
assert(live.empty() && allocations.empty());
const auto operation_count = calls;
// Every fallible allocation/upload operation must unwind all owned
// resources, including a failure while a previous frame is pending.
for (unsigned failure = 1; failure <= operation_count; ++failure) {
reset(); fail_at = failure;
throws([&] {
frameyap::OverlayTexture texture(2, 2, instance_extensions, select_device, device_extensions);
texture.upload(first); consume(texture);
texture.upload(second); consume(texture);
});
assert(live.empty() && allocations.empty() && pending.empty());
}
fail_at = 0;
}
reset();
throws([&] { frameyap::OverlayTexture texture(0, 2, instance_extensions, select_device, device_extensions); });
assert(calls == 0); // invalid sizes must not initialize Vulkan
throws([&] {
frameyap::OverlayTexture texture(2, 2, instance_extensions,
[](VkInstance) { return VkPhysicalDevice{}; }, device_extensions);
});
assert(live.empty()); // never pick an unrelated GPU when SteamVR returns none
}