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
@@ -104,26 +104,20 @@ bool Render_Graph_Builder::reaches(std::size_t from, std::size_t target) const {
std::shared_ptr<const Render_Plan> Render_Plan_History::publish(Render_Graph graph) {
normalize(graph);
std::lock_guard lock(mutex_);
const auto existing = std::find_if(plans_.begin(), plans_.end(), [&graph](const auto& plan) {
return same_topology(plan->graph, graph);
});
if (existing != plans_.end()) {
current_ = *existing;
return current_;
}
if (!plans_.empty() && same_topology(plans_.back()->graph, graph))
return plans_.back();
if (next_version_ == 0 || next_version_ == std::numeric_limits<Render_Plan_Version>::max())
throw std::overflow_error("render plan version exhausted");
auto plan = std::make_shared<Render_Plan>();
plan->version = next_version_++;
plan->graph = std::move(graph);
plans_.push_back(plan);
current_ = plan;
return plan;
}
std::shared_ptr<const Render_Plan> Render_Plan_History::current() const {
std::lock_guard lock(mutex_);
return current_;
return plans_.empty() ? nullptr : plans_.back();
}
std::shared_ptr<const Render_Plan> Render_Plan_History::find(Render_Plan_Version version) const {
@@ -68,7 +68,6 @@ public:
private:
mutable std::mutex mutex_;
std::vector<std::shared_ptr<const Render_Plan>> plans_;
std::shared_ptr<const Render_Plan> current_;
Render_Plan_Version next_version_{1};
};
+14 -34
View File
@@ -9,7 +9,6 @@
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -89,8 +88,6 @@ struct Scene_Base::Compiled_Render_Plan {
Renderable_Id id{};
std::shared_ptr<const Renderable_Graph> graph;
std::vector<Renderable_Id> dependency_parent_ids;
bool prepare_required{};
bool paint_required{};
};
std::vector<Renderable_Key> renderables;
std::vector<Renderable_Id> display_order;
@@ -978,9 +975,7 @@ std::shared_ptr<Scene_Base::Compiled_Render_Plan> Scene_Base::compile_render_pla
const auto& state = snapshot.renderables[index];
if (key.id != state.renderable_id ||
key.graph != state.render_graph ||
key.dependency_parent_ids != state.dependency_parent_ids ||
key.prepare_required != state.prepare_required ||
key.paint_required != state.paint_required)
key.dependency_parent_ids != state.dependency_parent_ids)
return false;
}
return true;
@@ -992,8 +987,7 @@ std::shared_ptr<Scene_Base::Compiled_Render_Plan> Scene_Base::compile_render_pla
compiled.renderables.reserve(snapshot.renderables.size());
for (const auto& state : snapshot.renderables) {
compiled.renderables.push_back({
state.renderable_id, state.render_graph, state.dependency_parent_ids,
state.prepare_required, state.paint_required});
state.renderable_id, state.render_graph, state.dependency_parent_ids});
}
compiled.display_order = snapshot.display_order;
};
@@ -1027,16 +1021,10 @@ std::shared_ptr<Scene_Base::Compiled_Render_Plan> Scene_Base::compile_render_pla
const auto& state = snapshot.renderables[item.snapshot_index];
if (!state.render_graph)
throw std::logic_error("frame renderable has no graph snapshot");
std::unordered_set<Render_Node_Id> selected;
for (const auto& node : state.render_graph->graph.nodes) {
if (node.kind == Render_Node_Kind::composite)
throw std::logic_error(
"renderable graph must not define composite nodes");
const bool include = node.kind == Render_Node_Kind::prepare
? state.prepare_required
: state.paint_required;
if (!include)
continue;
graph.nodes.push_back(node);
const auto& render_function =
state.render_graph->functions.at(node.execution_index);
@@ -1044,30 +1032,22 @@ std::shared_ptr<Scene_Base::Compiled_Render_Plan> Scene_Base::compile_render_pla
functions.emplace(node.node_id,
Execution_Binding{item.snapshot_index, Execution_Function{function}});
}, render_function);
selected.insert(node.node_id);
}
for (const auto& edge : state.render_graph->graph.edges) {
if (selected.contains(edge.from) && selected.contains(edge.to))
append_edge(edge.from, edge.to);
}
for (const auto& edge : state.render_graph->graph.edges)
append_edge(edge.from, edge.to);
const auto stage_boundary = [&](Render_Node_Kind kind, bool roots) {
std::vector<Render_Node_Id> result;
if ((kind == Render_Node_Kind::prepare &&
!state.prepare_required) ||
(kind == Render_Node_Kind::paint && !state.paint_required))
return result;
for (const auto& node : state.render_graph->graph.nodes) {
if (node.kind != kind || !selected.contains(node.node_id))
if (node.kind != kind)
continue;
const bool connected = std::any_of(
state.render_graph->graph.edges.begin(),
state.render_graph->graph.edges.end(),
[&](const Render_Edge& edge) {
const Render_Node_Id adjacent =
roots ? edge.from : edge.to;
const Render_Node_Id adjacent = roots ? edge.from : edge.to;
const bool matches = roots ? edge.to == node.node_id
: edge.from == node.node_id;
if (!matches || !selected.contains(adjacent))
if (!matches)
return false;
return std::any_of(
state.render_graph->graph.nodes.begin(),
@@ -1169,12 +1149,9 @@ std::shared_ptr<Scene_Base::Compiled_Render_Plan> Scene_Base::compile_render_pla
throw std::logic_error("render plan node has no execution binding");
execution_bindings[node.execution_index] = std::move(function->second);
}
if (const auto existing = compiled_render_plans_.find(plan->version);
existing != compiled_render_plans_.end()) {
auto& compiled = *existing->second;
update_key(compiled);
compiled.execution_bindings = std::move(execution_bindings);
compiled_render_plan_ = existing->second;
if (compiled_render_plan_ && compiled_render_plan_->plan == plan) {
update_key(*compiled_render_plan_);
compiled_render_plan_->execution_bindings = std::move(execution_bindings);
return compiled_render_plan_;
}
auto compiled = std::make_shared<Compiled_Render_Plan>();
@@ -1183,7 +1160,6 @@ std::shared_ptr<Scene_Base::Compiled_Render_Plan> Scene_Base::compile_render_pla
compiled->execution_bindings = std::move(execution_bindings);
compiled->runtime = std::make_unique<
renderive::render_graph::detail::Render_Graph_Runtime>(*compiled->plan);
compiled_render_plans_.emplace(compiled->plan->version, compiled);
compiled_render_plan_ = compiled;
return compiled;
}
@@ -1234,6 +1210,8 @@ void Scene_Base::execute_render_graph(Render_Task& task) {
case Render_Node_Kind::prepare: {
const auto& state = snapshot.renderables.at(
binding.renderable_index.value());
if (!state.prepare_required)
return Node_Execution_Result::completed();
std::get<Prepare_Render_Node_Function>(binding.function)(
Prepare_Render_Context{snapshot, state, metrics,
diagnostics});
@@ -1242,6 +1220,8 @@ void Scene_Base::execute_render_graph(Render_Task& task) {
case Render_Node_Kind::paint: {
const auto& state = snapshot.renderables.at(
binding.renderable_index.value());
if (!state.paint_required)
return Node_Execution_Result::completed();
if (!state.paint_buffer_)
throw std::logic_error(
"paint node has no frame color cache");
@@ -296,8 +296,6 @@ private:
const Render_Node_Id composite_begin_node_id_;
const Render_Node_Id scene_render_node_id_;
Render_Plan_History render_plan_history_;
std::unordered_map<Render_Plan_Version, std::shared_ptr<Compiled_Render_Plan>>
compiled_render_plans_;
std::shared_ptr<Compiled_Render_Plan> compiled_render_plan_;
Capture_Controller capture_controller_;
Capture_Repository capture_repository_;
@@ -54,11 +54,12 @@ TEST(render_dag_test, one_graph_assigns_dense_execution_slots_and_versions_only_
changed_graph.edges.push_back({100, 101});
const auto changed = history.publish(std::move(changed_graph));
EXPECT_GT(changed->version, first->version);
const auto reused = history.publish(make_parallel_graph());
EXPECT_EQ(reused, first);
EXPECT_EQ(history.current(), first);
const auto restored = history.publish(make_parallel_graph());
EXPECT_GT(restored->version, changed->version);
EXPECT_EQ(history.current(), restored);
EXPECT_EQ(history.find(first->version), first);
EXPECT_EQ(history.find(changed->version), changed);
EXPECT_EQ(history.find(restored->version), restored);
}
TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_from_frame_slots) {
@@ -191,7 +191,7 @@ TEST(render_plan_execution_test, dependent_prepare_does_not_wait_for_dependency_
}
#endif
TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independent_and_prune_nodes) {
TEST(render_plan_execution_test, prepare_and_paint_cache_validity_do_not_recompile_render_plan) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Pipeline_Renderable>::make(true);
attach_initial(scene, renderable);
@@ -199,6 +199,8 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen
scene.wait_for_render();
EXPECT_TRUE(renderable->prepare_cache_valid_for_test());
EXPECT_TRUE(renderable->paint_cache_valid_for_test());
EXPECT_EQ(renderable->prepare_count.load(), 1);
EXPECT_EQ(renderable->paint_count.load(), 1);
const auto first_plan = scene.render_plan_snapshot();
const Render_Node_Id prepare_id = node(*first_plan, renderable->renderable_id(),
Render_Node_Kind::prepare).node_id;
@@ -208,12 +210,13 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen
scene.render();
scene.wait_for_render();
const auto cached_plan = scene.render_plan_snapshot();
EXPECT_GT(cached_plan->version, first_plan->version);
EXPECT_EQ(std::count_if(cached_plan->graph.nodes.begin(), cached_plan->graph.nodes.end(),
[&](const Render_Node& value) {
return value.owner_id == renderable->renderable_id() &&
value.kind != Render_Node_Kind::composite;
}), 0);
EXPECT_EQ(cached_plan, first_plan);
EXPECT_EQ(renderable->prepare_count.load(), 1);
EXPECT_EQ(renderable->paint_count.load(), 1);
EXPECT_EQ(node(*cached_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id,
prepare_id);
EXPECT_EQ(node(*cached_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id,
paint_id);
renderable->invalidate_paint_for_test();
EXPECT_TRUE(renderable->prepare_cache_valid_for_test());
@@ -221,14 +224,13 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen
scene.render();
scene.wait_for_render();
const auto paint_plan = scene.render_plan_snapshot();
EXPECT_GT(paint_plan->version, cached_plan->version);
EXPECT_EQ(paint_plan, first_plan);
EXPECT_EQ(renderable->prepare_count.load(), 1);
EXPECT_EQ(renderable->paint_count.load(), 2);
EXPECT_EQ(node(*paint_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id,
prepare_id);
EXPECT_EQ(node(*paint_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id,
paint_id);
EXPECT_EQ(std::count_if(paint_plan->graph.nodes.begin(), paint_plan->graph.nodes.end(),
[&](const Render_Node& value) {
return value.owner_id == renderable->renderable_id() &&
value.kind == Render_Node_Kind::prepare;
}), 0);
renderable->invalidate_prepare_for_test();
EXPECT_FALSE(renderable->prepare_cache_valid_for_test());
@@ -236,7 +238,9 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen
scene.render();
scene.wait_for_render();
const auto rebuilt_plan = scene.render_plan_snapshot();
EXPECT_GT(rebuilt_plan->version, paint_plan->version);
EXPECT_EQ(rebuilt_plan, first_plan);
EXPECT_EQ(renderable->prepare_count.load(), 2);
EXPECT_EQ(renderable->paint_count.load(), 3);
EXPECT_EQ(node(*rebuilt_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id,
prepare_id);
EXPECT_EQ(node(*rebuilt_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id,
+1
View File
@@ -5,6 +5,7 @@ set(renderive_package_build_output
set(renderive_package_excludes
"^data/.*$"
"^performance/.*$"
"^renderive_package/.*$"
"^third_party/datoviz/.*$"
"^cmake-build-[^/]*/.*$"
"^(.*/)?node_modules/.*$"
+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([] {});
}
}
}
@@ -1,2 +1,2 @@
import {Stack,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {Json_Viewer} from "../common/json_viewer";
export function Performance_Panel({telemetry}:{telemetry:Gallery_Telemetry}) {return <Stack spacing={1}><Typography variant="h6"></Typography><Json_Viewer value={{performance:telemetry.performance,client_performance:telemetry.client_performance,consumer_feedback:telemetry.consumer_feedback}}/></Stack>;}
export function Performance_Panel({telemetry}:{telemetry:Gallery_Telemetry}) {return <Stack spacing={1}><Typography variant="h6"></Typography><Json_Viewer value={{performance:telemetry.performance,scheduler:telemetry.scheduler,client_performance:telemetry.client_performance,consumer_feedback:telemetry.consumer_feedback}}/></Stack>;}
@@ -1,3 +1,3 @@
import {Box,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {value_at_path} from "../protocol/format";
const METRICS:Array<[string,string,string]>=[["后端 FPS","performance.measured_fps"," FPS"],["像素 FPS","performance.pixel_response_fps"," FPS"],["渲染","performance.last_render_ms"," ms"],["编码","performance.last_pixel_encode_ms"," ms"]];
const METRICS:Array<[string,string,string]>=[["后端 FPS","performance.measured_fps"," FPS"],["像素 FPS","performance.pixel_response_fps"," FPS"],["帧完成延迟","performance.last_render_ms"," ms"],["编码","performance.last_pixel_encode_ms"," ms"]];
export function Performance_Strip({telemetry}:{telemetry:Gallery_Telemetry}) {return <Box sx={{display:"grid",gridTemplateColumns:"repeat(4,1fr)",gap:1,p:1.5,borderTop:"1px solid rgba(132,179,206,.12)"}}>{METRICS.map(([label,path,suffix])=><Box key={path}><Typography variant="caption" color="text.secondary">{label}</Typography><Typography fontFamily="monospace">{Number(value_at_path(telemetry,path)||0).toFixed(1)}{suffix}</Typography></Box>)}</Box>;}