This commit is contained in:
2026-08-15 18:59:20 +08:00
parent d48f6608ad
commit 8bfaeeb265
15 changed files with 304 additions and 242 deletions
+13 -14
View File
@@ -125,7 +125,7 @@ struct Basic_Point_Scene final
Basic_Point_Scene(const Scene_Options& options,
std::shared_ptr<Point_Visual> visual)
requires (!std::same_as<Strategy, Low_Latency_Frame>)
: Kernel_Scene() {
: Kernel_Scene(), render_domain(detail::Render_Domain::acquire(options.gpu_index)) {
initialize(options, std::move(visual));
}
@@ -134,14 +134,14 @@ struct Basic_Point_Scene final
requires std::same_as<Strategy, Low_Latency_Frame>
: Kernel_Scene(Observer_State<>{}, typename Strategy::Configuration{
.frequency_hz = options.maximum_frames_per_second,
.replace_pending_frame = true}) {
.replace_pending_frame = true}),
render_domain(detail::Render_Domain::acquire(options.gpu_index)) {
initialize(options, std::move(visual));
}
~Basic_Point_Scene() override {
this->shutdown();
gpu_completion.shutdown();
render_domain.invoke([this] { backend.reset(); });
render_domain->invoke([this] { backend.reset(); });
}
void initialize(const Scene_Options& options,
@@ -162,7 +162,7 @@ struct Basic_Point_Scene final
}
const detail::Scene_State initial{
options.viewport, options.clear_color, options.visual_family};
render_domain.invoke([this, &options, &initial] {
render_domain->invoke([this, &options, &initial] {
backend = std::make_unique<detail::Datoviz_Visual_Backend>(
options.gpu_index, options.validation_enabled, initial);
});
@@ -186,7 +186,7 @@ struct Basic_Point_Scene final
::renderive::Keyboard_Modifier modifiers) override {
const Extent viewport = this->Scene_State_Strategy::template get<
&detail::Scene_State::viewport>();
render_domain.invoke([this, type, x, y, button, modifiers, viewport] {
render_domain->invoke([this, type, x, y, button, modifiers, viewport] {
backend->dispatch_pointer(type, x, y, button, modifiers, viewport);
});
this->notify_model_dirty();
@@ -197,7 +197,7 @@ struct Basic_Point_Scene final
::renderive::Keyboard_Modifier modifiers) override {
const Extent viewport = this->Scene_State_Strategy::template get<
&detail::Scene_State::viewport>();
render_domain.invoke(
render_domain->invoke(
[this, x, y, delta_x, delta_y, modifiers, viewport] {
backend->dispatch_wheel(x, y, delta_x, delta_y, modifiers,
viewport);
@@ -206,7 +206,7 @@ struct Basic_Point_Scene final
}
void dispatch_key(const ::renderive::Key_Event& event) override {
render_domain.invoke([this, event] { backend->dispatch_key(event); });
render_domain->invoke([this, event] { backend->dispatch_key(event); });
this->notify_model_dirty();
}
@@ -327,7 +327,7 @@ struct Basic_Point_Scene final
auto async_frame = std::make_shared<Async_Frame_State>();
auto render_completion = std::make_shared<
detail::Render_Domain::Prepared_Task>(
render_domain.prepare(
render_domain->prepare(
[this, async_frame, metrics, diagnostics, source]() mutable {
if (!async_frame->pending) {
static_cast<void>(source->fail(
@@ -375,11 +375,11 @@ struct Basic_Point_Scene final
}));
auto completion = std::make_shared<
detail::Gpu_Completion_Service::Reservation>(
gpu_completion.prepare(
detail::Gpu_Completion_Service::instance().prepare(
[this, async_frame, render_completion](
Completion_Result result) noexcept {
async_frame->completion = std::move(result);
render_domain.post(std::move(*render_completion));
render_domain->post(std::move(*render_completion));
},
observe));
const auto queued_at = observe
@@ -387,7 +387,7 @@ struct Basic_Point_Scene final
: std::chrono::steady_clock::time_point{};
try {
render_domain.post(
render_domain->post(
[this, scene_state, scene_revision, frame_sequence, prepared,
source, async_frame, completion, observe,
queued_at] {
@@ -432,8 +432,7 @@ struct Basic_Point_Scene final
std::shared_ptr<Point_Visual> visual;
Renderable_Id point_id{};
detail::Render_Domain render_domain;
detail::Gpu_Completion_Service gpu_completion;
std::shared_ptr<detail::Render_Domain> render_domain;
std::unique_ptr<detail::Datoviz_Visual_Backend> backend;
mutable std::mutex frame_mutex;
std::shared_ptr<const Pixel_Frame> latest;
@@ -1,15 +1,23 @@
#include "Gpu_Completion_Service.h"
#include <chrono>
#include <algorithm>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace renderive::render_3d::detail {
Gpu_Completion_Service& Gpu_Completion_Service::instance() {
static Gpu_Completion_Service service;
return service;
}
Gpu_Completion_Service::Gpu_Completion_Service() {
pending_.set_capacity(default_capacity);
thread_ = std::thread([this] { run(); });
}
Gpu_Completion_Service::~Gpu_Completion_Service() {
shutdown();
stopping_.store(true, std::memory_order_release);
wake();
if (thread_.joinable())
thread_.join();
}
Gpu_Completion_Service::Reservation::Reservation(std::shared_ptr<Pending_Fence> pending) noexcept : pending_(std::move(pending)) {}
Gpu_Completion_Service::Reservation::~Reservation() {
@@ -26,108 +34,131 @@ void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence)
std::terminate();
pending->device = device;
pending->fence = fence;
if (pending->observe)
pending->watched_at = std::chrono::steady_clock::now();
pending->status = Pending_Fence::Status::watched;
}
pending->ready.notify_one();
pending->service->wake();
}
void Gpu_Completion_Service::Reservation::cancel() noexcept {
if (!pending_)
return;
auto pending = std::exchange(pending_, {});
cancel_reserved(pending);
pending->service->wake();
}
void Gpu_Completion_Service::cancel_reserved(const std::shared_ptr<Pending_Fence>& pending) noexcept {
if (!pending)
return;
{
std::lock_guard lock(pending->mutex);
if (pending->status == Pending_Fence::Status::reserved)
pending->status = Pending_Fence::Status::canceled;
}
pending->ready.notify_one();
std::lock_guard lock(pending->mutex);
if (pending->status == Pending_Fence::Status::reserved)
pending->status = Pending_Fence::Status::canceled;
}
Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion completion, bool observe) {
if (!completion)
throw std::invalid_argument("GPU completion callback is empty");
if (stopping_.load(std::memory_order_acquire))
throw std::runtime_error("GPU completion service is stopping");
auto pending = std::make_shared<Pending_Fence>();
pending->completion = std::move(completion);
pending->observe = observe;
{
std::lock_guard lock(mutex_);
if (stopping_)
throw std::runtime_error("GPU completion service is stopping");
pending->service = this;
slots_.acquire();
try {
pending_.push(pending);
} catch (...) {
slots_.release();
throw;
}
wake();
return Reservation(std::move(pending));
}
void Gpu_Completion_Service::shutdown() noexcept {
{
std::lock_guard lock(mutex_);
if (stopping_ && !thread_.joinable())
return;
stopping_ = true;
cancel_reserved(active_);
std::shared_ptr<Pending_Fence> pending;
while (pending_.try_pop(pending))
cancel_reserved(pending);
pending_.push(std::shared_ptr<Pending_Fence>{});
}
if (thread_.joinable())
thread_.join();
void Gpu_Completion_Service::wake() noexcept {
wake_.notify_one();
}
void Gpu_Completion_Service::run() noexcept {
std::vector<std::shared_ptr<Pending_Fence>> active;
active.reserve(static_cast<std::size_t>(default_capacity));
for (;;) {
std::shared_ptr<Pending_Fence> pending;
pending_.pop(pending);
if (!pending)
return;
{
std::lock_guard lock(mutex_);
if (stopping_) {
cancel_reserved(pending);
std::shared_ptr<Pending_Fence> incoming;
while (pending_.try_pop(incoming))
active.push_back(std::move(incoming));
bool progressed{};
for (auto iterator = active.begin(); iterator != active.end();) {
auto& pending = *iterator;
VkDevice device{VK_NULL_HANDLE};
VkFence fence{VK_NULL_HANDLE};
Completion completion;
std::chrono::steady_clock::time_point watched_at{};
bool observe{};
Pending_Fence::Status status;
{
std::lock_guard lock(pending->mutex);
status = pending->status;
if (status == Pending_Fence::Status::watched) {
device = pending->device;
fence = pending->fence;
watched_at = pending->watched_at;
observe = pending->observe;
}
}
if (status == Pending_Fence::Status::canceled) {
iterator = active.erase(iterator);
slots_.release();
progressed = true;
continue;
}
active_ = pending;
}
VkDevice device{VK_NULL_HANDLE};
VkFence fence{VK_NULL_HANDLE};
Completion completion;
bool observe{};
{
std::unique_lock lock(pending->mutex);
pending->ready.wait(lock, [&pending] { return pending->status != Pending_Fence::Status::reserved; });
if (pending->status == Pending_Fence::Status::canceled) {
lock.unlock();
std::lock_guard service_lock(mutex_);
active_.reset();
if (status == Pending_Fence::Status::reserved) {
if (stopping_.load(std::memory_order_acquire)) {
cancel_reserved(pending);
iterator = active.erase(iterator);
slots_.release();
progressed = true;
continue;
}
++iterator;
continue;
}
device = pending->device;
fence = pending->fence;
completion = std::move(pending->completion);
observe = pending->observe;
}
const auto wait_started = observe ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const VkResult result = vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
Result completion_result;
if (observe) {
const auto wait_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - wait_started).count();
completion_result.wait_duration_ns = wait_duration > 0 ? static_cast<std::uint64_t>(wait_duration) : 0;
}
if (result != VK_SUCCESS) {
const VkResult result = vkGetFenceStatus(device, fence);
if (result == VK_NOT_READY) {
++iterator;
continue;
}
{
std::lock_guard lock(pending->mutex);
completion = std::move(pending->completion);
pending->status = Pending_Fence::Status::canceled;
}
Result completion_result;
if (observe) {
const auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - watched_at).count();
completion_result.wait_duration_ns = duration > 0 ? static_cast<std::uint64_t>(duration) : 0;
}
if (result != VK_SUCCESS) {
try {
throw std::runtime_error("GPU fence wait failed with Vulkan result " + std::to_string(static_cast<int>(result)));
} catch (...) {
completion_result.error = std::current_exception();
}
}
try {
throw std::runtime_error("GPU fence wait failed with Vulkan result " + std::to_string(static_cast<int>(result)));
completion(std::move(completion_result));
} catch (...) {
completion_result.error = std::current_exception();
}
iterator = active.erase(iterator);
slots_.release();
progressed = true;
}
try {
completion(std::move(completion_result));
} catch (...) {
}
{
std::lock_guard lock(mutex_);
active_.reset();
if (stopping_.load(std::memory_order_acquire) && active.empty() && pending_.empty())
return;
if (!progressed) {
std::unique_lock lock(wait_mutex_);
if (active.empty())
wake_.wait(lock, [this] {
return stopping_.load(std::memory_order_acquire) || !pending_.empty();
});
else
wake_.wait_for(lock, poll_interval);
}
}
}
@@ -1,11 +1,15 @@
#pragma once
#include <volk.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <functional>
#include <memory>
#include <mutex>
#include <semaphore>
#include <thread>
#include <oneapi/tbb/concurrent_queue.h>
namespace renderive::render_3d::detail {
@@ -32,12 +36,10 @@ public:
std::shared_ptr<Pending_Fence> pending_;
friend class Gpu_Completion_Service;
};
Gpu_Completion_Service();
~Gpu_Completion_Service();
static Gpu_Completion_Service& instance();
Gpu_Completion_Service(const Gpu_Completion_Service&) = delete;
Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete;
[[nodiscard]] Reservation prepare(Completion completion, bool observe);
void shutdown() noexcept;
private:
struct Pending_Fence {
enum class Status {
@@ -46,20 +48,26 @@ private:
canceled
};
std::mutex mutex;
std::condition_variable ready;
VkDevice device{VK_NULL_HANDLE};
VkFence fence{VK_NULL_HANDLE};
Completion completion;
std::chrono::steady_clock::time_point watched_at{};
Gpu_Completion_Service* service{};
Status status{Status::reserved};
bool observe{};
};
Gpu_Completion_Service();
~Gpu_Completion_Service();
static void cancel_reserved(const std::shared_ptr<Pending_Fence>& pending) noexcept;
void wake() noexcept;
void run() noexcept;
static constexpr std::size_t default_capacity = 64;
std::mutex mutex_;
static constexpr std::ptrdiff_t default_capacity = 1024;
static constexpr auto poll_interval = std::chrono::microseconds(200);
std::counting_semaphore<default_capacity> slots_{default_capacity};
oneapi::tbb::concurrent_bounded_queue<std::shared_ptr<Pending_Fence>> pending_;
std::shared_ptr<Pending_Fence> active_;
std::mutex wait_mutex_;
std::condition_variable wake_;
std::atomic_bool stopping_{};
std::thread thread_;
bool stopping_{};
};
}
@@ -0,0 +1,81 @@
#include "Render_Domain.h"
#include <unordered_map>
namespace renderive::render_3d::detail {
namespace {
struct Render_Domain_Registry {
std::mutex mutex;
std::unordered_map<std::uint32_t, std::shared_ptr<Render_Domain>> domains;
};
Render_Domain_Registry& registry() {
static Render_Domain_Registry value;
return value;
}
}
Render_Domain::Prepared_Task::~Prepared_Task() {
release();
}
Render_Domain::Prepared_Task::Prepared_Task(Prepared_Task&& other) noexcept
: domain_(std::exchange(other.domain_, nullptr)), task_(std::move(other.task_)) {}
Render_Domain::Prepared_Task& Render_Domain::Prepared_Task::operator=(Prepared_Task&& other) noexcept {
if (this == &other)
return *this;
release();
domain_ = std::exchange(other.domain_, nullptr);
task_ = std::move(other.task_);
return *this;
}
void Render_Domain::Prepared_Task::release() noexcept {
if (!domain_)
return;
task_.reset();
domain_->slots_.release();
domain_ = nullptr;
}
std::shared_ptr<Render_Domain> Render_Domain::acquire(std::uint32_t gpu_index) {
auto& storage = registry();
std::lock_guard lock(storage.mutex);
if (const auto found = storage.domains.find(gpu_index); found != storage.domains.end())
return found->second;
auto domain = std::shared_ptr<Render_Domain>(new Render_Domain());
storage.domains.emplace(gpu_index, domain);
return domain;
}
Render_Domain::Render_Domain() {
tasks_.set_capacity(default_capacity);
thread_ = std::thread([this] { run(); });
}
Render_Domain::~Render_Domain() {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
slots_.acquire();
if (!tasks_.try_push(std::unique_ptr<Task>{}))
std::terminate();
if (thread_.joinable())
thread_.join();
}
void Render_Domain::post(std::function<void()> function) {
post(prepare(std::move(function)));
}
void Render_Domain::post(Prepared_Task task) noexcept {
if (!task.task_ || task.domain_ != this)
std::terminate();
std::lock_guard lock(mutex_);
if (stopping_)
std::terminate();
if (!tasks_.try_push(std::move(task.task_)))
std::terminate();
task.domain_ = nullptr;
}
void Render_Domain::run() {
for (;;) {
std::unique_ptr<Task> task;
tasks_.pop(task);
slots_.release();
if (!task)
return;
task->function();
}
}
}
+24 -43
View File
@@ -1,8 +1,10 @@
#pragma once
#include <cstdint>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <semaphore>
#include <stdexcept>
#include <thread>
#include <type_traits>
@@ -18,51 +20,36 @@ public:
class Prepared_Task final {
public:
Prepared_Task() = default;
~Prepared_Task() = default;
~Prepared_Task();
Prepared_Task(const Prepared_Task&) = delete;
Prepared_Task& operator=(const Prepared_Task&) = delete;
Prepared_Task(Prepared_Task&&) noexcept = default;
Prepared_Task& operator=(Prepared_Task&&) noexcept = default;
Prepared_Task(Prepared_Task&& other) noexcept;
Prepared_Task& operator=(Prepared_Task&& other) noexcept;
private:
explicit Prepared_Task(std::unique_ptr<Task> task) noexcept : task_(std::move(task)) {}
Prepared_Task(Render_Domain* domain, std::unique_ptr<Task> task) noexcept
: domain_(domain), task_(std::move(task)) {}
void release() noexcept;
Render_Domain* domain_{};
std::unique_ptr<Task> task_;
friend class Render_Domain;
};
Render_Domain() {
tasks_.set_capacity(default_capacity);
thread_ = std::thread([this] { run(); });
}
~Render_Domain() {
{
std::lock_guard lock(mutex_);
stopping_ = true;
tasks_.push(std::unique_ptr<Task>{});
}
if (thread_.joinable())
thread_.join();
}
static std::shared_ptr<Render_Domain> acquire(std::uint32_t gpu_index);
~Render_Domain();
Render_Domain(const Render_Domain&) = delete;
Render_Domain& operator=(const Render_Domain&) = delete;
[[nodiscard]] Prepared_Task prepare(std::function<void()> function) {
if (!function)
throw std::invalid_argument("render domain task is empty");
return Prepared_Task(std::make_unique<Task>(std::move(function)));
}
void post(std::function<void()> function) {
auto task = prepare(std::move(function));
std::lock_guard lock(mutex_);
if (stopping_)
throw std::runtime_error("Point_Scene render domain is stopping");
tasks_.push(std::move(task.task_));
}
void post(Prepared_Task task) noexcept {
if (!task.task_)
std::terminate();
std::lock_guard lock(mutex_);
if (stopping_)
std::terminate();
tasks_.push(std::move(task.task_));
slots_.acquire();
try {
return Prepared_Task(this, std::make_unique<Task>(std::move(function)));
} catch (...) {
slots_.release();
throw;
}
}
void post(std::function<void()> function);
void post(Prepared_Task task) noexcept;
template <class Function>
auto invoke(Function&& function) -> std::invoke_result_t<Function> {
using Result = std::invoke_result_t<Function>;
@@ -75,16 +62,10 @@ public:
return result.get();
}
private:
void run() {
for (;;) {
std::unique_ptr<Task> task;
tasks_.pop(task);
if (!task)
return;
task->function();
}
}
static constexpr std::size_t default_capacity = 64;
Render_Domain();
void run();
static constexpr std::ptrdiff_t default_capacity = 64;
std::counting_semaphore<default_capacity> slots_{default_capacity};
oneapi::tbb::concurrent_bounded_queue<std::unique_ptr<Task>> tasks_;
std::mutex mutex_;
bool stopping_{};
@@ -1,63 +1,48 @@
#include "render_3D/detail/Gpu_Completion_Service.h"
#include "render_3D/detail/Render_Domain.h"
#include <gtest/gtest.h>
#include <atomic>
#include <stdexcept>
#include <utility>
namespace renderive::render_3d::detail {
namespace {
static_assert(noexcept(
std::declval<Gpu_Completion_Service::Reservation&>().watch(
VK_NULL_HANDLE, VK_NULL_HANDLE)));
static_assert(noexcept(
std::declval<Render_Domain&>().post(
std::declval<Render_Domain::Prepared_Task>())));
TEST(GpuCompletionService, AbandonedReservationCancelsBeforeFenceWait) {
Gpu_Completion_Service service;
static_assert(noexcept(std::declval<Gpu_Completion_Service::Reservation&>().watch(VK_NULL_HANDLE, VK_NULL_HANDLE)));
static_assert(noexcept(std::declval<Render_Domain&>().post(std::declval<Render_Domain::Prepared_Task>())));
TEST(GpuCompletionService, UsesOneProcessWideService) {
EXPECT_EQ(&Gpu_Completion_Service::instance(), &Gpu_Completion_Service::instance());
}
TEST(GpuCompletionService, AbandonedReservationDoesNotComplete) {
std::atomic<int> completion_count{};
{
auto reservation = service.prepare(
auto reservation = Gpu_Completion_Service::instance().prepare(
[&](Gpu_Completion_Service::Result) {
completion_count.fetch_add(1, std::memory_order_relaxed);
},
false);
}, false);
}
service.shutdown();
EXPECT_EQ(completion_count.load(std::memory_order_relaxed), 0);
}
TEST(GpuCompletionService, PrepareRejectsAStoppedServiceBeforeSubmission) {
Gpu_Completion_Service service;
service.shutdown();
EXPECT_THROW({
auto reservation = service.prepare(
[](Gpu_Completion_Service::Result) {}, false);
static_cast<void>(reservation);
}, std::runtime_error);
TEST(RenderDomain, SharesDomainPerGpuIndex) {
auto first = Render_Domain::acquire(0);
auto second = Render_Domain::acquire(0);
auto other = Render_Domain::acquire(1);
EXPECT_EQ(first, second);
EXPECT_NE(first, other);
}
TEST(GpuCompletionService, ShutdownCancelsAnUnarmedReservation) {
Gpu_Completion_Service service;
auto reservation = service.prepare(
[](Gpu_Completion_Service::Result) {}, false);
service.shutdown();
}
TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) {
Render_Domain domain;
auto domain = Render_Domain::acquire(0);
std::atomic<bool> executed{};
auto task = domain.prepare([&] {
auto task = domain->prepare([&] {
executed.store(true, std::memory_order_release);
});
domain.post(std::move(task));
domain.invoke([] {});
domain->post(std::move(task));
domain->invoke([] {});
EXPECT_TRUE(executed.load(std::memory_order_acquire));
}
} // namespace
} // namespace renderive::render_3d::detail
TEST(RenderDomain, AbandonedPreparedTasksReleaseReservedCapacity) {
auto domain = Render_Domain::acquire(2);
for (std::size_t index = 0; index < 128; ++index) {
auto task = domain->prepare([] {});
}
domain->invoke([] {});
}
}
}