mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 01:00:04 +02:00
Submit the overlay through a persistent Vulkan texture
This commit is contained in:
1 parent
8417212ed2
commit
4c043c9962
13 files changed
+750
-26
No files matched your search
+12
-2
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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) +
|
||||
|
||||
@@ -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, ©);
|
||||
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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ©);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user