改进
This commit is contained in:
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,6 +5,7 @@ set(renderive_package_build_output
|
||||
set(renderive_package_excludes
|
||||
"^data/.*$"
|
||||
"^performance/.*$"
|
||||
"^renderive_package/.*$"
|
||||
"^third_party/datoviz/.*$"
|
||||
"^cmake-build-[^/]*/.*$"
|
||||
"^(.*/)?node_modules/.*$"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;}
|
||||
|
||||
Reference in New Issue
Block a user