blob: 9000c8583b327f63b25c97c4fe92deec13eed6f3 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/ui/lib/escher/impl/semaphore_pool.h"
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/event.h>
#include <gtest/gtest.h>
#include "src/ui/lib/escher/test/common/gtest_escher.h"
#include "src/ui/lib/escher/test/common/test_with_vk_validation_layer.h"
#include "src/ui/lib/escher/vk/vulkan_context.h"
#include "src/ui/lib/escher/vk/vulkan_device_queues.h"
namespace escher {
namespace {
using SemaphorePoolTest = escher::test::TestWithVkValidationLayer;
// Helper to get a fresh Zircon event.
zx::event CreateEvent() {
zx::event event;
zx_status_t status = zx::event::create(0, &event);
FX_DCHECK(status == ZX_OK);
return event;
}
// Verify that allocating from a completely empty pool returns a new, valid,
// unimported C++ Semaphore object.
VK_TEST_F(SemaphorePoolTest, AllocateFromEmptyPool) {
auto vulkan_queues =
escher::test::EscherEnvironment::GetGlobalTestEnvironment()->GetVulkanDevice();
auto device = vulkan_queues->GetVulkanContext().device;
auto dispatch_loader = vulkan_queues->dispatch_loader();
SemaphorePool pool(device, dispatch_loader);
auto sem = pool.Allocate();
ASSERT_TRUE(sem);
EXPECT_NE(sem->vk_semaphore(), vk::Semaphore());
EXPECT_FALSE(sem->is_imported());
}
// Verify that an unimported semaphore is routed to the cleaned_free_list_
// upon retirement and successfully reused by subsequent Allocate() calls.
VK_TEST_F(SemaphorePoolTest, RecycleAndReuseCleanedSemaphore) {
auto vulkan_queues =
escher::test::EscherEnvironment::GetGlobalTestEnvironment()->GetVulkanDevice();
auto device = vulkan_queues->GetVulkanContext().device;
auto dispatch_loader = vulkan_queues->dispatch_loader();
SemaphorePool pool(device, dispatch_loader);
auto sem1 = pool.Allocate();
const auto raw_vk_sem = sem1->vk_semaphore();
EXPECT_FALSE(sem1->is_imported());
// Release semaphore. When its ref-count hits 0, it should return to the pool.
sem1 = nullptr;
// Allocate again. The pool should reuse the exact same Vulkan semaphore.
auto sem2 = pool.Allocate();
EXPECT_EQ(sem2->vk_semaphore(), raw_vk_sem);
EXPECT_FALSE(sem2->is_imported());
}
// Verify that an imported semaphore is routed to the uncleaned_free_list_ upon
// retirement and successfully reused by subsequent AllocateAndImport() calls.
VK_TEST_F(SemaphorePoolTest, RecycleAndReuseUncleanedSemaphore) {
auto vulkan_queues =
escher::test::EscherEnvironment::GetGlobalTestEnvironment()->GetVulkanDevice();
auto device = vulkan_queues->GetVulkanContext().device;
auto dispatch_loader = vulkan_queues->dispatch_loader();
SemaphorePool pool(device, dispatch_loader);
auto sem1 = pool.AllocateAndImport(CreateEvent());
const auto raw_vk_sem = sem1->vk_semaphore();
EXPECT_TRUE(sem1->is_imported());
// Release semaphore. It should return to the pool's uncleaned list.
sem1 = nullptr;
// Allocate and import again. The pool should reuse the exact same Vulkan semaphore.
auto sem2 = pool.AllocateAndImport(CreateEvent());
EXPECT_EQ(sem2->vk_semaphore(), raw_vk_sem);
EXPECT_TRUE(sem2->is_imported());
}
// Verify the decoupled fallback where Allocate() pulls from the uncleaned list,
// performs a lazy clean, resets/preserves the flags, and returns.
VK_TEST_F(SemaphorePoolTest, AllocatePullsFromUncleanedWithLazyStomp) {
auto vulkan_queues =
escher::test::EscherEnvironment::GetGlobalTestEnvironment()->GetVulkanDevice();
auto device = vulkan_queues->GetVulkanContext().device;
auto dispatch_loader = vulkan_queues->dispatch_loader();
SemaphorePool pool(device, dispatch_loader);
// Put a semaphore into the uncleaned list by allocating/importing and releasing.
auto sem1 = pool.AllocateAndImport(CreateEvent());
const auto raw_vk_sem = sem1->vk_semaphore();
sem1 = nullptr;
// Call Allocate() (with cleaned list empty). It should fallback to uncleaned,
// perform a lazy clean, and return it.
auto sem2 = pool.Allocate();
EXPECT_EQ(sem2->vk_semaphore(), raw_vk_sem);
EXPECT_FALSE(sem2->is_imported());
}
// Verify the decoupled fallback where AllocateAndImport() pulls from the cleaned
// list, overwrites it with a new import, and sets is_imported to true.
VK_TEST_F(SemaphorePoolTest, AllocateAndImportPullsFromCleaned) {
auto vulkan_queues =
escher::test::EscherEnvironment::GetGlobalTestEnvironment()->GetVulkanDevice();
auto device = vulkan_queues->GetVulkanContext().device;
auto dispatch_loader = vulkan_queues->dispatch_loader();
SemaphorePool pool(device, dispatch_loader);
// Put a semaphore into the cleaned list by allocating internally and releasing.
auto sem1 = pool.Allocate();
const auto raw_vk_sem = sem1->vk_semaphore();
sem1 = nullptr;
// Call AllocateAndImport() (with uncleaned list empty). It should fallback to cleaned,
// import the new event, and return it.
auto sem2 = pool.AllocateAndImport(CreateEvent());
EXPECT_EQ(sem2->vk_semaphore(), raw_vk_sem);
EXPECT_TRUE(sem2->is_imported());
}
// Verify that a cleaned semaphore can be signaled and waited on again by the GPU queue.
VK_TEST_F(SemaphorePoolTest, VerifySignalingCleanedSemaphore) {
auto vulkan_queues =
escher::test::EscherEnvironment::GetGlobalTestEnvironment()->GetVulkanDevice();
auto device = vulkan_queues->GetVulkanContext().device;
auto dispatch_loader = vulkan_queues->dispatch_loader();
auto queue = vulkan_queues->GetVulkanContext().queue;
SemaphorePool pool(device, dispatch_loader);
auto sem1 = pool.Allocate();
const auto raw_vk_sem = sem1->vk_semaphore();
// Signal semaphore via a queue submit.
{
vk::SubmitInfo submit_info;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &raw_vk_sem;
auto result = queue.submit(1, &submit_info, vk::Fence());
EXPECT_EQ(result, vk::Result::eSuccess);
}
// Wait for GPU to finish executing the queue command.
{
auto idle_result = device.waitIdle();
EXPECT_EQ(idle_result, vk::Result::eSuccess);
}
// Retire semaphore, so it goes to the cleaned list.
sem1 = nullptr;
auto sem2 = pool.Allocate();
EXPECT_EQ(sem2->vk_semaphore(), raw_vk_sem);
// Signal it again. If the clean failed to reset the Vulkan-internal signal state,
// the Vulkan Validation Layers will instantly trigger a validation error.
{
vk::SubmitInfo submit_info;
submit_info.signalSemaphoreCount = 1;
const vk::Semaphore raw_vk_sem2 = sem2->vk_semaphore();
submit_info.pSignalSemaphores = &raw_vk_sem2;
auto result = queue.submit(1, &submit_info, vk::Fence());
EXPECT_EQ(result, vk::Result::eSuccess);
}
// Wait for the queue to finish executing.
auto idle_result = device.waitIdle();
EXPECT_EQ(idle_result, vk::Result::eSuccess);
}
// Verify that an uncleaned semaphore is successfully signaled, and that importing a new event
// successfully replaces its payload, allowing it to be signaled and waited on again.
VK_TEST_F(SemaphorePoolTest, VerifySignalingRecycledUncleanedSemaphore) {
auto vulkan_queues =
escher::test::EscherEnvironment::GetGlobalTestEnvironment()->GetVulkanDevice();
auto device = vulkan_queues->GetVulkanContext().device;
auto dispatch_loader = vulkan_queues->dispatch_loader();
auto queue = vulkan_queues->GetVulkanContext().queue;
SemaphorePool pool(device, dispatch_loader);
zx::event event1 = CreateEvent();
zx::event event1_dup;
auto dup_status = event1.duplicate(ZX_RIGHT_SAME_RIGHTS, &event1_dup);
ASSERT_EQ(dup_status, ZX_OK);
auto sem1 = pool.AllocateAndImport(std::move(event1));
const auto raw_vk_sem = sem1->vk_semaphore();
// Signal the Vulkan semaphore.
{
vk::SubmitInfo submit_info;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &raw_vk_sem;
auto result = queue.submit(1, &submit_info, vk::Fence());
EXPECT_EQ(result, vk::Result::eSuccess);
}
// Verify that the duplicate event is signaled on the CPU.
auto wait_status = event1_dup.wait_one(ZX_EVENT_SIGNALED, zx::time::infinite(), nullptr);
EXPECT_EQ(wait_status, ZX_OK);
// Wait for GPU to finish executing the queue command.
{
auto idle_result = device.waitIdle();
EXPECT_EQ(idle_result, vk::Result::eSuccess);
}
// Retire the semaphore, so it goes to the uncleaned list.
sem1 = nullptr;
zx::event event2 = CreateEvent();
zx::event event2_dup;
dup_status = event2.duplicate(ZX_RIGHT_SAME_RIGHTS, &event2_dup);
ASSERT_EQ(dup_status, ZX_OK);
auto sem2 = pool.AllocateAndImport(std::move(event2));
EXPECT_EQ(sem2->vk_semaphore(), raw_vk_sem);
// Signal it again.
{
vk::SubmitInfo submit_info;
submit_info.signalSemaphoreCount = 1;
const vk::Semaphore raw_vk_sem2 = sem2->vk_semaphore();
submit_info.pSignalSemaphores = &raw_vk_sem2;
auto result = queue.submit(1, &submit_info, vk::Fence());
EXPECT_EQ(result, vk::Result::eSuccess);
}
// Verify that the new event is signaled on the CPU.
wait_status = event2_dup.wait_one(ZX_EVENT_SIGNALED, zx::time::infinite(), nullptr);
EXPECT_EQ(wait_status, ZX_OK);
// Wait for the queue to finish.
auto idle_result = device.waitIdle();
EXPECT_EQ(idle_result, vk::Result::eSuccess);
}
} // namespace
} // namespace escher