Files
InstanceBrick--nightcap/tests/vr/queue_submission_test.cc
T
InstanceBrick 7dc4f55642 Add native ARM64 Steam Frame VR and eye-tracked rendering
Integrate Frame OpenXR input, SteamVR video controls, gaze foveation, native audio, frame pacing and source-based packaging.
2026-10-16 15:31:49 -04:00

290 lines
11 KiB
C++

#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
#include "mocktail/vr/openxr_backend.h"
using SubmitAdapter = VkResult(VKAPI_PTR *)(PFN_vkQueueSubmit, VkDevice,
VkQueue, uint32_t,
const VkSubmitInfo *, VkFence);
extern "C" VKAPI_ATTR VkResult VKAPI_CALL mocktail_vulkan_submit_synchronized(
PFN_vkQueueSubmit raw, VkDevice device, VkQueue queue, uint32_t count,
const VkSubmitInfo *submits, VkFence fence);
extern "C" void mocktail_vr_xr_queue_submit_adapter(SubmitAdapter);
extern "C" VKAPI_ATTR VkResult VKAPI_CALL
mocktail_vulkan_idle_synchronized(PFN_vkDeviceWaitIdle raw, VkDevice device);
extern "C" VKAPI_ATTR std::int32_t mocktail_vulkan_run_queue_synchronized(
VkDevice device, VkQueue queue, std::int32_t (*operation)(void *), void *context);
namespace {
std::mutex state_mutex;
std::condition_variable state_changed;
bool waiting, signal_submitted;
bool idle_release;
std::atomic<int> inside_driver{0}, max_inside{0};
VKAPI_ATTR VkResult VKAPI_CALL Reset(VkDevice, uint32_t, const VkFence *) {
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL Submit(VkQueue, uint32_t,
const VkSubmitInfo *info, VkFence) {
if (info && info->signalSemaphoreCount) {
std::lock_guard<std::mutex> lock(state_mutex);
signal_submitted = true;
state_changed.notify_all();
}
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL Wait(VkDevice, uint32_t, const VkFence *,
VkBool32, uint64_t) {
std::unique_lock<std::mutex> lock(state_mutex);
waiting = true;
state_changed.notify_all();
return state_changed.wait_for(lock, std::chrono::seconds(2),
[] { return signal_submitted; })
? VK_SUCCESS
: VK_TIMEOUT;
}
VKAPI_ATTR VkResult VKAPI_CALL SlowSubmit(VkQueue, uint32_t,
const VkSubmitInfo *, VkFence) {
const int active = ++inside_driver;
int previous = max_inside.load();
while (active > previous &&
!max_inside.compare_exchange_weak(previous, active)) {
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
--inside_driver;
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL BlockingIdle(VkDevice) {
std::unique_lock<std::mutex> lock(state_mutex);
waiting = true;
state_changed.notify_all();
return state_changed.wait_for(lock, std::chrono::seconds(2),
[] { return idle_release; })
? VK_SUCCESS
: VK_TIMEOUT;
}
TEST(VrQueueSubmission, DeviceIdleExcludesPrivateSubmitsUntilDriverReturns) {
// An armed VR backend does this.
mocktail::vr::internal::RequestVulkanQueueSharing();
waiting = signal_submitted = idle_release = false;
VkResult idle_result = VK_NOT_READY;
std::thread idler([&] {
idle_result = mocktail_vulkan_idle_synchronized(
BlockingIdle, reinterpret_cast<VkDevice>(2));
});
bool entered = false;
{
std::unique_lock<std::mutex> lock(state_mutex);
entered = state_changed.wait_for(lock, std::chrono::seconds(1),
[] { return waiting; });
}
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
VkSubmitInfo signal{};
signal.signalSemaphoreCount = 1;
signal.pSignalSemaphores = &semaphore;
std::thread submitter([&] {
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
Submit, reinterpret_cast<VkDevice>(2),
reinterpret_cast<VkQueue>(30), 1, &signal, VK_NULL_HANDLE),
VK_SUCCESS);
});
{
std::unique_lock<std::mutex> lock(state_mutex);
EXPECT_FALSE(state_changed.wait_for(lock, std::chrono::milliseconds(50),
[] { return signal_submitted; }));
idle_release = true;
state_changed.notify_all();
}
idler.join();
submitter.join();
EXPECT_TRUE(entered);
EXPECT_EQ(idle_result, VK_SUCCESS);
EXPECT_TRUE(signal_submitted);
}
TEST(VrQueueSubmission, OtherDeviceCanSignalWhileDeviceIdleIsPending) {
waiting = signal_submitted = idle_release = false;
VkResult idle_result = VK_NOT_READY;
// Wait emulates device A waiting for an imported semaphore signal from B.
auto idle = [](VkDevice) -> VkResult {
return Wait(VK_NULL_HANDLE, 0, nullptr, VK_TRUE, UINT64_MAX);
};
std::thread idler([&] {
idle_result =
mocktail_vulkan_idle_synchronized(idle, reinterpret_cast<VkDevice>(2));
});
bool entered = false;
{
std::unique_lock<std::mutex> lock(state_mutex);
entered = state_changed.wait_for(lock, std::chrono::seconds(1),
[] { return waiting; });
}
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
VkSubmitInfo signal{};
signal.signalSemaphoreCount = 1;
signal.pSignalSemaphores = &semaphore;
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
Submit, reinterpret_cast<VkDevice>(3),
reinterpret_cast<VkQueue>(31), 1, &signal, VK_NULL_HANDLE),
VK_SUCCESS);
idler.join();
EXPECT_TRUE(entered);
EXPECT_EQ(idle_result, VK_SUCCESS);
}
TEST(VrQueueSubmission, AnotherQueueCanSignalWhilePresentWaitsForFence) {
waiting = signal_submitted = false;
mocktail_vr_xr_queue_submit_adapter(mocktail_vulkan_submit_synchronized);
const auto graphics = reinterpret_cast<VkQueue>(30);
const auto signal_queue = reinterpret_cast<VkQueue>(31);
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
VkPresentInfoKHR present{};
present.waitSemaphoreCount = 1;
present.pWaitSemaphores = &semaphore;
VkResult prepared = VK_NOT_READY;
std::thread presenter([&] {
prepared = mocktail::vr::internal::WaitForPresentDependencies(
reinterpret_cast<VkDevice>(2), graphics, present,
reinterpret_cast<VkFence>(33), Reset, Submit, Wait);
});
bool entered = false;
{
std::unique_lock<std::mutex> lock(state_mutex);
entered = state_changed.wait_for(lock, std::chrono::seconds(1),
[] { return waiting; });
}
VkSubmitInfo signal{};
signal.signalSemaphoreCount = 1;
signal.pSignalSemaphores = &semaphore;
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
Submit, reinterpret_cast<VkDevice>(2), signal_queue, 1, &signal,
VK_NULL_HANDLE),
VK_SUCCESS);
presenter.join();
mocktail_vr_xr_queue_submit_adapter(nullptr);
EXPECT_TRUE(entered);
EXPECT_EQ(prepared, VK_SUCCESS);
}
TEST(VrQueueSubmission, GuestCallsAreNotSerializedWithoutVr) {
// ctest runs each test in its own process, so sharing starts disabled here:
// without an armed VR backend the guest owns its queue synchronization.
inside_driver = max_inside = 0;
const auto queue = reinterpret_cast<VkQueue>(30);
auto call = [&] {
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
SlowSubmit, reinterpret_cast<VkDevice>(2), queue, 0, nullptr,
VK_NULL_HANDLE),
VK_SUCCESS);
};
std::thread first(call), second(call);
first.join();
second.join();
EXPECT_EQ(max_inside, 2);
}
TEST(VrQueueSubmission, PrivateAndGuestCallsCannotEnterOneQueueConcurrently) {
mocktail::vr::internal::RequestVulkanQueueSharing();
inside_driver = max_inside = 0;
const auto queue = reinterpret_cast<VkQueue>(30);
auto call = [&] {
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
SlowSubmit, reinterpret_cast<VkDevice>(2), queue, 0, nullptr,
VK_NULL_HANDLE),
VK_SUCCESS);
};
std::thread first(call), second(call);
first.join();
second.join();
EXPECT_EQ(max_inside, 1);
}
TEST(VrQueueSubmission, FenceWaitDoesNotKeepItsOwnQueueHostLocked) {
waiting = signal_submitted = false;
mocktail_vr_xr_queue_submit_adapter(mocktail_vulkan_submit_synchronized);
const auto queue = reinterpret_cast<VkQueue>(30);
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
VkPresentInfoKHR present{};
present.waitSemaphoreCount = 1;
present.pWaitSemaphores = &semaphore;
VkResult result = VK_NOT_READY;
std::thread presenter([&] {
result = mocktail::vr::internal::WaitForPresentDependencies(
reinterpret_cast<VkDevice>(2), queue, present,
reinterpret_cast<VkFence>(33), Reset, Submit, Wait);
});
{
std::unique_lock<std::mutex> lock(state_mutex);
state_changed.wait_for(lock, std::chrono::seconds(1),
[] { return waiting; });
}
// An unrelated already-ready batch can still be enqueued to this queue.
EXPECT_EQ(
mocktail_vulkan_submit_synchronized(Submit, reinterpret_cast<VkDevice>(2),
queue, 0, nullptr, VK_NULL_HANDLE),
VK_SUCCESS);
{
std::lock_guard<std::mutex> lock(state_mutex);
signal_submitted = true;
state_changed.notify_all();
}
presenter.join();
mocktail_vr_xr_queue_submit_adapter(nullptr);
EXPECT_EQ(result, VK_SUCCESS);
}
TEST(VrQueueSubmission, RuntimeFrameCallExcludesGuestOnTheSameQueue) {
mocktail::vr::internal::RequestVulkanQueueSharing();
inside_driver = max_inside = 0;
const auto device = reinterpret_cast<VkDevice>(2);
const auto queue = reinterpret_cast<VkQueue>(30);
std::thread runtime([&] {
EXPECT_EQ(mocktail_vulkan_run_queue_synchronized(
device, queue, [](void *context) -> std::int32_t {
EXPECT_EQ(context, reinterpret_cast<void *>(42));
SlowSubmit(VK_NULL_HANDLE, 0, nullptr, VK_NULL_HANDLE);
return -37;
}, reinterpret_cast<void *>(42)), -37);
});
std::thread guest([&] {
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
SlowSubmit, device, queue, 0, nullptr, VK_NULL_HANDLE), VK_SUCCESS);
});
runtime.join();
guest.join();
EXPECT_EQ(max_inside, 1);
// An error from the runtime must also release the queue lock.
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
Submit, device, queue, 0, nullptr, VK_NULL_HANDLE), VK_SUCCESS);
}
TEST(VrQueueSubmission, RuntimeFrameCallDoesNotBlockADifferentQueue) {
mocktail::vr::internal::RequestVulkanQueueSharing();
waiting = signal_submitted = false;
const auto device = reinterpret_cast<VkDevice>(2);
std::int32_t result = VK_NOT_READY;
std::thread runtime([&] {
result = mocktail_vulkan_run_queue_synchronized(
device, reinterpret_cast<VkQueue>(30), [](void *) -> std::int32_t {
return Wait(VK_NULL_HANDLE, 0, nullptr, VK_TRUE, UINT64_MAX);
}, nullptr);
});
bool entered;
{
std::unique_lock<std::mutex> lock(state_mutex);
entered = state_changed.wait_for(lock, std::chrono::seconds(1),
[] { return waiting; });
}
const auto semaphore = reinterpret_cast<VkSemaphore>(32);
VkSubmitInfo signal{};
signal.signalSemaphoreCount = 1;
signal.pSignalSemaphores = &semaphore;
EXPECT_EQ(mocktail_vulkan_submit_synchronized(
Submit, device, reinterpret_cast<VkQueue>(31), 1, &signal, VK_NULL_HANDLE),
VK_SUCCESS);
runtime.join();
EXPECT_TRUE(entered);
EXPECT_EQ(result, VK_SUCCESS);
}
} // namespace