修复若干问题

This commit is contained in:
2026-08-15 02:51:11 +08:00
parent 282768e0ea
commit 04a9156cd6
25 changed files with 631 additions and 176 deletions
@@ -42,8 +42,12 @@ void External_Operation::on_complete(Completion completion) const {
error = state_->error; error = state_->error;
invoke = true; invoke = true;
} }
if (invoke) if (invoke) {
completion(std::move(error)); try {
completion(std::move(error));
} catch (...) {
}
}
} }
External_Operation::operator bool() const noexcept { External_Operation::operator bool() const noexcept {
@@ -13,6 +13,9 @@ public:
External_Operation() = default; External_Operation() = default;
// Completion callbacks are notification boundaries. Exceptions thrown by
// them are contained regardless of whether completion happened before or
// after subscription.
void on_complete(Completion completion) const; void on_complete(Completion completion) const;
[[nodiscard]] explicit operator bool() const noexcept; [[nodiscard]] explicit operator bool() const noexcept;
@@ -26,6 +26,8 @@ double percentile(std::vector<std::uint64_t> values, double probability) {
struct Node_Samples { struct Node_Samples {
std::vector<std::uint64_t> durations; std::vector<std::uint64_t> durations;
std::vector<std::uint64_t> cpu_durations;
std::vector<std::uint64_t> external_durations;
std::vector<std::uint64_t> waits; std::vector<std::uint64_t> waits;
std::size_t critical_count{}; std::size_t critical_count{};
}; };
@@ -50,6 +52,18 @@ Node_Statistics statistics_for(Render_Node_Id id, const Node_Samples& samples) {
result.p99_ns = percentile(samples.durations, 0.99); result.p99_ns = percentile(samples.durations, 0.99);
result.minimum_ns = *std::min_element(samples.durations.begin(), samples.durations.end()); result.minimum_ns = *std::min_element(samples.durations.begin(), samples.durations.end());
result.maximum_ns = *std::max_element(samples.durations.begin(), samples.durations.end()); result.maximum_ns = *std::max_element(samples.durations.begin(), samples.durations.end());
const auto cpu_sum = std::accumulate(
samples.cpu_durations.begin(), samples.cpu_durations.end(),
std::uint64_t{});
const auto external_sum = std::accumulate(
samples.external_durations.begin(), samples.external_durations.end(),
std::uint64_t{});
result.average_cpu_ns = static_cast<double>(cpu_sum) /
samples.cpu_durations.size();
result.average_external_ns = static_cast<double>(external_sum) /
samples.external_durations.size();
result.p95_cpu_ns = percentile(samples.cpu_durations, 0.95);
result.p95_external_ns = percentile(samples.external_durations, 0.95);
result.critical_path_frequency = samples.critical_count; result.critical_path_frequency = samples.critical_count;
result.average_scheduler_wait_ns = samples.waits.empty() result.average_scheduler_wait_ns = samples.waits.empty()
? 0.0 ? 0.0
@@ -227,6 +241,8 @@ std::vector<Node_Statistics> analyze_node_statistics(
for (const auto& node : frame.nodes) { for (const auto& node : frame.nodes) {
auto& values = samples[node.node_id]; auto& values = samples[node.node_id];
values.durations.push_back(node.duration_ns); values.durations.push_back(node.duration_ns);
values.cpu_durations.push_back(node.cpu_duration_ns);
values.external_durations.push_back(node.external_duration_ns);
values.waits.push_back(node.scheduler_wait_ns); values.waits.push_back(node.scheduler_wait_ns);
values.critical_count += node.on_critical_path ? 1 : 0; values.critical_count += node.on_critical_path ? 1 : 0;
} }
@@ -52,6 +52,10 @@ struct Node_Statistics {
double p99_ns{}; double p99_ns{};
std::uint64_t minimum_ns{}; std::uint64_t minimum_ns{};
std::uint64_t maximum_ns{}; std::uint64_t maximum_ns{};
double average_cpu_ns{};
double average_external_ns{};
double p95_cpu_ns{};
double p95_external_ns{};
std::size_t critical_path_frequency{}; std::size_t critical_path_frequency{};
double average_scheduler_wait_ns{}; double average_scheduler_wait_ns{};
}; };
@@ -0,0 +1,37 @@
#pragma once
#include <string>
#include <utility>
#include <vector>
struct Node_Diagnostic_Attachment {
std::string type;
std::string content;
};
class Scene_Base;
class Node_Diagnostic_Sink {
public:
Node_Diagnostic_Sink() = default;
[[nodiscard]] bool enabled() const noexcept {
return attachments_ != nullptr;
}
void attach(std::string type, std::string content) const {
if (!attachments_)
return;
attachments_->push_back(
{std::move(type), std::move(content)});
}
private:
explicit Node_Diagnostic_Sink(
std::vector<Node_Diagnostic_Attachment>& attachments) noexcept
: attachments_(&attachments) {}
std::vector<Node_Diagnostic_Attachment>* attachments_{};
friend class Scene_Base;
};
@@ -4,6 +4,7 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <vector> #include <vector>
#include "renderive/render_graph/Node_Diagnostics.hpp"
#include "renderive/render_graph/Render_Plan.hpp" #include "renderive/render_graph/Render_Plan.hpp"
enum class Node_Execution_Status : std::uint8_t { enum class Node_Execution_Status : std::uint8_t {
@@ -28,7 +29,9 @@ enum class Node_Metric_Kind : std::uint8_t {
submit_duration_ns, submit_duration_ns,
gpu_fence_wait_ns, gpu_fence_wait_ns,
gpu_render_duration_ns, gpu_render_duration_ns,
gpu_transition_duration_ns,
gpu_copy_duration_ns, gpu_copy_duration_ns,
gpu_total_duration_ns,
readback_duration_ns, readback_duration_ns,
backend_resource_version, backend_resource_version,
backend_frame_index, backend_frame_index,
@@ -60,6 +63,7 @@ struct Node_Execution {
std::uint32_t worker_id{}; std::uint32_t worker_id{};
Node_Execution_Status status{Node_Execution_Status::pending}; Node_Execution_Status status{Node_Execution_Status::pending};
Node_Execution_Metrics metrics; Node_Execution_Metrics metrics;
std::vector<Node_Diagnostic_Attachment> attachments;
[[nodiscard]] std::uint64_t duration_ns() const noexcept; [[nodiscard]] std::uint64_t duration_ns() const noexcept;
[[nodiscard]] std::uint64_t cpu_duration_ns() const noexcept; [[nodiscard]] std::uint64_t cpu_duration_ns() const noexcept;
[[nodiscard]] std::uint64_t external_duration_ns() const noexcept; [[nodiscard]] std::uint64_t external_duration_ns() const noexcept;
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include "renderive/render_graph/Node_Diagnostics.hpp"
#include "Frame_Render_Snapshot.hpp" #include "Frame_Render_Snapshot.hpp"
class Color_Cache; class Color_Cache;
@@ -10,6 +11,7 @@ struct Prepare_Render_Context {
const Frame_Render_Snapshot& frame; const Frame_Render_Snapshot& frame;
const Renderable_Frame_State& renderable; const Renderable_Frame_State& renderable;
Node_Execution_Metrics* metrics{}; Node_Execution_Metrics* metrics{};
Node_Diagnostic_Sink diagnostics;
template <class Data> template <class Data>
void publish(std::shared_ptr<const Data> data) const { void publish(std::shared_ptr<const Data> data) const {
@@ -27,6 +29,7 @@ struct Paint_Render_Context {
const Renderable_Frame_State& renderable; const Renderable_Frame_State& renderable;
Color_Cache& color_cache; Color_Cache& color_cache;
Node_Execution_Metrics* metrics{}; Node_Execution_Metrics* metrics{};
Node_Diagnostic_Sink diagnostics;
}; };
struct Composite_Render_Context { struct Composite_Render_Context {
@@ -34,11 +37,13 @@ struct Composite_Render_Context {
const Renderable_Frame_State* renderable{}; const Renderable_Frame_State* renderable{};
const Color_Cache* color_cache{}; const Color_Cache* color_cache{};
Node_Execution_Metrics* metrics{}; Node_Execution_Metrics* metrics{};
Node_Diagnostic_Sink diagnostics;
}; };
struct Scene_Render_Context { struct Scene_Render_Context {
const Frame_Render_Snapshot& frame; const Frame_Render_Snapshot& frame;
Node_Execution_Metrics* metrics{}; Node_Execution_Metrics* metrics{};
Node_Diagnostic_Sink diagnostics;
template <class Data> template <class Data>
[[nodiscard]] std::shared_ptr<const Data> prepared( [[nodiscard]] std::shared_ptr<const Data> prepared(
+10 -5
View File
@@ -1162,19 +1162,23 @@ void Scene_Base::execute_render_graph(Render_Task& task) {
auto& executor = Execution_Context::executor(); auto& executor = Execution_Context::executor();
renderive::render_graph::detail::Render_Graph_Runtime runtime( renderive::render_graph::detail::Render_Graph_Runtime runtime(
*task.plan, execution_slots, *task.plan, execution_slots,
[this, &task, &snapshot]( [this, &task, &snapshot, &execution_slots](
std::size_t execution_index, std::size_t execution_index,
Node_Execution_Metrics* metrics) -> Node_Execution_Result { Node_Execution_Metrics* metrics) -> Node_Execution_Result {
const auto& node = task.plan->graph.nodes.at(execution_index); const auto& node = task.plan->graph.nodes.at(execution_index);
const auto& binding = const auto& binding =
task.execution_bindings.at(execution_index); task.execution_bindings.at(execution_index);
Node_Diagnostic_Sink diagnostics;
if (auto* execution = execution_slots.at(execution_index))
diagnostics = Node_Diagnostic_Sink(execution->attachments);
Render_Execution_Scope scope(*this); Render_Execution_Scope scope(*this);
switch (node.kind) { switch (node.kind) {
case Render_Node_Kind::prepare: { case Render_Node_Kind::prepare: {
const auto& state = snapshot.renderables.at( const auto& state = snapshot.renderables.at(
binding.renderable_index.value()); binding.renderable_index.value());
std::get<Prepare_Render_Node_Function>(binding.function)( std::get<Prepare_Render_Node_Function>(binding.function)(
Prepare_Render_Context{snapshot, state, metrics}); Prepare_Render_Context{snapshot, state, metrics,
diagnostics});
return Node_Execution_Result::completed(); return Node_Execution_Result::completed();
} }
case Render_Node_Kind::paint: { case Render_Node_Kind::paint: {
@@ -1185,7 +1189,8 @@ void Scene_Base::execute_render_graph(Render_Task& task) {
"paint node has no frame color cache"); "paint node has no frame color cache");
std::get<Paint_Render_Node_Function>(binding.function)( std::get<Paint_Render_Node_Function>(binding.function)(
Paint_Render_Context{snapshot, state, Paint_Render_Context{snapshot, state,
*state.paint_buffer_, metrics}); *state.paint_buffer_, metrics,
diagnostics});
return Node_Execution_Result::completed(); return Node_Execution_Result::completed();
} }
case Render_Node_Kind::composite: { case Render_Node_Kind::composite: {
@@ -1199,12 +1204,12 @@ void Scene_Base::execute_render_graph(Render_Task& task) {
: nullptr; : nullptr;
std::get<Composite_Render_Node_Function>(binding.function)( std::get<Composite_Render_Node_Function>(binding.function)(
Composite_Render_Context{snapshot, state, cache, Composite_Render_Context{snapshot, state, cache,
metrics}); metrics, diagnostics});
return Node_Execution_Result::completed(); return Node_Execution_Result::completed();
} }
case Render_Node_Kind::render: case Render_Node_Kind::render:
return std::get<Scene_Render_Node_Function>(binding.function)( return std::get<Scene_Render_Node_Function>(binding.function)(
Scene_Render_Context{snapshot, metrics}); Scene_Render_Context{snapshot, metrics, diagnostics});
} }
throw std::logic_error("unknown render node kind"); throw std::logic_error("unknown render node kind");
}, },
@@ -181,6 +181,13 @@ TEST(render_dag_test,
ASSERT_EQ(analysis.workers.size(), 1U); ASSERT_EQ(analysis.workers.size(), 1U);
EXPECT_EQ(analysis.workers.front().work_duration_ns, 20U); EXPECT_EQ(analysis.workers.front().work_duration_ns, 20U);
EXPECT_DOUBLE_EQ(analysis.workers.front().utilization, 0.1); EXPECT_DOUBLE_EQ(analysis.workers.front().utilization, 0.1);
const auto statistics = analyze_node_statistics(
std::span<const Frame_Analysis>(&analysis, 1));
ASSERT_EQ(statistics.size(), 1U);
EXPECT_DOUBLE_EQ(statistics.front().average_cpu_ns, 20.0);
EXPECT_DOUBLE_EQ(statistics.front().average_external_ns, 180.0);
EXPECT_DOUBLE_EQ(statistics.front().p95_cpu_ns, 20.0);
EXPECT_DOUBLE_EQ(statistics.front().p95_external_ns, 180.0);
} }
TEST(render_dag_test, failed_capture_ticket_is_retried_until_the_requested_frame_completes) { TEST(render_dag_test, failed_capture_ticket_is_retried_until_the_requested_frame_completes) {
@@ -106,6 +106,21 @@ TEST(external_operation_test, completion_before_subscription_is_delivered_once)
EXPECT_EQ(completion_count, 1); EXPECT_EQ(completion_count, 1);
} }
TEST(external_operation_test,
callback_exceptions_are_contained_for_both_completion_orders) {
External_Operation_Source completed_first;
const auto completed_operation = completed_first.operation();
ASSERT_TRUE(completed_first.complete());
EXPECT_NO_THROW(completed_operation.on_complete(
[](std::exception_ptr) { throw std::runtime_error("late callback"); }));
External_Operation_Source subscribed_first;
const auto subscribed_operation = subscribed_first.operation();
subscribed_operation.on_complete(
[](std::exception_ptr) { throw std::runtime_error("early callback"); });
EXPECT_TRUE(subscribed_first.complete());
}
TEST(render_graph_runtime_test, TEST(render_graph_runtime_test,
external_successor_stays_blocked_until_operation_completes) { external_successor_stays_blocked_until_operation_completes) {
const auto plan = two_node_plan(); const auto plan = two_node_plan();
+3
View File
@@ -121,6 +121,9 @@ if (RENDERIVE_BUILD_TESTS)
set(Renderive_render_3D_test_target "Renderive_render_3D_${Renderive_render_3D_test_name}_${Renderive_render_3D_test_hash}") set(Renderive_render_3D_test_target "Renderive_render_3D_${Renderive_render_3D_test_name}_${Renderive_render_3D_test_hash}")
add_executable("${Renderive_render_3D_test_target}" "${Renderive_render_3D_test_source}") add_executable("${Renderive_render_3D_test_target}" "${Renderive_render_3D_test_source}")
target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE Renderive_render_3D GTest::gtest_main) target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE Renderive_render_3D GTest::gtest_main)
if (Renderive_render_3D_test_name STREQUAL "Gpu_Completion_Service_Tests")
target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE volk::volk_headers)
endif ()
renderive_stage_render_3D_runtime("${Renderive_render_3D_test_target}") renderive_stage_render_3D_runtime("${Renderive_render_3D_test_target}")
add_test(NAME "${Renderive_render_3D_test_target}" COMMAND "${Renderive_render_3D_test_target}") add_test(NAME "${Renderive_render_3D_test_target}" COMMAND "${Renderive_render_3D_test_target}")
endforeach () endforeach ()
+130 -102
View File
@@ -15,7 +15,9 @@
#include <exception> #include <exception>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional>
#include <stdexcept> #include <stdexcept>
#include <type_traits>
#include <utility> #include <utility>
namespace renderive::render_3d { namespace renderive::render_3d {
@@ -41,38 +43,46 @@ float datoviz_wheel_step(float pixel_delta, float angle_delta) noexcept {
} }
void publish_trace(Node_Execution_Metrics* metrics, void publish_trace(Node_Execution_Metrics* metrics,
const detail::Datoviz_Frame_Trace& trace) { Node_Diagnostic_Sink diagnostics,
if (!metrics) detail::Datoviz_Frame_Trace trace) {
return; if (metrics) {
metrics->set(Node_Metric_Kind::queue_wait_ns, metrics->set(Node_Metric_Kind::queue_wait_ns,
trace.render_domain_queue_wait_ns); trace.render_domain_queue_wait_ns);
metrics->set(Node_Metric_Kind::apply_duration_ns, trace.apply_ns); metrics->set(Node_Metric_Kind::apply_duration_ns, trace.apply_ns);
metrics->set(Node_Metric_Kind::plan_emit_duration_ns, trace.emit_ns); metrics->set(Node_Metric_Kind::plan_emit_duration_ns, trace.emit_ns);
metrics->set(Node_Metric_Kind::backend_execute_duration_ns, metrics->set(Node_Metric_Kind::backend_execute_duration_ns,
trace.execute_ns); trace.execute_ns);
metrics->set(Node_Metric_Kind::submit_duration_ns, trace.submit_ns); metrics->set(Node_Metric_Kind::submit_duration_ns, trace.submit_ns);
metrics->set(Node_Metric_Kind::gpu_fence_wait_ns, metrics->set(Node_Metric_Kind::gpu_fence_wait_ns,
trace.gpu_fence_wait_ns); trace.gpu_fence_wait_ns);
metrics->set(Node_Metric_Kind::readback_duration_ns, metrics->set(Node_Metric_Kind::readback_duration_ns,
trace.readback_ns); trace.readback_ns);
if (trace.gpu) { if (trace.gpu) {
metrics->set(Node_Metric_Kind::gpu_render_duration_ns, metrics->set(Node_Metric_Kind::gpu_render_duration_ns,
trace.gpu->render_ns); trace.gpu->render_ns);
metrics->set(Node_Metric_Kind::gpu_copy_duration_ns, metrics->set(Node_Metric_Kind::gpu_transition_duration_ns,
trace.gpu->copy_ns); trace.gpu->transition_ns);
metrics->set(Node_Metric_Kind::gpu_copy_duration_ns,
trace.gpu->copy_ns);
metrics->set(Node_Metric_Kind::gpu_total_duration_ns,
trace.gpu->total_ns);
}
metrics->set(Node_Metric_Kind::backend_resource_version,
trace.artifact_resource_version);
metrics->set(Node_Metric_Kind::backend_frame_index,
trace.artifact_frame_index);
metrics->set(Node_Metric_Kind::backend_artifact_status,
trace.artifact_status);
metrics->set(Node_Metric_Kind::backend_validation_code,
trace.validation_code);
metrics->set(Node_Metric_Kind::backend_validation_command,
trace.validation_command_index);
metrics->set(Node_Metric_Kind::backend_artifact_json_bytes,
trace.artifact_json.size());
} }
metrics->set(Node_Metric_Kind::backend_resource_version, if (!trace.artifact_json.empty())
trace.artifact_resource_version); diagnostics.attach("datoviz.drp2.artifact.json",
metrics->set(Node_Metric_Kind::backend_frame_index, std::move(trace.artifact_json));
trace.artifact_frame_index);
metrics->set(Node_Metric_Kind::backend_artifact_status,
trace.artifact_status);
metrics->set(Node_Metric_Kind::backend_validation_code,
trace.validation_code);
metrics->set(Node_Metric_Kind::backend_validation_command,
trace.validation_command_index);
metrics->set(Node_Metric_Kind::backend_artifact_json_bytes,
trace.artifact_json.size());
} }
struct Scene_Model { struct Scene_Model {
@@ -301,22 +311,100 @@ struct Basic_Point_Scene final
const std::uint64_t frame_sequence = const std::uint64_t frame_sequence =
context.frame.render_sequence; context.frame.render_sequence;
Node_Execution_Metrics* const metrics = context.metrics; Node_Execution_Metrics* const metrics = context.metrics;
const Node_Diagnostic_Sink diagnostics = context.diagnostics;
const bool observe = metrics != nullptr;
auto source = std::make_shared<External_Operation_Source>(); auto source = std::make_shared<External_Operation_Source>();
const auto operation = source->operation(); const auto operation = source->operation();
const auto queued_at = std::chrono::steady_clock::now(); using Pending_Frame = detail::Datoviz_Visual_Backend::Pending_Frame;
static_assert(std::is_nothrow_move_constructible_v<Pending_Frame>);
using Completion_Result = detail::Gpu_Completion_Service::Result;
static_assert(std::is_nothrow_move_assignable_v<Completion_Result>);
struct Async_Frame_State {
std::optional<Pending_Frame> pending;
Completion_Result completion;
};
auto async_frame = std::make_shared<Async_Frame_State>();
auto render_completion = std::make_shared<
detail::Render_Domain::Prepared_Task>(
render_domain.prepare(
[this, async_frame, metrics, diagnostics, source]() mutable {
if (!async_frame->pending) {
static_cast<void>(source->fail(
std::make_exception_ptr(std::logic_error(
"GPU completion has no pending frame"))));
return;
}
auto pending = std::move(*async_frame->pending);
async_frame->pending.reset();
pending.trace.gpu_fence_wait_ns =
async_frame->completion.wait_duration_ns;
if (async_frame->completion.error) {
try {
backend->discard(std::move(pending));
static_cast<void>(source->fail(
std::move(async_frame->completion.error)));
} catch (...) {
static_cast<void>(source->fail(
std::current_exception()));
}
return;
}
try {
auto completed = backend->collect(
std::move(pending));
auto frame = std::move(completed.frame);
if (metrics && frame) {
metrics->set(
Node_Metric_Kind::pixel_count,
static_cast<std::uint64_t>(
frame->extent.width) *
frame->extent.height);
}
publish_trace(metrics, diagnostics,
std::move(completed.trace));
{
std::lock_guard lock(frame_mutex);
latest = std::move(frame);
}
static_cast<void>(source->complete());
} catch (...) {
static_cast<void>(source->fail(
std::current_exception()));
}
}));
auto completion = std::make_shared<
detail::Gpu_Completion_Service::Reservation>(
gpu_completion.prepare(
[this, async_frame, render_completion](
Completion_Result result) noexcept {
async_frame->completion = std::move(result);
render_domain.post(std::move(*render_completion));
},
observe));
const auto queued_at = observe
? std::chrono::steady_clock::now()
: std::chrono::steady_clock::time_point{};
try { try {
render_domain.post( render_domain.post(
[this, scene_state, scene_revision, frame_sequence, prepared, [this, scene_state, scene_revision, frame_sequence, prepared,
metrics, source, queued_at] { source, async_frame, completion, observe,
queued_at] {
try { try {
const auto queue_wait = std::uint64_t queue_wait_ns{};
std::chrono::duration_cast<std::chrono::nanoseconds>( if (observe) {
std::chrono::steady_clock::now() - queued_at) const auto queue_wait =
.count(); std::chrono::duration_cast<
std::chrono::nanoseconds>(
std::chrono::steady_clock::now() -
queued_at).count();
queue_wait_ns = queue_wait > 0
? static_cast<std::uint64_t>(queue_wait)
: 0;
}
auto pending = backend->submit( auto pending = backend->submit(
scene_state, scene_revision, *prepared, scene_state, scene_revision, *prepared,
frame_sequence, metrics != nullptr); frame_sequence, observe);
if (!pending) { if (!pending) {
{ {
std::lock_guard lock(frame_mutex); std::lock_guard lock(frame_mutex);
@@ -326,70 +414,10 @@ struct Basic_Point_Scene final
return; return;
} }
pending->trace.render_domain_queue_wait_ns = pending->trace.render_domain_queue_wait_ns =
queue_wait > 0 queue_wait_ns;
? static_cast<std::uint64_t>(queue_wait) auto& owned = async_frame->pending.emplace(
: 0; std::move(*pending));
gpu_completion.watch( completion->watch(owned.device, owned.fence);
pending->device, pending->fence,
[this, pending = *pending, metrics, source](
detail::Gpu_Completion_Service::Result result)
mutable {
pending.trace.gpu_fence_wait_ns =
result.wait_duration_ns;
try {
render_domain.post(
[this, pending = std::move(pending),
metrics, source,
wait_error = std::move(result.error)]()
mutable {
if (wait_error) {
try {
backend->discard(
std::move(pending));
static_cast<void>(
source->fail(std::move(
wait_error)));
} catch (...) {
static_cast<void>(
source->fail(
std::current_exception()));
}
return;
}
try {
auto completed =
backend->collect(
std::move(pending));
auto frame =
std::move(completed.frame);
if (metrics && frame) {
metrics->set(
Node_Metric_Kind::
pixel_count,
static_cast<
std::uint64_t>(
frame->extent.width) *
frame->extent.height);
}
publish_trace(
metrics, completed.trace);
{
std::lock_guard lock(
frame_mutex);
latest = std::move(frame);
}
static_cast<void>(
source->complete());
} catch (...) {
static_cast<void>(source->fail(
std::current_exception()));
}
});
} catch (...) {
static_cast<void>(source->fail(
std::current_exception()));
}
});
} catch (...) { } catch (...) {
static_cast<void>( static_cast<void>(
source->fail(std::current_exception())); source->fail(std::current_exception()));
@@ -8,11 +8,14 @@ namespace renderive::render_3d::detail {
struct Datoviz_Gpu_Timing { struct Datoviz_Gpu_Timing {
std::uint64_t render_ns{}; std::uint64_t render_ns{};
std::uint64_t transition_ns{};
std::uint64_t copy_ns{}; std::uint64_t copy_ns{};
std::uint64_t total_ns{};
}; };
struct Datoviz_Frame_Trace { struct Datoviz_Frame_Trace {
std::uint64_t render_sequence{}; std::uint64_t render_sequence{};
bool observed{};
std::uint64_t render_domain_queue_wait_ns{}; std::uint64_t render_domain_queue_wait_ns{};
std::uint64_t apply_ns{}; std::uint64_t apply_ns{};
std::uint64_t emit_ns{}; std::uint64_t emit_ns{};
@@ -238,7 +238,6 @@ public:
dvz_buffer_usage(readback_, VK_BUFFER_USAGE_TRANSFER_DST_BIT); dvz_buffer_usage(readback_, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
if (dvz_buffer_create(readback_) != 0) if (dvz_buffer_create(readback_) != 0)
throw std::runtime_error("failed to create Datoviz readback buffer"); throw std::runtime_error("failed to create Datoviz readback buffer");
initialize_timestamps(device, queue);
} catch (...) { } catch (...) {
destroy(); destroy();
throw; throw;
@@ -247,9 +246,15 @@ public:
~Frame_Target() { destroy(); } ~Frame_Target() { destroy(); }
void begin() { void begin(bool observe) {
if (in_flight_) if (in_flight_)
throw std::logic_error("Datoviz frame target is still in flight"); throw std::logic_error("Datoviz frame target is still in flight");
observing_ = observe;
if (observing_ && !timestamps_initialized_) {
initialize_timestamps(
dvz_gpu_ctx_device(gpu_context_),
dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN));
}
dvz_cmd_reset(commands_); dvz_cmd_reset(commands_);
if (dvz_cmd_begin_result(commands_) != 0) if (dvz_cmd_begin_result(commands_) != 0)
throw std::runtime_error("failed to begin Datoviz command buffer"); throw std::runtime_error("failed to begin Datoviz command buffer");
@@ -278,7 +283,7 @@ public:
dvz_barrier_image_mip(image_barrier, 0, 1); dvz_barrier_image_mip(image_barrier, 0, 1);
dvz_barrier_image_layers(image_barrier, 0, 1); dvz_barrier_image_layers(image_barrier, 0, 1);
dvz_cmd_barriers(commands_, &barriers); dvz_cmd_barriers(commands_, &barriers);
if (timestamps_supported_) { if (observing_ && timestamps_supported_) {
const VkCommandBuffer command_buffer = const VkCommandBuffer command_buffer =
dvz_commands_handle(commands_); dvz_commands_handle(commands_);
vkCmdResetQueryPool(command_buffer, query_pool_, 0, 4); vkCmdResetQueryPool(command_buffer, query_pool_, 0, 4);
@@ -315,7 +320,7 @@ public:
if (!recording_) if (!recording_)
throw std::logic_error("Datoviz frame target is not recording"); throw std::logic_error("Datoviz frame target is not recording");
const VkCommandBuffer command_buffer = dvz_commands_handle(commands_); const VkCommandBuffer command_buffer = dvz_commands_handle(commands_);
if (timestamps_supported_) { if (observing_ && timestamps_supported_) {
vkCmdWriteTimestamp( vkCmdWriteTimestamp(
command_buffer, command_buffer,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
@@ -338,7 +343,7 @@ public:
dvz_barrier_image_mip(image_barrier, 0, 1); dvz_barrier_image_mip(image_barrier, 0, 1);
dvz_barrier_image_layers(image_barrier, 0, 1); dvz_barrier_image_layers(image_barrier, 0, 1);
dvz_cmd_barriers(commands_, &image_barriers); dvz_cmd_barriers(commands_, &image_barriers);
if (timestamps_supported_) { if (observing_ && timestamps_supported_) {
vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT,
query_pool_, 2); query_pool_, 2);
} }
@@ -360,7 +365,7 @@ public:
dvz_barrier_buffer_access(buffer_barrier, VK_ACCESS_2_TRANSFER_WRITE_BIT, dvz_barrier_buffer_access(buffer_barrier, VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_ACCESS_2_HOST_READ_BIT); VK_ACCESS_2_HOST_READ_BIT);
dvz_cmd_barriers(commands_, &buffer_barriers); dvz_cmd_barriers(commands_, &buffer_barriers);
if (timestamps_supported_) { if (observing_ && timestamps_supported_) {
vkCmdWriteTimestamp(command_buffer, vkCmdWriteTimestamp(command_buffer,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
query_pool_, 3); query_pool_, 3);
@@ -390,9 +395,11 @@ public:
result.gpu_timing = collect_gpu_timing(); result.gpu_timing = collect_gpu_timing();
} catch (...) { } catch (...) {
in_flight_ = false; in_flight_ = false;
observing_ = false;
throw; throw;
} }
in_flight_ = false; in_flight_ = false;
observing_ = false;
return result; return result;
} }
@@ -400,6 +407,7 @@ public:
if (!in_flight_) if (!in_flight_)
throw std::logic_error("Datoviz frame target has no pending frame"); throw std::logic_error("Datoviz frame target has no pending frame");
in_flight_ = false; in_flight_ = false;
observing_ = false;
} }
[[nodiscard]] VkDevice device() const { [[nodiscard]] VkDevice device() const {
@@ -418,10 +426,12 @@ public:
if (recording_ && commands_ != nullptr) if (recording_ && commands_ != nullptr)
dvz_cmd_reset(commands_); dvz_cmd_reset(commands_);
recording_ = false; recording_ = false;
observing_ = false;
} }
private: private:
void initialize_timestamps(DvzDevice* device, DvzQueue* queue) noexcept { void initialize_timestamps(DvzDevice* device, DvzQueue* queue) noexcept {
timestamps_initialized_ = true;
if (device == nullptr || queue == nullptr || if (device == nullptr || queue == nullptr ||
vkGetPhysicalDeviceQueueFamilyProperties == nullptr || vkGetPhysicalDeviceQueueFamilyProperties == nullptr ||
vkGetPhysicalDeviceProperties == nullptr || vkGetPhysicalDeviceProperties == nullptr ||
@@ -467,7 +477,8 @@ private:
[[nodiscard]] std::optional<Datoviz_Gpu_Timing> [[nodiscard]] std::optional<Datoviz_Gpu_Timing>
collect_gpu_timing() const noexcept { collect_gpu_timing() const noexcept {
if (!timestamps_supported_ || query_pool_ == VK_NULL_HANDLE) if (!observing_ || !timestamps_supported_ ||
query_pool_ == VK_NULL_HANDLE)
return std::nullopt; return std::nullopt;
const VkDevice device = const VkDevice device =
dvz_device_handle(dvz_gpu_ctx_device(gpu_context_)); dvz_device_handle(dvz_gpu_ctx_device(gpu_context_));
@@ -497,7 +508,9 @@ private:
}; };
return Datoviz_Gpu_Timing{ return Datoviz_Gpu_Timing{
elapsed(timestamps[0], timestamps[1]), elapsed(timestamps[0], timestamps[1]),
elapsed(timestamps[2], timestamps[3])}; elapsed(timestamps[1], timestamps[2]),
elapsed(timestamps[2], timestamps[3]),
elapsed(timestamps[0], timestamps[3])};
} }
void destroy() noexcept { void destroy() noexcept {
@@ -556,6 +569,8 @@ private:
std::uint32_t timestamp_valid_bits_{}; std::uint32_t timestamp_valid_bits_{};
bool recording_{}; bool recording_{};
bool in_flight_{}; bool in_flight_{};
bool observing_{};
bool timestamps_initialized_{};
bool timestamps_supported_{}; bool timestamps_supported_{};
}; };
@@ -1023,15 +1038,17 @@ std::optional<Datoviz_Visual_Backend::Pending_Frame>
Datoviz_Visual_Backend::submit( Datoviz_Visual_Backend::submit(
const Scene_State& scene, std::uint64_t scene_revision, const Scene_State& scene, std::uint64_t scene_revision,
const Prepared_Point& point, std::uint64_t frame_sequence, const Prepared_Point& point, std::uint64_t frame_sequence,
bool capture_trace) { bool observe) {
require_domain(); require_domain();
if (scene.viewport.empty()) if (scene.viewport.empty())
return std::nullopt; return std::nullopt;
Datoviz_Frame_Trace trace; Datoviz_Frame_Trace trace;
trace.render_sequence = frame_sequence; trace.render_sequence = frame_sequence;
auto phase_started = trace_now_ns(); trace.observed = observe;
std::uint64_t phase_started = observe ? trace_now_ns() : 0;
apply(scene, scene_revision, point); apply(scene, scene_revision, point);
trace.apply_ns = trace_now_ns() - phase_started; if (observe)
trace.apply_ns = trace_now_ns() - phase_started;
if (target_ == nullptr || target_extent_ != scene.viewport) { if (target_ == nullptr || target_extent_ != scene.viewport) {
target_.reset(); target_.reset();
@@ -1040,36 +1057,42 @@ Datoviz_Visual_Backend::submit(
++target_generation_); ++target_generation_);
} }
target_->begin(); target_->begin(observe);
DvzSceneFrameArtifact* artifact{}; DvzSceneFrameArtifact* artifact{};
try { try {
phase_started = trace_now_ns(); if (observe)
phase_started = trace_now_ns();
artifact = emit(scene); artifact = emit(scene);
trace.emit_ns = trace_now_ns() - phase_started; if (observe)
trace.emit_ns = trace_now_ns() - phase_started;
} catch (...) { } catch (...) {
target_->abort(); target_->abort();
throw; throw;
} }
trace.artifact_status = static_cast<std::uint32_t>( if (observe) {
dvz_scene_frame_artifact_status(artifact)); trace.artifact_status = static_cast<std::uint32_t>(
trace.artifact_resource_version = dvz_scene_frame_artifact_status(artifact));
dvz_scene_frame_artifact_resource_version(artifact); trace.artifact_resource_version =
trace.artifact_frame_index = dvz_scene_frame_artifact_resource_version(artifact);
dvz_scene_frame_artifact_frame_index(artifact); trace.artifact_frame_index =
dvz_scene_frame_artifact_frame_index(artifact);
}
const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact); const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact);
const DvzStreamFrame target_frame = target_->stream_frame(); const DvzStreamFrame target_frame = target_->stream_frame();
phase_started = trace_now_ns(); if (observe)
phase_started = trace_now_ns();
const bool attached = stream != nullptr && const bool attached = stream != nullptr &&
dvz_drp2_runtime_attach_frame_target( dvz_drp2_runtime_attach_frame_target(
runtime_, color_target_id, &target_frame); runtime_, color_target_id, &target_frame);
const DvzDrp2ValidationResult result = const DvzDrp2ValidationResult result =
attached ? dvz_drp2_runtime_execute(runtime_, stream) attached ? dvz_drp2_runtime_execute(runtime_, stream)
: DvzDrp2ValidationResult{}; : DvzDrp2ValidationResult{};
trace.execute_ns = trace_now_ns() - phase_started; if (observe)
trace.execute_ns = trace_now_ns() - phase_started;
trace.validation_ok = attached && result.ok; trace.validation_ok = attached && result.ok;
trace.validation_code = static_cast<std::uint32_t>(result.code); trace.validation_code = static_cast<std::uint32_t>(result.code);
trace.validation_command_index = result.command_index; trace.validation_command_index = result.command_index;
if (capture_trace || !trace.validation_ok) { if (observe || !trace.validation_ok) {
if (char* json = dvz_scene_frame_artifact_json( if (char* json = dvz_scene_frame_artifact_json(
artifact, "renderive_frame")) { artifact, "renderive_frame")) {
trace.artifact_json = json; trace.artifact_json = json;
@@ -1095,9 +1118,11 @@ Datoviz_Visual_Backend::submit(
throw std::runtime_error(std::move(message)); throw std::runtime_error(std::move(message));
} }
phase_started = trace_now_ns(); if (observe)
phase_started = trace_now_ns();
target_->submit(); target_->submit();
trace.submit_ns = trace_now_ns() - phase_started; if (observe)
trace.submit_ns = trace_now_ns() - phase_started;
return Pending_Frame{target_->device(), target_->fence(), scene.viewport, return Pending_Frame{target_->device(), target_->fence(), scene.viewport,
frame_sequence, target_->generation(), frame_sequence, target_->generation(),
std::move(trace)}; std::move(trace)};
@@ -1109,9 +1134,12 @@ Datoviz_Visual_Backend::Completed_Frame Datoviz_Visual_Backend::collect(
if (target_ == nullptr || if (target_ == nullptr ||
target_->generation() != pending.target_generation) target_->generation() != pending.target_generation)
throw std::logic_error("Datoviz pending frame target no longer exists"); throw std::logic_error("Datoviz pending frame target no longer exists");
const std::uint64_t readback_started = trace_now_ns(); const std::uint64_t readback_started = pending.trace.observed
? trace_now_ns()
: 0;
auto collection = target_->collect(); auto collection = target_->collect();
pending.trace.readback_ns = trace_now_ns() - readback_started; if (pending.trace.observed)
pending.trace.readback_ns = trace_now_ns() - readback_started;
pending.trace.gpu = std::move(collection.gpu_timing); pending.trace.gpu = std::move(collection.gpu_timing);
auto output = std::make_shared<Pixel_Frame>(); auto output = std::make_shared<Pixel_Frame>();
output->extent = pending.extent; output->extent = pending.extent;
@@ -45,7 +45,7 @@ public:
[[nodiscard]] std::optional<Pending_Frame> submit( [[nodiscard]] std::optional<Pending_Frame> submit(
const Scene_State& scene, std::uint64_t scene_revision, const Scene_State& scene, std::uint64_t scene_revision,
const Prepared_Point& point, std::uint64_t frame_sequence, const Prepared_Point& point, std::uint64_t frame_sequence,
bool capture_trace); bool observe);
[[nodiscard]] Completed_Frame collect(Pending_Frame pending); [[nodiscard]] Completed_Frame collect(Pending_Frame pending);
void discard(Pending_Frame pending); void discard(Pending_Frame pending);
@@ -14,19 +14,61 @@ Gpu_Completion_Service::~Gpu_Completion_Service() {
shutdown(); shutdown();
} }
void Gpu_Completion_Service::watch( Gpu_Completion_Service::Reservation::Reservation(
VkDevice device, VkFence fence, Completion completion) { std::shared_ptr<Pending_Fence> pending) noexcept
if (device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE) : pending_(std::move(pending)) {}
throw std::invalid_argument("GPU completion fence is invalid");
Gpu_Completion_Service::Reservation::~Reservation() {
cancel();
}
Gpu_Completion_Service::Reservation::Reservation(
Reservation&& other) noexcept
: pending_(std::exchange(other.pending_, {})) {}
void Gpu_Completion_Service::Reservation::watch(
VkDevice device, VkFence fence) noexcept {
if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE)
std::terminate();
auto pending = std::exchange(pending_, {});
{
std::lock_guard lock(pending->mutex);
if (pending->status != Pending_Fence::Status::reserved)
std::terminate();
pending->device = device;
pending->fence = fence;
pending->status = Pending_Fence::Status::watched;
}
pending->ready.notify_one();
}
void Gpu_Completion_Service::Reservation::cancel() noexcept {
if (!pending_)
return;
auto pending = std::exchange(pending_, {});
{
std::lock_guard lock(pending->mutex);
if (pending->status == Pending_Fence::Status::reserved)
pending->status = Pending_Fence::Status::canceled;
}
pending->ready.notify_one();
}
Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(
Completion completion, bool observe) {
if (!completion) if (!completion)
throw std::invalid_argument("GPU completion callback is empty"); throw std::invalid_argument("GPU completion callback is empty");
auto pending = std::make_shared<Pending_Fence>();
pending->completion = std::move(completion);
pending->observe = observe;
{ {
std::lock_guard lock(mutex_); std::lock_guard lock(mutex_);
if (stopping_) if (stopping_)
throw std::runtime_error("GPU completion service is stopping"); throw std::runtime_error("GPU completion service is stopping");
pending_.push({device, fence, std::move(completion)}); pending_.push_back(pending);
} }
ready_.notify_one(); ready_.notify_one();
return Reservation(std::move(pending));
} }
void Gpu_Completion_Service::shutdown() noexcept { void Gpu_Completion_Service::shutdown() noexcept {
@@ -35,6 +77,19 @@ void Gpu_Completion_Service::shutdown() noexcept {
if (stopping_ && !thread_.joinable()) if (stopping_ && !thread_.joinable())
return; return;
stopping_ = true; stopping_ = true;
const auto cancel_reserved = [](const auto& pending) {
if (!pending)
return;
{
std::lock_guard pending_lock(pending->mutex);
if (pending->status == Pending_Fence::Status::reserved)
pending->status = Pending_Fence::Status::canceled;
}
pending->ready.notify_one();
};
cancel_reserved(active_);
for (const auto& pending : pending_)
cancel_reserved(pending);
} }
ready_.notify_one(); ready_.notify_one();
if (thread_.joinable()) if (thread_.joinable())
@@ -43,7 +98,7 @@ void Gpu_Completion_Service::shutdown() noexcept {
void Gpu_Completion_Service::run() noexcept { void Gpu_Completion_Service::run() noexcept {
for (;;) { for (;;) {
Pending_Fence pending; std::shared_ptr<Pending_Fence> pending;
{ {
std::unique_lock lock(mutex_); std::unique_lock lock(mutex_);
ready_.wait(lock, [this] { ready_.wait(lock, [this] {
@@ -52,19 +107,45 @@ void Gpu_Completion_Service::run() noexcept {
if (stopping_ && pending_.empty()) if (stopping_ && pending_.empty())
return; return;
pending = std::move(pending_.front()); pending = std::move(pending_.front());
pending_.pop(); pending_.pop_front();
active_ = pending;
} }
const auto wait_started = std::chrono::steady_clock::now(); 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();
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( const VkResult result = vkWaitForFences(
pending.device, 1, &pending.fence, VK_TRUE, UINT64_MAX); device, 1, &fence, VK_TRUE, UINT64_MAX);
const auto wait_duration =
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - wait_started).count();
Result completion_result; Result completion_result;
completion_result.wait_duration_ns = wait_duration > 0 if (observe) {
? static_cast<std::uint64_t>(wait_duration) const auto wait_duration =
: 0; 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) { if (result != VK_SUCCESS) {
try { try {
throw std::runtime_error( throw std::runtime_error(
@@ -75,9 +156,13 @@ void Gpu_Completion_Service::run() noexcept {
} }
} }
try { try {
pending.completion(std::move(completion_result)); completion(std::move(completion_result));
} catch (...) { } catch (...) {
} }
{
std::lock_guard lock(mutex_);
active_.reset();
}
} }
} }
@@ -4,15 +4,18 @@
#include <condition_variable> #include <condition_variable>
#include <cstdint> #include <cstdint>
#include <deque>
#include <exception> #include <exception>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <queue> #include <memory>
#include <thread> #include <thread>
namespace renderive::render_3d::detail { namespace renderive::render_3d::detail {
class Gpu_Completion_Service final { class Gpu_Completion_Service final {
struct Pending_Fence;
public: public:
struct Result { struct Result {
std::exception_ptr error; std::exception_ptr error;
@@ -21,27 +24,59 @@ public:
using Completion = std::function<void(Result)>; using Completion = std::function<void(Result)>;
class Reservation final {
public:
Reservation() = default;
~Reservation();
Reservation(const Reservation&) = delete;
Reservation& operator=(const Reservation&) = delete;
Reservation(Reservation&& other) noexcept;
Reservation& operator=(Reservation&&) = delete;
void watch(VkDevice device, VkFence fence) noexcept;
private:
explicit Reservation(std::shared_ptr<Pending_Fence> pending) noexcept;
void cancel() noexcept;
std::shared_ptr<Pending_Fence> pending_;
friend class Gpu_Completion_Service;
};
Gpu_Completion_Service(); Gpu_Completion_Service();
~Gpu_Completion_Service(); ~Gpu_Completion_Service();
Gpu_Completion_Service(const Gpu_Completion_Service&) = delete; Gpu_Completion_Service(const Gpu_Completion_Service&) = delete;
Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete; Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete;
void watch(VkDevice device, VkFence fence, Completion completion); [[nodiscard]] Reservation prepare(Completion completion, bool observe);
void shutdown() noexcept; void shutdown() noexcept;
private: private:
struct Pending_Fence { struct Pending_Fence {
enum class Status {
reserved,
watched,
canceled
};
std::mutex mutex;
std::condition_variable ready;
VkDevice device{VK_NULL_HANDLE}; VkDevice device{VK_NULL_HANDLE};
VkFence fence{VK_NULL_HANDLE}; VkFence fence{VK_NULL_HANDLE};
Completion completion; Completion completion;
Status status{Status::reserved};
bool observe{};
}; };
void run() noexcept; void run() noexcept;
std::mutex mutex_; std::mutex mutex_;
std::condition_variable ready_; std::condition_variable ready_;
std::queue<Pending_Fence> pending_; std::deque<std::shared_ptr<Pending_Fence>> pending_;
std::shared_ptr<Pending_Fence> active_;
std::thread thread_; std::thread thread_;
bool stopping_{}; bool stopping_{};
}; };
+66 -10
View File
@@ -5,7 +5,6 @@
#include <future> #include <future>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <queue>
#include <stdexcept> #include <stdexcept>
#include <thread> #include <thread>
#include <type_traits> #include <type_traits>
@@ -14,7 +13,34 @@
namespace renderive::render_3d::detail { namespace renderive::render_3d::detail {
class Render_Domain final { class Render_Domain final {
struct Task {
explicit Task(std::function<void()> value)
: function(std::move(value)) {}
std::function<void()> function;
std::unique_ptr<Task> next;
};
public: public:
class Prepared_Task final {
public:
Prepared_Task() = default;
~Prepared_Task() = default;
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;
private:
explicit Prepared_Task(std::unique_ptr<Task> task) noexcept
: task_(std::move(task)) {}
std::unique_ptr<Task> task_;
friend class Render_Domain;
};
Render_Domain() : thread_([this] { run(); }) {} Render_Domain() : thread_([this] { run(); }) {}
~Render_Domain() { ~Render_Domain() {
{ {
@@ -29,14 +55,32 @@ public:
Render_Domain(const Render_Domain&) = delete; Render_Domain(const Render_Domain&) = delete;
Render_Domain& operator=(const Render_Domain&) = delete; Render_Domain& operator=(const Render_Domain&) = delete;
void post(std::function<void()> function) { [[nodiscard]] Prepared_Task prepare(std::function<void()> function) {
if (!function) if (!function)
throw std::invalid_argument("render domain task is empty"); 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_); std::lock_guard lock(mutex_);
if (stopping_) if (stopping_)
throw std::runtime_error("Point_Scene render domain is stopping"); throw std::runtime_error("Point_Scene render domain is stopping");
tasks_.push(std::move(function)); enqueue_locked(std::move(task.task_));
}
condition_.notify_one();
}
void post(Prepared_Task task) noexcept {
if (!task.task_)
std::terminate();
{
std::lock_guard lock(mutex_);
if (stopping_)
std::terminate();
enqueue_locked(std::move(task.task_));
} }
condition_.notify_one(); condition_.notify_one();
} }
@@ -57,22 +101,34 @@ public:
private: private:
void run() { void run() {
for (;;) { for (;;) {
std::function<void()> task; std::unique_ptr<Task> task;
{ {
std::unique_lock lock(mutex_); std::unique_lock lock(mutex_);
condition_.wait(lock, [&] { return stopping_ || !tasks_.empty(); }); condition_.wait(lock, [&] { return stopping_ || first_; });
if (stopping_ && tasks_.empty()) if (stopping_ && !first_)
return; return;
task = std::move(tasks_.front()); task = std::move(first_);
tasks_.pop(); first_ = std::move(task->next);
if (!first_)
last_ = nullptr;
} }
task(); task->function();
} }
} }
void enqueue_locked(std::unique_ptr<Task> task) noexcept {
Task* const inserted = task.get();
if (last_)
last_->next = std::move(task);
else
first_ = std::move(task);
last_ = inserted;
}
std::mutex mutex_; std::mutex mutex_;
std::condition_variable condition_; std::condition_variable condition_;
std::queue<std::function<void()>> tasks_; std::unique_ptr<Task> first_;
Task* last_{};
bool stopping_{}; bool stopping_{};
std::thread thread_; std::thread thread_;
}; };
@@ -0,0 +1,63 @@
#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;
std::atomic<int> completion_count{};
{
auto reservation = service.prepare(
[&](Gpu_Completion_Service::Result) {
completion_count.fetch_add(1, std::memory_order_relaxed);
},
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(GpuCompletionService, ShutdownCancelsAnUnarmedReservation) {
Gpu_Completion_Service service;
auto reservation = service.prepare(
[](Gpu_Completion_Service::Result) {}, false);
service.shutdown();
}
TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) {
Render_Domain domain;
std::atomic<bool> executed{};
auto task = domain.prepare([&] {
executed.store(true, std::memory_order_release);
});
domain.post(std::move(task));
domain.invoke([] {});
EXPECT_TRUE(executed.load(std::memory_order_acquire));
}
} // namespace
} // namespace renderive::render_3d::detail
@@ -139,8 +139,36 @@ TEST(PointRenderIntegration,
Node_Metric_Kind::gpu_fence_wait_ns)); Node_Metric_Kind::gpu_fence_wait_ns));
EXPECT_TRUE(execution.metrics.contains( EXPECT_TRUE(execution.metrics.contains(
Node_Metric_Kind::readback_duration_ns)); Node_Metric_Kind::readback_duration_ns));
EXPECT_GT(execution.metrics.get( const auto artifact_bytes = execution.metrics.get(
Node_Metric_Kind::backend_artifact_json_bytes), 0U); Node_Metric_Kind::backend_artifact_json_bytes);
EXPECT_GT(artifact_bytes, 0U);
const auto artifact = std::find_if(
execution.attachments.begin(), execution.attachments.end(),
[](const Node_Diagnostic_Attachment& attachment) {
return attachment.type == "datoviz.drp2.artifact.json";
});
ASSERT_NE(artifact, execution.attachments.end());
EXPECT_EQ(artifact->content.size(), artifact_bytes);
EXPECT_FALSE(artifact->content.empty());
const bool gpu_timing = execution.metrics.contains(
Node_Metric_Kind::gpu_render_duration_ns);
EXPECT_EQ(gpu_timing, execution.metrics.contains(
Node_Metric_Kind::gpu_transition_duration_ns));
EXPECT_EQ(gpu_timing, execution.metrics.contains(
Node_Metric_Kind::gpu_copy_duration_ns));
EXPECT_EQ(gpu_timing, execution.metrics.contains(
Node_Metric_Kind::gpu_total_duration_ns));
if (gpu_timing) {
const auto total = execution.metrics.get(
Node_Metric_Kind::gpu_total_duration_ns);
EXPECT_GE(total, execution.metrics.get(
Node_Metric_Kind::gpu_render_duration_ns));
EXPECT_GE(total, execution.metrics.get(
Node_Metric_Kind::gpu_transition_duration_ns));
EXPECT_GE(total, execution.metrics.get(
Node_Metric_Kind::gpu_copy_duration_ns));
}
} catch (const std::exception& error) { } catch (const std::exception& error) {
GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what();
} }
+13 -2
View File
@@ -59,7 +59,8 @@ inline Json metrics_json(const Node_Execution_Metrics& metrics) {
"primitive_count", "queue_wait_ns", "apply_duration_ns", "primitive_count", "queue_wait_ns", "apply_duration_ns",
"plan_emit_duration_ns", "backend_execute_duration_ns", "plan_emit_duration_ns", "backend_execute_duration_ns",
"submit_duration_ns", "gpu_fence_wait_ns", "submit_duration_ns", "gpu_fence_wait_ns",
"gpu_render_duration_ns", "gpu_copy_duration_ns", "gpu_render_duration_ns", "gpu_transition_duration_ns",
"gpu_copy_duration_ns", "gpu_total_duration_ns",
"readback_duration_ns", "backend_resource_version", "readback_duration_ns", "backend_resource_version",
"backend_frame_index", "backend_artifact_status", "backend_frame_index", "backend_artifact_status",
"backend_validation_code", "backend_validation_command", "backend_validation_code", "backend_validation_command",
@@ -147,6 +148,11 @@ inline Json plan_json(const Scene_Base& scene, const Render_Plan& plan) {
inline Json execution_json(const Node_Execution& execution, inline Json execution_json(const Node_Execution& execution,
const Frame_Snapshot& frame) { const Frame_Snapshot& frame) {
Json attachments = Json::array();
for (const auto& attachment : execution.attachments) {
attachments.push_back({{"type", attachment.type},
{"content", attachment.content}});
}
return { return {
{"node_id", execution.node_id}, {"node_id", execution.node_id},
{"ready_time_ns", execution.ready_time_ns}, {"ready_time_ns", execution.ready_time_ns},
@@ -172,7 +178,8 @@ inline Json execution_json(const Node_Execution& execution,
{"external_duration_ns", execution.external_duration_ns()}, {"external_duration_ns", execution.external_duration_ns()},
{"worker_id", execution.worker_id}, {"worker_id", execution.worker_id},
{"status", execution_status(execution.status)}, {"status", execution_status(execution.status)},
{"metrics", metrics_json(execution.metrics)} {"metrics", metrics_json(execution.metrics)},
{"attachments", std::move(attachments)}
}; };
} }
@@ -240,6 +247,10 @@ inline Json node_statistics_json(const Node_Statistics& statistics) {
{"p99_ns", statistics.p99_ns}, {"p99_ns", statistics.p99_ns},
{"minimum_ns", statistics.minimum_ns}, {"minimum_ns", statistics.minimum_ns},
{"maximum_ns", statistics.maximum_ns}, {"maximum_ns", statistics.maximum_ns},
{"average_cpu_ns", statistics.average_cpu_ns},
{"average_external_ns", statistics.average_external_ns},
{"p95_cpu_ns", statistics.p95_cpu_ns},
{"p95_external_ns", statistics.p95_external_ns},
{"critical_path_frequency", statistics.critical_path_frequency}, {"critical_path_frequency", statistics.critical_path_frequency},
{"average_scheduler_wait_ns", statistics.average_scheduler_wait_ns} {"average_scheduler_wait_ns", statistics.average_scheduler_wait_ns}
}; };
+5
View File
@@ -551,6 +551,7 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic
EXPECT_TRUE(execution.contains("end_time_ns")); EXPECT_TRUE(execution.contains("end_time_ns"));
EXPECT_TRUE(execution.contains("worker_id")); EXPECT_TRUE(execution.contains("worker_id"));
EXPECT_TRUE(execution.contains("status")); EXPECT_TRUE(execution.contains("status"));
EXPECT_TRUE(execution.contains("attachments"));
const auto& analysis = frame.at("analysis"); const auto& analysis = frame.at("analysis");
EXPECT_TRUE(analysis.contains("critical_path")); EXPECT_TRUE(analysis.contains("critical_path"));
EXPECT_TRUE(analysis.contains("parallel_overlap_ns")); EXPECT_TRUE(analysis.contains("parallel_overlap_ns"));
@@ -570,6 +571,10 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic
EXPECT_TRUE(statistic.contains("p50_ns")); EXPECT_TRUE(statistic.contains("p50_ns"));
EXPECT_TRUE(statistic.contains("p95_ns")); EXPECT_TRUE(statistic.contains("p95_ns"));
EXPECT_TRUE(statistic.contains("p99_ns")); EXPECT_TRUE(statistic.contains("p99_ns"));
EXPECT_TRUE(statistic.contains("average_cpu_ns"));
EXPECT_TRUE(statistic.contains("average_external_ns"));
EXPECT_TRUE(statistic.contains("p95_cpu_ns"));
EXPECT_TRUE(statistic.contains("p95_external_ns"));
EXPECT_TRUE(statistic.contains("critical_path_frequency")); EXPECT_TRUE(statistic.contains("critical_path_frequency"));
EXPECT_TRUE(statistic.contains("average_scheduler_wait_ns")); EXPECT_TRUE(statistic.contains("average_scheduler_wait_ns"));
} }
+12 -3
View File
@@ -1,9 +1,14 @@
import {Stack,Typography} from "@mui/material"; import {Stack,Typography} from "@mui/material";
import type {Gallery_Capture_Session,Gallery_Captured_Frame,Gallery_Render_Plan} from "../protocol/gallery_types"; import type {Gallery_Capture_Session,Gallery_Captured_Frame,Gallery_Render_Plan,Json_Value} from "../protocol/gallery_types";
import {Metric_Grid} from "../common/metric_grid"; import {Metric_Grid} from "../common/metric_grid";
import {Json_Viewer} from "../common/json_viewer"; import {Json_Viewer} from "../common/json_viewer";
import {format_nanoseconds} from "../protocol/format"; import {format_nanoseconds} from "../protocol/format";
function attachment_value(type:string,content:string):Json_Value {
if(!type.endsWith(".json"))return content;
try{return JSON.parse(content) as Json_Value;}catch{return content;}
}
export function Node_Detail({frame,plan,session,selected_node_id}:{frame:Gallery_Captured_Frame;plan:Gallery_Render_Plan;session:Gallery_Capture_Session;selected_node_id:number|null}) { export function Node_Detail({frame,plan,session,selected_node_id}:{frame:Gallery_Captured_Frame;plan:Gallery_Render_Plan;session:Gallery_Capture_Session;selected_node_id:number|null}) {
const node=plan.nodes.find(item=>item.node_id===selected_node_id); const node=plan.nodes.find(item=>item.node_id===selected_node_id);
const execution=frame.node_executions.find(item=>item.node_id===selected_node_id); const execution=frame.node_executions.find(item=>item.node_id===selected_node_id);
@@ -22,6 +27,10 @@ export function Node_Detail({frame,plan,session,selected_node_id}:{frame:Gallery
["Critical",`${((analysis?.critical_path_contribution??0)*100).toFixed(2)}%`], ["Critical",`${((analysis?.critical_path_contribution??0)*100).toFixed(2)}%`],
["Moving avg",format_nanoseconds(history?.moving_average_ns)], ["Moving avg",format_nanoseconds(history?.moving_average_ns)],
["P95",format_nanoseconds(history?.p95_ns)], ["P95",format_nanoseconds(history?.p95_ns)],
["P99",format_nanoseconds(history?.p99_ns)] ["P99",format_nanoseconds(history?.p99_ns)],
]}/>{execution.metrics&&<Json_Viewer value={execution.metrics}/>}</Stack>; ["CPU avg",format_nanoseconds(history?.average_cpu_ns)],
["CPU P95",format_nanoseconds(history?.p95_cpu_ns)],
["External avg",format_nanoseconds(history?.average_external_ns)],
["External P95",format_nanoseconds(history?.p95_external_ns)]
]}/>{execution.metrics&&<Json_Viewer value={execution.metrics}/>} {execution.attachments.map((attachment,index)=><Stack key={`${attachment.type}:${index}`} spacing={1}><Typography variant="subtitle2">{attachment.type}</Typography><Json_Viewer value={attachment_value(attachment.type,attachment.content)}/></Stack>)}</Stack>;
} }
+3 -2
View File
@@ -31,10 +31,11 @@ export interface Gallery_Render_Node {node_id: number; id?: string | number; nam
export interface Gallery_Render_Edge {from: number | string; to: number | string;} export interface Gallery_Render_Edge {from: number | string; to: number | string;}
export interface Gallery_Renderable_Cache {owner_id: number; name: string; prepare_cache: string; paint_cache: string;} export interface Gallery_Renderable_Cache {owner_id: number; name: string; prepare_cache: string; paint_cache: string;}
export interface Gallery_Render_Plan {version: number; nodes: Gallery_Render_Node[]; edges: Gallery_Render_Edge[]; renderables: Gallery_Renderable_Cache[];} export interface Gallery_Render_Plan {version: number; nodes: Gallery_Render_Node[]; edges: Gallery_Render_Edge[]; renderables: Gallery_Renderable_Cache[];}
export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; cpu_end_offset_ns: number; external_start_offset_ns: number; external_end_offset_ns: number; end_offset_ns: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; status: "pending"|"ready"|"running"|"waiting_external"|"complete"|"failed"; metrics?: Record<string, Json_Primitive>;} export interface Gallery_Node_Diagnostic_Attachment {type: string; content: string;}
export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; cpu_end_offset_ns: number; external_start_offset_ns: number; external_end_offset_ns: number; end_offset_ns: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; status: "pending"|"ready"|"running"|"waiting_external"|"complete"|"failed"; metrics?: Record<string, Json_Primitive>; attachments: Gallery_Node_Diagnostic_Attachment[];}
export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; scheduler_wait_ns: number; critical_path_contribution: number; on_critical_path: boolean;} export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; scheduler_wait_ns: number; critical_path_contribution: number; on_critical_path: boolean;}
export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: {total_work_duration_ns?: number; total_external_duration_ns?: number; nodes: Gallery_Node_Analysis[]};} export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: {total_work_duration_ns?: number; total_external_duration_ns?: number; nodes: Gallery_Node_Analysis[]};}
export interface Gallery_Node_Statistics {node_id: number; moving_average_ns: number; p95_ns: number; p99_ns: number; critical_path_frequency: number;} export interface Gallery_Node_Statistics {node_id: number; moving_average_ns: number; p95_ns: number; p99_ns: number; average_cpu_ns: number; average_external_ns: number; p95_cpu_ns: number; p95_external_ns: number; critical_path_frequency: number;}
export interface Gallery_Plan_Statistics {render_plan_version: number; frame_count: number; render_average_ns: number; render_p95_ns: number; average_parallelism: number; peak_parallelism: number; scheduler_wait_average_ns: number; nodes: Gallery_Node_Statistics[];} export interface Gallery_Plan_Statistics {render_plan_version: number; frame_count: number; render_average_ns: number; render_p95_ns: number; average_parallelism: number; peak_parallelism: number; scheduler_wait_average_ns: number; nodes: Gallery_Node_Statistics[];}
export interface Gallery_Capture_Session {session_id: number; active: boolean; requested_count: number; captured_count: number; frames: Gallery_Captured_Frame[]; node_statistics: Gallery_Node_Statistics[]; plan_statistics: Gallery_Plan_Statistics[]; summary?: Record<string, Json_Value>;} export interface Gallery_Capture_Session {session_id: number; active: boolean; requested_count: number; captured_count: number; frames: Gallery_Captured_Frame[]; node_statistics: Gallery_Node_Statistics[]; plan_statistics: Gallery_Plan_Statistics[]; summary?: Record<string, Json_Value>;}
export interface Gallery_Performance_Capture {controller: {enabled?: boolean; session_id?: number}; sessions: Gallery_Capture_Session[]; plans: Gallery_Render_Plan[];} export interface Gallery_Performance_Capture {controller: {enabled?: boolean; session_id?: number}; sessions: Gallery_Capture_Session[]; plans: Gallery_Render_Plan[];}
+1 -1
View File
@@ -1,2 +1,2 @@
import {describe,expect,it} from "vitest";import {build_dag_model} from "../../src/dag/dag_model"; import {describe,expect,it} from "vitest";import {build_dag_model} from "../../src/dag/dag_model";
describe("DAG view model",()=>{it("uses node_id for current plan and capture overlays",()=>{const plan={version:2,nodes:[{node_id:7,name:"paint",owner:"plot",kind:"paint"}],edges:[],renderables:[]};const frame={frame_id:1,render_plan_version:2,render_duration_ns:10,node_executions:[{node_id:7,worker_id:3,start_offset_ns:0,cpu_end_offset_ns:10,external_start_offset_ns:0,external_end_offset_ns:0,end_offset_ns:10,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,status:"complete" as const}],analysis:{total_work_duration_ns:10,total_external_duration_ns:0,nodes:[{node_id:7,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,scheduler_wait_ns:1,critical_path_contribution:1,on_critical_path:true}]}};const model=build_dag_model(plan,frame,[],7);expect(model.nodes[0].id).toBe("7");expect(model.nodes[0].data).toMatchObject({duration_ns:10,worker_id:3,critical:true,selected:true});});}); describe("DAG view model",()=>{it("uses node_id for current plan and capture overlays",()=>{const plan={version:2,nodes:[{node_id:7,name:"paint",owner:"plot",kind:"paint"}],edges:[],renderables:[]};const frame={frame_id:1,render_plan_version:2,render_duration_ns:10,node_executions:[{node_id:7,worker_id:3,start_offset_ns:0,cpu_end_offset_ns:10,external_start_offset_ns:0,external_end_offset_ns:0,end_offset_ns:10,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,status:"complete" as const,attachments:[]}],analysis:{total_work_duration_ns:10,total_external_duration_ns:0,nodes:[{node_id:7,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,scheduler_wait_ns:1,critical_path_contribution:1,on_critical_path:true}]}};const model=build_dag_model(plan,frame,[],7);expect(model.nodes[0].id).toBe("7");expect(model.nodes[0].data).toMatchObject({duration_ns:10,worker_id:3,critical:true,selected:true});});});