性能优化

This commit is contained in:
2026-08-23 02:08:33 +08:00
parent 15107a01ad
commit b3af1c1b04
6 changed files with 571 additions and 325 deletions
+3
View File
@@ -15,6 +15,9 @@ enum class Frame_Trace_Marker : std::uint8_t {
paint_started,
paint_finished,
backend_queue_entered,
backend_prepare_started,
backend_prepare_finished,
backend_submit_queued,
backend_queue_left,
gpu_submitted,
gpu_completed,
@@ -2,6 +2,7 @@
#include "Datoviz_Visual_Backend.hpp"
#include "Gpu_Completion_Service.hpp"
#include "Render_Domain.hpp"
#include <concurrentqueue-1.0.5/blockingconcurrentqueue.h>
#include <algorithm>
#include <atomic>
#include <cmath>
@@ -9,7 +10,9 @@
#include <mutex>
#include <optional>
#include <stdexcept>
#include <thread>
#include <utility>
#include <variant>
#include <vector>
namespace aethera::render_3d::detail {
namespace {
@@ -24,83 +27,127 @@ Mouse_Button held_button(Mouse_Button_Mask buttons) {
return Mouse_Button::none;
}
void record_datoviz_trace(Frame_3D* frame, const Datoviz_Frame_Trace& trace) {
if (trace.apply_ns) frame->record(Frame_Trace_Measurement::backend_apply_ns, trace.apply_ns);
if (trace.emit_ns) frame->record(Frame_Trace_Measurement::backend_plan_ns, trace.emit_ns);
if (trace.execute_ns) frame->record(Frame_Trace_Measurement::backend_execute_ns, trace.execute_ns);
if (trace.submit_ns) frame->record(Frame_Trace_Measurement::backend_submit_ns, trace.submit_ns);
if (trace.readback_ns) frame->record(Frame_Trace_Measurement::readback_ns, trace.readback_ns);
if (trace.apply_ns)
frame->record(Frame_Trace_Measurement::backend_apply_ns, trace.apply_ns);
if (trace.emit_ns)
frame->record(Frame_Trace_Measurement::backend_plan_ns, trace.emit_ns);
if (trace.execute_ns)
frame->record(Frame_Trace_Measurement::backend_execute_ns, trace.execute_ns);
if (trace.submit_ns)
frame->record(Frame_Trace_Measurement::backend_submit_ns, trace.submit_ns);
if (trace.readback_ns)
frame->record(Frame_Trace_Measurement::readback_ns, trace.readback_ns);
if (!trace.gpu) return;
frame->record(Frame_Trace_Measurement::gpu_render_ns, trace.gpu->render_ns);
frame->record(Frame_Trace_Measurement::gpu_transition_ns, trace.gpu->transition_ns);
frame->record(Frame_Trace_Measurement::gpu_transition_ns,
trace.gpu->transition_ns);
frame->record(Frame_Trace_Measurement::gpu_copy_ns, trace.gpu->copy_ns);
frame->record(Frame_Trace_Measurement::gpu_total_ns, trace.gpu->total_ns);
}
}
struct Async_Render_Backend::Implementation : std::enable_shared_from_this<Implementation> {
struct Async_Render_Backend::Implementation
: std::enable_shared_from_this<Implementation> {
struct Completion {
Frame_3D* output{}; /* 调用方持有到本项回调结束。 */
Frame_Callback callback{}; /* 提交时捕获,避免后来替换出口改变历史语义。 */
};
struct Pending {
std::optional<Datoviz_Visual_Backend::Pending_Frame> backend_frame{};
std::vector<Completion> completions{}; /* 一次实际 GPU 图需要结清的全部逻辑提交。 */
Extent extent{};
Frame_3D* output{}; /* 调用方持有到本项回调返回。 */
Frame_Callback callback{}; /* render 入队时捕获的完成出口。 */
};
struct Submission {
Prepared_Visual_Batch visuals{}; /* 最新提交的大批量不可变 Visual 快照。 */
Scene_3D_Parameters parameters{}; /* 小型 Scene 参数按值复制进闭包。 */
std::vector<Completion> completions{}; /* 被最新画面合并的历史完成项。 */
Prepared_Visual_Batch visuals{}; /* Scene CPU Prepare 发布的大批量不可变快照。 */
Scene_3D_Parameters parameters{}; /* 本帧 Scene、Camera 与轴参数快照。 */
std::vector<Completion> completions{}; /* 合并到同一实际画面的逻辑帧。 */
};
std::shared_ptr<Render_Domain> render_domain;
std::unique_ptr<Datoviz_Visual_Backend> backend;
std::uint32_t gpu_index{};
bool validation_enabled{};
std::vector<Visual_Registration> visual_registrations{};
mutable std::mutex render_mutex;
std::optional<Submission> pending_submission{}; /* 唯一待提交画面;旧大数据由新快照直接替换。 */
Frame_Callback frame_callback{};
bool frame_in_flight{};
mutable std::mutex failure_mutex;
std::exception_ptr failure{};
std::atomic_bool available{};
Implementation(std::uint32_t gpu_index_value, bool validation_enabled_value,
struct Pending {
Datoviz_Visual_Backend::Pending_Frame backend_frame{}; /* 三缓冲目标对应的已录制提交。 */
std::vector<Completion> completions{}; /* GPU 完成时一起结清的逻辑帧。 */
Extent extent{}; /* 失败时生成空结果所需的尺寸。 */
};
struct Gpu_Completion {
std::shared_ptr<Pending> pending{}; /* 完成前保持帧目标和回调集合存活。 */
std::optional<Gpu_Completion_Service::Result> result{}; /* fence 正常交付时的结果。 */
std::exception_ptr failure{}; /* 提交域或完成服务的 Unknown Failure。 */
};
struct Event_Command {
std::shared_ptr<const Event> event{}; /* Scene 转移给准备域的输入事件。 */
Extent viewport{}; /* 事件产生时的物理像素视口。 */
};
struct Stop_Command {};
using Command = std::variant<Submission, Gpu_Completion,
Event_Command, Stop_Command>;
static constexpr std::size_t command_capacity = 64;
std::shared_ptr<Render_Domain> render_domain; /* 同 GPU 唯一的轻量 Queue Submit 域。 */
std::unique_ptr<Datoviz_Visual_Backend> backend{}; /* 仅准备线程构造、准备、收集和销毁。 */
std::uint32_t gpu_index{}; /* Datoviz 共享 GPU Context 的设备下标。 */
bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */
std::vector<Visual_Registration> visual_registrations{}; /* 首帧初始化后释放的稳定 Visual 注册表。 */
moodycamel::BlockingConcurrentQueue<Command> commands{command_capacity}; /* 多生产者到单准备线程的命令入口。 */
std::thread preparation_thread{}; /* 每 Scene 独立的 Datoviz CPU 准备域。 */
std::optional<Submission> deferred_submission{}; /* 三槽占满时仅保留的最新画面和历史回调。 */
std::mutex callback_mutex{}; /* 保护完成回调替换与 render 捕获。 */
Frame_Callback frame_callback{}; /* 后续 render 捕获的完成出口。 */
std::mutex failure_mutex{}; /* 保护跨线程传播的最后 Unknown Failure。 */
std::exception_ptr failure{}; /* 后端不可用的根因。 */
std::atomic_bool available{true}; /* 对 Scene 发布的后端可接收状态。 */
std::atomic_bool stop_requested{}; /* 拒绝新命令并等待在途槽结清。 */
Implementation(std::uint32_t gpu_index_value,
bool validation_enabled_value,
std::vector<Visual_Registration> visuals)
: render_domain(Render_Domain::acquire(gpu_index_value)),
gpu_index(gpu_index_value), validation_enabled(validation_enabled_value),
visual_registrations(std::move(visuals)) {
available.store(true, std::memory_order_release);
}
~Implementation() {
available.store(false, std::memory_order_release);
if (!backend) return;
const auto destroyed = render_domain->invoke([this] { backend.reset(); });
if (!destroyed) backend.release();
}
gpu_index(gpu_index_value),
validation_enabled(validation_enabled_value),
visual_registrations(std::move(visuals)),
preparation_thread([this] { run(); }) {}
~Implementation() { stop(); }
void stop() noexcept;
void run() noexcept;
void fail(std::exception_ptr value) noexcept;
void enqueue(Command command);
void initialize(const Scene_3D_Parameters& parameters);
void render(Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters,
Frame_3D* frame);
void submit(Submission submission);
void finish(std::shared_ptr<Pending> pending,
Gpu_Completion_Service::Result completion) noexcept;
void render(Prepared_Visual_Batch visuals,
Scene_3D_Parameters parameters, Frame_3D* frame);
void accept(Submission submission);
void merge_deferred(Submission submission);
void prepare(Submission submission);
void submit(std::shared_ptr<Pending> pending);
void finish(Gpu_Completion completion);
void resolve(std::shared_ptr<Pending> pending,
std::optional<Datoviz_Visual_Backend::Completed_Frame> completed);
void complete(std::shared_ptr<Pending> pending);
static std::vector<Frame_3D*> frames(const Pending& pending);
void complete(std::vector<Completion> completions);
void dispatch(Event_Command command);
};
Async_Render_Backend::Async_Render_Backend(
std::uint32_t gpu_index, bool validation_enabled,
std::vector<Visual_Registration> visuals)
: implementation_(std::make_shared<Implementation>(
gpu_index, validation_enabled, std::move(visuals))) {}
Async_Render_Backend::~Async_Render_Backend() = default;
void Async_Render_Backend::Implementation::fail(std::exception_ptr value) noexcept {
void Async_Render_Backend::Implementation::stop() noexcept {
available.store(false, std::memory_order_release);
if (!stop_requested.exchange(true, std::memory_order_acq_rel)) {
try {
enqueue(Stop_Command{});
}
catch (...) {
std::terminate();
}
}
if (preparation_thread.joinable()) preparation_thread.join();
}
void Async_Render_Backend::Implementation::fail(
std::exception_ptr value) noexcept {
{
std::lock_guard lock(failure_mutex);
failure = std::move(value);
}
available.store(false, std::memory_order_release);
}
void Async_Render_Backend::Implementation::enqueue(Command command) {
if (!commands.enqueue(std::move(command))) throw std::bad_alloc{};
}
void Async_Render_Backend::Implementation::initialize(
const Scene_3D_Parameters& parameters) {
if (backend) return;
@@ -109,149 +156,154 @@ void Async_Render_Backend::Implementation::initialize(
visual_registrations.clear();
visual_registrations.shrink_to_fit();
}
std::vector<Frame_3D*> Async_Render_Backend::Implementation::frames(
const Pending& pending) {
std::vector<Frame_3D*> result;
result.reserve(pending.completions.size());
for (const auto& completion : pending.completions)
result.push_back(completion.output);
return result;
}
void Async_Render_Backend::Implementation::render(
Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters,
Frame_3D* frame) {
if (!frame)
throw std::invalid_argument("3D backend requires a non-null external frame");
if (!available.load(std::memory_order_acquire)) return;
frame->mark(Frame_Trace_Marker::backend_queue_entered);
std::optional<Submission> submission;
Frame_Callback callback;
{
std::lock_guard lock(render_mutex);
Completion completion{frame, frame_callback};
if (frame_in_flight) {
Submission latest{std::move(visuals), parameters, {}};
if (pending_submission)
latest.completions = std::move(pending_submission->completions);
latest.completions.push_back(std::move(completion));
pending_submission = std::move(latest);
std::lock_guard lock(callback_mutex);
callback = frame_callback;
}
frame->mark(Frame_Trace_Marker::backend_queue_entered);
enqueue(Submission{std::move(visuals), parameters,
{Completion{frame, std::move(callback)}}});
}
void Async_Render_Backend::Implementation::merge_deferred(
Submission submission) {
if (deferred_submission) {
submission.completions.insert(
submission.completions.begin(),
std::make_move_iterator(deferred_submission->completions.begin()),
std::make_move_iterator(deferred_submission->completions.end()));
}
deferred_submission = std::move(submission);
}
void Async_Render_Backend::Implementation::accept(Submission submission) {
if (stop_requested.load(std::memory_order_acquire) ||
!available.load(std::memory_order_acquire)) {
auto pending = std::make_shared<Pending>();
pending->extent = submission.parameters.viewport;
pending->completions = std::move(submission.completions);
resolve(std::move(pending), std::nullopt);
return;
}
frame_in_flight = true;
submission.emplace(Submission{std::move(visuals), parameters,
{std::move(completion)}});
if (backend &&
!backend->can_prepare(submission.parameters, submission.visuals)) {
merge_deferred(std::move(submission));
return;
}
submit(std::move(*submission));
prepare(std::move(submission));
}
void Async_Render_Backend::Implementation::submit(Submission submission) {
auto self = shared_from_this();
auto pending = std::make_shared<Pending>();
pending->completions = std::move(submission.completions);
pending->extent = submission.parameters.viewport;
const auto sequence = pending->completions.back().output->identity().sequence;
struct Submission_Task {
Prepared_Visual_Batch visuals;
Scene_3D_Parameters parameters;
std::uint64_t sequence{};
std::shared_ptr<Pending> pending;
};
auto task = std::make_shared<Submission_Task>(Submission_Task{
std::move(submission.visuals), submission.parameters, sequence, pending});
const auto queued = render_domain->post(
[self, task] {
for (const auto& completion : task->pending->completions)
completion.output->mark(Frame_Trace_Marker::backend_queue_left);
self->initialize(task->parameters);
void Async_Render_Backend::Implementation::prepare(Submission submission) {
std::optional<Datoviz_Visual_Backend::Pending_Frame> prepared;
try {
initialize(submission.parameters);
for (const auto& completion : submission.completions)
completion.output->mark(Frame_Trace_Marker::backend_prepare_started);
const auto sequence =
submission.completions.back().output->identity().sequence;
const bool readback = std::ranges::any_of(
task->pending->completions,
[](const Completion& completion) {
submission.completions, [](const Completion& completion) {
return completion.output->output() == Frame_3D_Output::pixels;
});
auto reservation = Gpu_Completion_Service::instance().prepare(
[self, pending = task->pending](Gpu_Completion_Service::Result result) {
self->finish(pending, std::move(result));
},
[self, pending = task->pending](std::exception_ptr value) {
self->fail(std::move(value));
self->resolve(pending, std::nullopt);
}, true);
if (!reservation) {
self->resolve(task->pending, std::nullopt);
return;
}
task->pending->backend_frame = self->backend->submit(
task->parameters, task->visuals, task->sequence, true, readback);
if (!task->pending->backend_frame) {
self->resolve(task->pending, std::nullopt);
return;
}
for (const auto& completion : task->pending->completions) {
record_datoviz_trace(completion.output,
task->pending->backend_frame->trace);
completion.output->mark(Frame_Trace_Marker::gpu_submitted);
}
reservation.reservation.watch(task->pending->backend_frame->device,
task->pending->backend_frame->fence);
},
[self, pending](std::exception_ptr value) {
self->fail(std::move(value));
self->resolve(pending, std::nullopt);
});
if (queued != Render_Domain::Post_Result::queued) {
fail(std::make_exception_ptr(
std::runtime_error("render domain stopped before 3D submission")));
resolve(std::move(pending), std::nullopt);
}
}
void Async_Render_Backend::Implementation::finish(
std::shared_ptr<Pending> pending,
Gpu_Completion_Service::Result completion) noexcept {
for (const auto& item : pending->completions) {
item.output->mark(Frame_Trace_Marker::gpu_completed);
item.output->record(Frame_Trace_Measurement::gpu_fence_wait_ns,
completion.wait_duration_ns);
}
auto self = shared_from_this();
try {
const auto result = render_domain->post(
[self, pending, completion] {
if (!pending->backend_frame) {
self->resolve(pending, std::nullopt);
return;
}
if (completion.error !=
Gpu_Completion_Service::Completion_Error::none) {
self->backend->discard(std::move(*pending->backend_frame));
pending->backend_frame.reset();
self->resolve(pending, std::nullopt);
return;
}
for (const auto& item : pending->completions)
item.output->mark(Frame_Trace_Marker::readback_started);
auto completed = self->backend->collect(
std::move(*pending->backend_frame));
pending->backend_frame.reset();
self->resolve(pending, std::move(completed));
},
[self, pending](std::exception_ptr value) {
self->fail(std::move(value));
self->resolve(pending, std::nullopt);
});
if (result != Render_Domain::Post_Result::queued) {
fail(std::make_exception_ptr(std::runtime_error(
"render domain stopped before GPU completion collection")));
resolve(std::move(pending), std::nullopt);
}
prepared = backend->prepare(
submission.parameters, submission.visuals, sequence, true, readback);
}
catch (...) {
fail(std::current_exception());
auto pending = std::make_shared<Pending>();
pending->extent = submission.parameters.viewport;
pending->completions = std::move(submission.completions);
resolve(std::move(pending), std::nullopt);
return;
}
if (!prepared) {
merge_deferred(std::move(submission));
return;
}
for (const auto& completion : submission.completions) {
completion.output->mark(Frame_Trace_Marker::backend_prepare_finished);
record_datoviz_trace(completion.output, prepared->trace);
completion.output->mark(Frame_Trace_Marker::backend_submit_queued);
}
auto pending = std::make_shared<Pending>();
pending->backend_frame = std::move(*prepared);
pending->extent = submission.parameters.viewport;
pending->completions = std::move(submission.completions);
try {
submit(pending);
}
catch (...) {
backend->discard(std::move(pending->backend_frame));
fail(std::current_exception());
resolve(std::move(pending), std::nullopt);
}
}
void Async_Render_Backend::Implementation::submit(
std::shared_ptr<Pending> pending) {
auto self = shared_from_this();
const auto queued = render_domain->post(
[self, pending] {
for (const auto& completion : pending->completions)
completion.output->mark(Frame_Trace_Marker::backend_queue_left);
auto reservation = Gpu_Completion_Service::instance().prepare(
[self, pending](Gpu_Completion_Service::Result result) {
try {
self->enqueue(Gpu_Completion{pending, result, {}});
}
catch (...) {
self->fail(std::current_exception());
}
},
[self, pending](std::exception_ptr value) {
try {
self->enqueue(Gpu_Completion{pending, std::nullopt,
std::move(value)});
}
catch (...) {
self->fail(std::current_exception());
}
}, true);
if (!reservation) {
self->enqueue(Gpu_Completion{
pending, std::nullopt,
std::make_exception_ptr(std::runtime_error(
"GPU completion service stopped before submission"))});
return;
}
self->backend->submit(pending->backend_frame);
for (const auto& completion : pending->completions)
completion.output->mark(Frame_Trace_Marker::gpu_submitted);
reservation.reservation.watch(pending->backend_frame.device,
pending->backend_frame.fence);
},
[self, pending](std::exception_ptr value) {
try {
self->enqueue(Gpu_Completion{pending, std::nullopt,
std::move(value)});
}
catch (...) {
self->fail(std::current_exception());
}
});
if (queued != Render_Domain::Post_Result::queued) {
enqueue(Gpu_Completion{
std::move(pending), std::nullopt,
std::make_exception_ptr(std::runtime_error(
"render domain stopped before 3D queue submission"))});
}
}
void Async_Render_Backend::Implementation::resolve(
std::shared_ptr<Pending> pending,
std::optional<Datoviz_Visual_Backend::Completed_Frame> completed) {
std::vector<Frame_3D*> outputs;
outputs.reserve(pending->completions.size());
for (const auto& completion : pending->completions)
outputs.push_back(completion.output);
if (completed) {
auto outputs = frames(*pending);
for (Frame_3D* output : outputs) {
output->mark(Frame_Trace_Marker::readback_finished);
record_datoviz_trace(output, completed->trace);
@@ -262,48 +314,14 @@ void Async_Render_Backend::Implementation::resolve(
else {
const auto pixel_count = static_cast<std::size_t>(pending->extent.width) *
pending->extent.height * 4U;
auto outputs = frames(*pending);
Frame_3D_Access::assign_pixels(outputs, pending->extent,
std::vector<std::byte>(pixel_count));
Frame_3D_Access::assign_pixels(
outputs, pending->extent, std::vector<std::byte>(pixel_count));
}
complete(std::move(pending));
complete(std::move(pending->completions));
}
void Async_Render_Backend::Implementation::complete(
std::shared_ptr<Pending> pending) {
std::optional<Submission> next;
const std::size_t rendered_count = pending->completions.size();
{
std::lock_guard lock(render_mutex);
frame_in_flight = false;
if (pending_submission && available.load(std::memory_order_acquire)) {
next = std::move(pending_submission);
pending_submission.reset();
frame_in_flight = true;
}
else if (pending_submission) {
auto historical = std::move(pending_submission->completions);
pending_submission.reset();
pending->completions.insert(
pending->completions.end(),
std::make_move_iterator(historical.begin()),
std::make_move_iterator(historical.end()));
}
}
if (pending->completions.size() > rendered_count && rendered_count != 0) {
Frame_3D* source = pending->completions.front().output;
for (std::size_t index = rendered_count;
index < pending->completions.size(); ++index) {
try {
Frame_3D_Access::share_pixels(
source, pending->completions[index].output);
}
catch (...) {
std::lock_guard lock(failure_mutex);
failure = std::current_exception();
}
}
}
for (auto& item : pending->completions) {
std::vector<Completion> completions) {
for (auto& item : completions) {
if (!item.output || !item.callback) continue;
item.output->mark(Frame_Trace_Marker::callback_started);
try {
@@ -315,15 +333,127 @@ void Async_Render_Backend::Implementation::complete(
}
item.output->mark(Frame_Trace_Marker::callback_finished);
}
if (next) submit(std::move(*next));
}
void Async_Render_Backend::Implementation::finish(Gpu_Completion completion) {
for (const auto& item : completion.pending->completions) {
item.output->mark(Frame_Trace_Marker::gpu_completed);
if (completion.result)
item.output->record(Frame_Trace_Measurement::gpu_fence_wait_ns,
completion.result->wait_duration_ns);
item.output->mark(Frame_Trace_Marker::readback_started);
}
std::optional<Datoviz_Visual_Backend::Completed_Frame> completed;
if (completion.failure) {
fail(std::move(completion.failure));
backend->discard(std::move(completion.pending->backend_frame));
}
else if (!completion.result ||
completion.result->error !=
Gpu_Completion_Service::Completion_Error::none) {
backend->discard(std::move(completion.pending->backend_frame));
}
else {
completed = backend->collect(
std::move(completion.pending->backend_frame));
}
resolve(std::move(completion.pending), std::move(completed));
if (deferred_submission &&
!stop_requested.load(std::memory_order_acquire) &&
available.load(std::memory_order_acquire)) {
auto next = std::move(*deferred_submission);
deferred_submission.reset();
prepare(std::move(next));
}
else if (deferred_submission &&
!available.load(std::memory_order_acquire)) {
auto pending = std::make_shared<Pending>();
pending->extent = deferred_submission->parameters.viewport;
pending->completions = std::move(deferred_submission->completions);
deferred_submission.reset();
resolve(std::move(pending), std::nullopt);
}
}
void Async_Render_Backend::Implementation::dispatch(Event_Command command) {
if (!backend || !command.event) return;
const auto* pointer =
dynamic_cast<const Pointer_Event_Capability*>(command.event.get());
const auto* wheel =
dynamic_cast<const Wheel_Event_Capability*>(command.event.get());
const auto* key = dynamic_cast<const Key_Event*>(command.event.get());
if (wheel) {
backend->dispatch_wheel(
static_cast<float>(pointer->position_x()),
static_cast<float>(pointer->position_y()),
wheel_step(wheel->pixel_delta_x_value(),
wheel->angle_delta_x_value()),
wheel_step(wheel->pixel_delta_y_value(),
wheel->angle_delta_y_value()),
pointer->keyboard_modifiers(), command.viewport);
}
else if (pointer) {
backend->dispatch_pointer(
command.event->type,
static_cast<float>(pointer->position_x()),
static_cast<float>(pointer->position_y()),
command.event->type == Event_Type::pointer_move
? held_button(pointer->pointer_buttons())
: pointer->pointer_button(),
pointer->keyboard_modifiers(), command.viewport);
}
else if (key) backend->dispatch_key(*key);
command.event->accept();
}
void Async_Render_Backend::Implementation::run() noexcept {
bool stopping{};
for (;;) {
Command command;
commands.wait_dequeue(command);
try {
if (auto* submission = std::get_if<Submission>(&command))
accept(std::move(*submission));
else if (auto* completion = std::get_if<Gpu_Completion>(&command))
finish(std::move(*completion));
else if (auto* event = std::get_if<Event_Command>(&command))
dispatch(std::move(*event));
else stopping = true;
}
catch (...) {
fail(std::current_exception());
if (auto* submission = std::get_if<Submission>(&command)) {
auto pending = std::make_shared<Pending>();
pending->extent = submission->parameters.viewport;
pending->completions = std::move(submission->completions);
try { resolve(std::move(pending), std::nullopt); }
catch (...) { fail(std::current_exception()); }
}
else if (auto* completion = std::get_if<Gpu_Completion>(&command)) {
try { resolve(std::move(completion->pending), std::nullopt); }
catch (...) { fail(std::current_exception()); }
}
}
if (stopping && (!backend || backend->idle())) {
if (deferred_submission) {
auto pending = std::make_shared<Pending>();
pending->extent = deferred_submission->parameters.viewport;
pending->completions =
std::move(deferred_submission->completions);
deferred_submission.reset();
try { resolve(std::move(pending), std::nullopt); }
catch (...) { fail(std::current_exception()); }
}
backend.reset();
return;
}
}
}
void Async_Render_Backend::render(
Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters,
Frame_3D* frame) {
implementation_->render(std::move(visuals), parameters, frame);
}
void Async_Render_Backend::set_frame_callback(Frame_Callback callback) {
std::lock_guard lock(implementation_->render_mutex);
std::lock_guard lock(implementation_->callback_mutex);
implementation_->frame_callback = std::move(callback);
}
bool Async_Render_Backend::available() const noexcept {
@@ -353,39 +483,13 @@ Async_Render_Backend::dispatch_event(std::shared_ptr<const Event> event,
return Dispatch_Event_Result::invalid_event;
}
else if (!key) return Dispatch_Event_Result::ignored;
const auto queued = self->render_domain->post(
[self, event = std::move(event), viewport] {
if (!self->backend) return;
if (const auto* event_wheel =
dynamic_cast<const Wheel_Event_Capability*>(event.get())) {
const auto* event_pointer =
dynamic_cast<const Pointer_Event_Capability*>(event.get());
self->backend->dispatch_wheel(
static_cast<float>(event_pointer->position_x()),
static_cast<float>(event_pointer->position_y()),
wheel_step(event_wheel->pixel_delta_x_value(),
event_wheel->angle_delta_x_value()),
wheel_step(event_wheel->pixel_delta_y_value(),
event_wheel->angle_delta_y_value()),
event_pointer->keyboard_modifiers(), viewport);
try {
self->enqueue(Implementation::Event_Command{std::move(event), viewport});
}
else if (const auto* event_pointer =
dynamic_cast<const Pointer_Event_Capability*>(event.get()))
self->backend->dispatch_pointer(
event->type,
static_cast<float>(event_pointer->position_x()),
static_cast<float>(event_pointer->position_y()),
event->type == Event_Type::pointer_move
? held_button(event_pointer->pointer_buttons())
: event_pointer->pointer_button(),
event_pointer->keyboard_modifiers(), viewport);
else if (const auto* event_key = dynamic_cast<const Key_Event*>(event.get()))
self->backend->dispatch_key(*event_key);
event->accept();
},
[self](std::exception_ptr value) { self->fail(std::move(value)); });
return queued == Render_Domain::Post_Result::queued
? Dispatch_Event_Result::queued
: Dispatch_Event_Result::backend_unavailable;
catch (...) {
self->fail(std::current_exception());
return Dispatch_Event_Result::backend_unavailable;
}
return Dispatch_Event_Result::queued;
}
}
@@ -201,6 +201,7 @@ public:
}
[[nodiscard]] DvzGpuCtx* gpu_context() const noexcept { return gpu_context_; }
[[nodiscard]] std::mutex& api_mutex() noexcept { return api_mutex_; }
private:
Datoviz_Render_Context(std::uint32_t gpu_index, bool validation_enabled) {
@@ -214,6 +215,7 @@ private:
}
DvzGpuCtx* gpu_context_{}; /* 共享 GPU Device 与分配器的唯一所有权。 */
std::mutex api_mutex_{}; /* 同 GPU Datoviz/Vulkan API 与 Frame Target 状态的唯一串行化域。 */
};
class Datoviz_Visual_Backend::Frame_Target final {
@@ -278,7 +280,8 @@ public:
destroy();
}
void begin(bool observe, bool readback) {
if (in_flight_) throw std::logic_error("Datoviz frame target is still in flight");
if (recording_ || prepared_ || in_flight_)
throw std::logic_error("Datoviz frame target is not available");
observing_ = observe;
readback_requested_ = readback;
if (observing_ && !timestamps_initialized_) {
@@ -352,7 +355,7 @@ public:
frame.wait_semaphore_fd = -1;
return frame;
}
void submit() {
void finish_recording() {
if (!recording_) throw std::logic_error("Datoviz frame target is not recording");
const VkCommandBuffer command_buffer = dvz_commands_handle(commands_);
if (observing_ && timestamps_supported_) {
@@ -414,10 +417,16 @@ public:
dvz_fence_reset(fence_);
dvz_submit(submit_);
dvz_submit_command(submit_, dvz_commands_handle(commands_));
prepared_ = true;
}
void submit() {
if (!prepared_ || in_flight_)
throw std::logic_error("Datoviz frame target has no prepared submission");
DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN);
if (dvz_submit_send(submit_, dvz_queue_handle(queue),
dvz_fence_handle(fence_)) != VK_SUCCESS)
throw std::runtime_error("failed to submit Datoviz point frame");
prepared_ = false;
in_flight_ = true;
completed_layout_ = readback_requested_ ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
}
@@ -443,7 +452,14 @@ public:
return result;
}
void discard_after_completion() {
if (!in_flight_) throw std::logic_error("Datoviz frame target has no pending frame");
if (prepared_) {
prepared_ = false;
observing_ = false;
readback_requested_ = false;
return;
}
if (!in_flight_)
throw std::logic_error("Datoviz frame target has no pending frame");
in_flight_ = false;
observing_ = false;
readback_requested_ = false;
@@ -457,9 +473,14 @@ public:
[[nodiscard]] std::uint64_t generation() const noexcept {
return generation_;
}
[[nodiscard]] Extent extent() const noexcept { return extent_; }
[[nodiscard]] bool available() const noexcept {
return !recording_ && !prepared_ && !in_flight_;
}
void abort() noexcept {
if (recording_ && commands_ != nullptr) dvz_cmd_reset(commands_);
recording_ = false;
prepared_ = false;
observing_ = false;
readback_requested_ = false;
}
@@ -544,9 +565,9 @@ private:
return timing;
}
void destroy() {
if (in_flight_)
if (recording_ || prepared_ || in_flight_)
throw std::logic_error(
"Datoviz frame target is still in flight during destruction");
"Datoviz frame target is busy during destruction");
if (query_pool_ != VK_NULL_HANDLE && gpu_context_ != nullptr) {
vkDestroyQueryPool(
dvz_device_handle(dvz_gpu_ctx_device(gpu_context_)),
@@ -598,18 +619,26 @@ private:
float timestamp_period_ns_{};
std::uint32_t timestamp_valid_bits_{};
bool recording_{};
bool prepared_{};
bool in_flight_{};
bool observing_{};
bool readback_requested_{};
bool timestamps_initialized_{};
bool timestamps_supported_{};
};
struct Datoviz_Visual_Backend::Frame_Targets {
static constexpr std::size_t count = 3;
std::array<std::unique_ptr<Frame_Target>, count> values{}; /* 固定三槽,槽地址在后端生命周期内稳定。 */
};
Datoviz_Visual_Backend::Datoviz_Visual_Backend(
std::uint32_t gpu_index, bool validation_enabled,
const std::vector<Visual_Registration>& visuals,
const Scene_3D_Parameters& initial_scene) : domain_thread_(std::this_thread::get_id()) {
const Scene_3D_Parameters& initial_scene)
: domain_thread_(std::this_thread::get_id()),
targets_(std::make_unique<Frame_Targets>()) {
try {
render_context_ = Datoviz_Render_Context::acquire(gpu_index, validation_enabled);
std::lock_guard api_lock(render_context_->api_mutex());
auto* gpu_context = render_context_->gpu_context();
DvzDrp2RuntimeConfig runtime_configuration = dvz_drp2_runtime_vklite_config(
dvz_gpu_ctx_device(gpu_context), dvz_gpu_ctx_alloc(gpu_context));
@@ -1082,7 +1111,7 @@ void Datoviz_Visual_Backend::apply_camera(const plot::Camera_Descriptor& source)
void Datoviz_Visual_Backend::apply(
const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared) {
require_domain();
if (target_extent_ != scene.viewport) {
if (figure_extent_ != scene.viewport) {
if (dvz_figure_resize(figure_, scene.viewport.width,
scene.viewport.height) != DVZ_OK)
throw std::runtime_error("failed to resize Datoviz point figure");
@@ -1091,6 +1120,7 @@ void Datoviz_Visual_Backend::apply(
scene.viewport.width, scene.viewport.height, 1.0F, 1.0F
};
dvz_input_emit_resize(input_router_, &resize);
figure_extent_ = scene.viewport;
}
apply_camera(scene.camera);
apply_axes(scene);
@@ -1306,6 +1336,8 @@ void Datoviz_Visual_Backend::dispatch_pointer(
::aethera::Mouse_Button mouse_button,
::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) {
require_domain();
std::lock_guard api_lock(render_context_->api_mutex());
input_changed_ = true;
if (applied_camera_ && applied_camera_->controller == plot::Camera_Controller::turntable) {
if (mouse_button == ::aethera::Mouse_Button::left &&
!applied_camera_->turntable_control.rotate_enabled) return;
@@ -1330,6 +1362,8 @@ void Datoviz_Visual_Backend::dispatch_wheel(
float x, float y, float delta_x, float delta_y,
::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) {
require_domain();
std::lock_guard api_lock(render_context_->api_mutex());
input_changed_ = true;
if (applied_camera_ &&
applied_camera_->controller == plot::Camera_Controller::turntable &&
!applied_camera_->turntable_control.zoom_enabled) return;
@@ -1341,6 +1375,8 @@ void Datoviz_Visual_Backend::dispatch_wheel(
void Datoviz_Visual_Backend::dispatch_key(
const ::aethera::Key_Event& event) {
require_domain();
std::lock_guard api_lock(render_context_->api_mutex());
input_changed_ = true;
if (event.key == ::aethera::Key::home && event.type == ::aethera::Event_Type::key_press) {
DvzResult reset = DVZ_ERROR;
switch (dvz_controller_type(camera_controller_)) {
@@ -1400,11 +1436,38 @@ DvzSceneFrameArtifact* Datoviz_Visual_Backend::emit(
}
return artifact;
}
std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::submit(
std::optional<std::uint8_t> Datoviz_Visual_Backend::acquire_target(Extent extent) {
for (std::uint8_t index = 0; index < targets_->values.size(); ++index) {
auto& candidate = targets_->values[index];
if (candidate && candidate->available() && candidate->extent() == extent)
return index;
}
for (std::uint8_t index = 0; index < targets_->values.size(); ++index) {
auto& candidate = targets_->values[index];
if (candidate && !candidate->available()) continue;
candidate = std::make_unique<Frame_Target>(
render_context_->gpu_context(), extent, ++target_generation_);
return index;
}
return std::nullopt;
}
Datoviz_Visual_Backend::Frame_Target& Datoviz_Visual_Backend::target(
const Pending_Frame& pending) {
if (pending.target_index >= targets_->values.size())
throw std::logic_error("Datoviz pending frame target index is invalid");
auto& result = targets_->values[pending.target_index];
if (!result || result->generation() != pending.target_generation)
throw std::logic_error("Datoviz pending frame target no longer exists");
return *result;
}
std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::prepare(
const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals,
std::uint64_t frame_sequence, bool observe, bool readback) {
require_domain();
std::lock_guard api_lock(render_context_->api_mutex());
if (scene.viewport.empty()) return std::nullopt;
const auto target_index = acquire_target(scene.viewport);
if (!target_index) return std::nullopt;
Datoviz_Frame_Trace trace;
trace.render_sequence = frame_sequence;
trace.observed = observe;
@@ -1442,13 +1505,13 @@ std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::sub
}
}
if (observe) trace.apply_ns = trace_now_ns() - phase_started;
if (target_ == nullptr || target_extent_ != scene.viewport) {
target_.reset();
target_extent_ = scene.viewport;
target_ = std::make_unique<Frame_Target>(render_context_->gpu_context(), target_extent_,
++target_generation_);
}
target_->begin(observe, readback);
auto& frame_target = *targets_->values[*target_index];
frame_target.begin(observe, readback);
struct Recording_Scope {
Frame_Target& target; /* 异常退出时回收尚未发布的录制槽。 */
bool released{}; /* finish_recording 成功后禁止回滚。 */
~Recording_Scope() { if (!released) target.abort(); }
} recording_scope{frame_target};
DvzSceneFrameArtifact* artifact{};
try {
if (observe) phase_started = trace_now_ns();
@@ -1456,7 +1519,7 @@ std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::sub
if (observe) trace.emit_ns = trace_now_ns() - phase_started;
}
catch (...) {
target_->abort();
frame_target.abort();
raise_context("preparing Datoviz frame", std::current_exception());
}
if (observe) {
@@ -1468,7 +1531,7 @@ std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::sub
dvz_scene_frame_artifact_frame_index(artifact);
}
const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact);
const DvzStreamFrame target_frame = target_->stream_frame();
const DvzStreamFrame target_frame = frame_target.stream_frame();
if (observe) phase_started = trace_now_ns();
const bool attached = stream != nullptr &&
dvz_drp2_runtime_attach_frame_target(
@@ -1481,7 +1544,7 @@ std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::sub
trace.validation_ok = attached && result.ok;
trace.validation_code = static_cast<std::uint32_t>(result.code);
trace.validation_command_index = result.command_index;
if (observe || !trace.validation_ok) {
if (!trace.validation_ok) {
if (char* json = dvz_scene_frame_artifact_json(
artifact, "renderive_frame")) {
trace.artifact_json = json;
@@ -1490,11 +1553,11 @@ std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::sub
}
dvz_scene_frame_artifact_destroy(artifact);
if (!attached) {
target_->abort();
frame_target.abort();
throw std::runtime_error("failed to attach the Datoviz point frame target");
}
if (!result.ok) {
target_->abort();
frame_target.abort();
std::string message =
"failed to execute Datoviz point frame: validation code " +
std::to_string(static_cast<unsigned>(result.code)) +
@@ -1506,45 +1569,89 @@ std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::sub
}
throw std::runtime_error(std::move(message));
}
if (observe) phase_started = trace_now_ns();
target_->submit();
if (observe) trace.submit_ns = trace_now_ns() - phase_started;
frame_target.finish_recording();
recording_scope.released = true;
input_changed_ = false;
return Pending_Frame{
target_->device(), target_->fence(), scene.viewport,
frame_sequence, target_->generation(),
frame_target.device(), frame_target.fence(), scene.viewport,
frame_sequence, *target_index, frame_target.generation(),
std::move(trace)
};
}
void Datoviz_Visual_Backend::submit(Pending_Frame& pending) {
std::lock_guard api_lock(render_context_->api_mutex());
const std::uint64_t started = pending.trace.observed ? trace_now_ns() : 0;
target(pending).submit();
if (pending.trace.observed)
pending.trace.submit_ns = trace_now_ns() - started;
}
Datoviz_Visual_Backend::Completed_Frame Datoviz_Visual_Backend::collect(
Pending_Frame pending) {
require_domain();
if (target_ == nullptr ||
target_->generation() != pending.target_generation)
throw std::logic_error("Datoviz pending frame target no longer exists");
std::lock_guard api_lock(render_context_->api_mutex());
const std::uint64_t readback_started = pending.trace.observed
? trace_now_ns()
: 0;
auto collection = target_->collect();
auto collection = target(pending).collect();
if (pending.trace.observed) pending.trace.readback_ns = trace_now_ns() - readback_started;
pending.trace.gpu = std::move(collection.gpu_timing);
return {pending.extent, std::move(collection.pixels), std::move(pending.trace)};
}
void Datoviz_Visual_Backend::discard(Pending_Frame pending) {
require_domain();
if (target_ == nullptr ||
target_->generation() != pending.target_generation)
throw std::logic_error("Datoviz pending frame target no longer exists");
target_->discard_after_completion();
std::lock_guard api_lock(render_context_->api_mutex());
target(pending).discard_after_completion();
}
bool Datoviz_Visual_Backend::can_prepare(
const Scene_3D_Parameters& scene,
const Prepared_Visual_Batch& prepared) const {
if (!render_context_) return false;
std::lock_guard api_lock(render_context_->api_mutex());
const bool target_available = std::ranges::any_of(
targets_->values, [](const auto& target) {
return !target || target->available();
});
if (!target_available) return false;
const bool all_targets_available = std::ranges::all_of(
targets_->values, [](const auto& target) {
return !target || target->available();
});
if (all_targets_available) return true;
if (input_changed_ || figure_extent_ != scene.viewport ||
!applied_camera_ || *applied_camera_ != scene.camera ||
!applied_axes_ || *applied_axes_ !=
std::array{scene.x_axis, scene.y_axis, scene.z_axis})
return false;
for (const auto& source : prepared) {
const auto target = std::ranges::find_if(
visuals_, [&](const Visual_Instance& value) {
return value.identity == source.identity;
});
if (target == visuals_.end() ||
target->applied_revision != source.visual.revision)
return false;
}
return prepared.size() == visuals_.size();
}
bool Datoviz_Visual_Backend::idle() const {
if (!render_context_) return true;
std::lock_guard api_lock(render_context_->api_mutex());
return std::ranges::all_of(targets_->values, [](const auto& target) {
return !target || target->available();
});
}
void Datoviz_Visual_Backend::destroy() {
if (std::this_thread::get_id() != domain_thread_)
throw std::logic_error(
"Datoviz backend may only be destroyed on the render domain");
auto context = render_context_;
std::unique_lock<std::mutex> api_lock;
if (context) api_lock = std::unique_lock(context->api_mutex());
if (runtime_ != nullptr) {
dvz_drp2_runtime_destroy(runtime_);
runtime_ = nullptr;
}
target_.reset();
for (auto& target : targets_->values) target.reset();
if (camera_controller_ != nullptr) {
dvz_controller_destroy(camera_controller_);
camera_controller_ = nullptr;
@@ -1578,6 +1685,8 @@ void Datoviz_Visual_Backend::destroy() {
scene_ = nullptr;
}
render_context_.reset();
if (api_lock.owns_lock()) api_lock.unlock();
context.reset();
}
} // namespace aethera::render_3d::detail
@@ -23,6 +23,7 @@ public:
VkFence fence{VK_NULL_HANDLE}; /* 标记本帧 GPU 完成的 fence。 */
Extent extent{}; /* 本帧离屏目标尺寸。 */
std::uint64_t sequence{}; /* Scene 分配的帧序号。 */
std::uint8_t target_index{}; /* 本帧独占的三缓冲目标槽位。 */
std::uint64_t target_generation{}; /* 防止复用过期目标的资源代次。 */
Datoviz_Frame_Trace trace; /* 本帧可选诊断数据。 */
};
@@ -37,11 +38,16 @@ public:
~Datoviz_Visual_Backend() noexcept(false);
Datoviz_Visual_Backend(const Datoviz_Visual_Backend&) = delete;
Datoviz_Visual_Backend& operator=(const Datoviz_Visual_Backend&) = delete;
[[nodiscard]] std::optional<Pending_Frame> submit(
[[nodiscard]] std::optional<Pending_Frame> prepare(
const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals,
std::uint64_t frame_sequence, bool observe, bool readback);
void submit(Pending_Frame& pending);
[[nodiscard]] Completed_Frame collect(Pending_Frame pending);
void discard(Pending_Frame pending);
[[nodiscard]] bool can_prepare(
const Scene_3D_Parameters& scene,
const Prepared_Visual_Batch& visuals) const;
[[nodiscard]] bool idle() const;
void dispatch_pointer(Event_Type type, float x, float y,
Mouse_Button button,
Keyboard_Modifier modifiers,
@@ -52,6 +58,7 @@ public:
void dispatch_key(const Key_Event& event);
private:
class Frame_Target;
struct Frame_Targets;
struct Visual_Instance {
Visual_Identity identity{}; /* Scene 注册的稳定身份。 */
Visual_Family family{Visual_Family::point}; /* 原生 Visual 的确定 family。 */
@@ -68,6 +75,8 @@ private:
void apply(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals);
void apply_visual(Visual_Instance& target, const Prepared_Visual& visual);
[[nodiscard]] DvzSceneFrameArtifact* emit(const Scene_3D_Parameters& scene);
[[nodiscard]] std::optional<std::uint8_t> acquire_target(Extent extent);
[[nodiscard]] Frame_Target& target(const Pending_Frame& pending);
void destroy();
std::thread::id domain_thread_; /* 唯一允许访问 Datoviz 对象的线程。 */
std::shared_ptr<Datoviz_Render_Context> render_context_; /* 同一 GPU 上所有后端共享的 Device 与分配器。 */
@@ -83,11 +92,12 @@ private:
DvzController* camera_controller_{}; /* 当前绑定到 Panel 的 Datoviz 原生相机控制器。 */
DvzInputRouter* input_router_{}; /* Scene 输入事件路由器。 */
DvzPointerGestureHandler* gesture_handler_{}; /* 指针手势解析器。 */
std::unique_ptr<Frame_Target> target_; /* 当前尺寸对应的离屏提交和读回目标。 */
Extent target_extent_{}; /* target_ 当前适配的像素尺寸。 */
std::unique_ptr<Frame_Targets> targets_; /* 三个可并行处于准备、GPU 和读回阶段的目标。 */
Extent figure_extent_{}; /* Datoviz Figure 当前应用的像素尺寸。 */
std::uint64_t target_generation_{}; /* 每次重建 target_ 时递增的资源代次。 */
std::optional<plot::Camera_Descriptor> applied_camera_{}; /* 已应用到 Panel 的 Camera 配置。 */
std::optional<std::array<plot::Axis_Descriptor, 3>> applied_axes_{}; /* 已生成 Visual 的轴描述快照。 */
bool input_changed_{}; /* 输入控制器是否产生尚未录入完成帧的资源更新。 */
};
} // namespace aethera::render_3d::detail
+30 -23
View File
@@ -41,7 +41,7 @@ enum class Frame_Pacing_Mode : std::uint32_t {
maximum_rate
};
struct Frame_Pacing_Properties {
Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; /* 控制后继 render 请求节奏的策略。 */
Frame_Pacing_Mode mode{Frame_Pacing_Mode::maximum_rate}; /* 控制后继 render 请求节奏的策略。 */
double fixed_rate_fps{30.0}; /* 固定频率策略的目标帧率,单位为 FPS。 */
double minimum_latency_headroom{1.25}; /* 最低延迟策略相对 P95 生成耗时的安全系数。 */
};
@@ -278,10 +278,9 @@ struct Plot::Private {
std::unordered_map<const void*, Frame_Handler> handlers; /* 以连接身份索引的完成帧订阅。 */
std::uint64_t next_frame_sequence{1}; /* 下一外部帧使用的单调序号;只在 strand 访问。 */
Frame_Policy frame_policy{}; /* 仅保存可编辑刷新策略,不保存衍生统计。 */
std::optional<Managed_Frame> active_frame{}; /* 当前由 Scene/异步后端借用指针的外部帧。 */
std::unordered_map<Render_Frame*, Managed_Frame> active_frames{}; /* Scene 借用中的外部帧,以回调原地址为唯一索引。 */
std::deque<const void*> pending_order{}; /* 按首次等待顺序保存连接身份,避免连接间饥饿。 */
std::unordered_map<const void*, Managed_Frame> pending_frames{}; /* 每个连接只保留最新一个尚未提交 Scene 的外部帧。 */
bool frame_in_flight{}; /* Scene 是否已有一次尚未完成回调的帧。 */
template <typename Scene_Object>
Private(asio::any_io_executor executor,
std::unique_ptr<Scene_Object> value_scene,
@@ -301,7 +300,8 @@ struct Plot::Private {
void request_frame(Frame_Submission submission);
void render_frame(Managed_Frame frame);
void publish_completed_frame(Render_Frame* frame);
void frame_completed();
void frame_completed(Render_Frame* frame);
[[nodiscard]] std::size_t frame_capacity() const noexcept;
};
nlohmann::json Plot::Private::schema() const {
auto result = view->schema();
@@ -329,7 +329,7 @@ Plot::Private::Managed_Frame Plot::Private::make_frame(Frame_Submission submissi
}
void Plot::Private::request_frame(Frame_Submission submission) {
auto frame = make_frame(std::move(submission));
if (frame_in_flight) {
if (active_frames.size() >= frame_capacity()) {
const auto found = pending_frames.find(frame.owner);
if (found != pending_frames.end()) found->second = std::move(frame);
else {
@@ -342,47 +342,54 @@ void Plot::Private::request_frame(Frame_Submission submission) {
render_frame(std::move(frame));
}
void Plot::Private::render_frame(Managed_Frame frame) {
frame_in_flight = true;
active_frame = std::move(frame);
auto& request = active_frame->request;
Render_Frame* identity = std::visit(
[](const auto& value) -> Render_Frame* { return value.get(); }, frame.frame);
auto [active, inserted] = active_frames.emplace(identity, std::move(frame));
if (!inserted) throw std::logic_error("Plot received a duplicate frame address");
auto& managed = active->second;
auto& request = managed.request;
request.width = std::clamp(request.width, 160U, 1920U);
request.height = std::clamp(request.height, 120U, 1080U);
view->update(request);
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&scene)) {
(*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(Size{static_cast<int>(request.width), static_cast<int>(request.height)});
const auto result = (*scene_2d)->render(std::get<std::unique_ptr<Frame_2D>>(active_frame->frame).get());
const auto result = (*scene_2d)->render(std::get<std::unique_ptr<Frame_2D>>(managed.frame).get());
if (result != Render_Scene_2D::Render_Result::completed)
asio::post(strand, [this] {
frame_completed();
asio::post(strand, [this, identity] {
frame_completed(identity);
});
return;
}
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{request.width, request.height});
const auto result = scene_3d->render(std::get<std::unique_ptr<Frame_3D>>(active_frame->frame).get());
const auto result = scene_3d->render(std::get<std::unique_ptr<Frame_3D>>(managed.frame).get());
if (result != Render_Scene_3D::Render_Result::submitted)
asio::post(strand, [this] {
frame_completed();
asio::post(strand, [this, identity] {
frame_completed(identity);
});
}
void Plot::Private::publish_completed_frame(Render_Frame* frame) {
if (!active_frame) throw std::logic_error("frame callback has no externally owned active frame");
const auto active = active_frames.find(frame);
if (active == active_frames.end())
throw std::logic_error("frame callback has no externally owned active frame");
auto& managed = active->second;
const auto pacing = frame_policy.snapshot();
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&active_frame->frame); frame_2d && frame_2d->get() == frame) publish(active_frame->owner, encode_frame(frame_2d->get(), active_frame->request.delivery, pacing));
else if (auto* frame_3d = std::get_if<std::unique_ptr<Frame_3D>>(&active_frame->frame); frame_3d && frame_3d->get() == frame) publish(active_frame->owner, encode_frame(frame_3d->get(), active_frame->request.delivery, pacing));
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&managed.frame); frame_2d && frame_2d->get() == frame) publish(managed.owner, encode_frame(frame_2d->get(), managed.request.delivery, pacing));
else if (auto* frame_3d = std::get_if<std::unique_ptr<Frame_3D>>(&managed.frame); frame_3d && frame_3d->get() == frame) publish(managed.owner, encode_frame(frame_3d->get(), managed.request.delivery, pacing));
else throw std::logic_error("frame callback does not match the externally owned active frame");
frame_completed();
frame_completed(frame);
}
void Plot::Private::frame_completed() {
active_frame.reset();
frame_in_flight = false;
while (!pending_order.empty()) {
std::size_t Plot::Private::frame_capacity() const noexcept {
return std::holds_alternative<std::unique_ptr<Scene_3D>>(scene) ? 3U : 1U;
}
void Plot::Private::frame_completed(Render_Frame* frame) {
active_frames.erase(frame);
while (active_frames.size() < frame_capacity() && !pending_order.empty()) {
const auto owner = pending_order.front();
pending_order.pop_front();
auto next = pending_frames.extract(owner);
if (next.empty()) continue;
render_frame(std::move(next.mapped()));
return;
}
}
Plot::Plot(asio::any_io_executor executor,
+36 -23
View File
@@ -281,16 +281,24 @@ function pipeline_stage_values(values: Frame_Stage_Values, dimension: Plot["dime
stages.pipeline_3d_scene_coordination_ms = take(Math.max(0, scene - event - prepare - paint));
const scene_finished = values.marker_scene_render_finished_ms ?? 0;
const queue_entered = values.marker_backend_queue_entered_ms ?? scene_finished;
const queue_left = values.marker_backend_queue_left_ms ?? scene_finished;
stages.pipeline_3d_backend_queue_ms = take(Math.max(0, queue_left - Math.max(scene_finished, queue_entered)));
const prepare_started = values.marker_backend_prepare_started_ms ?? queue_entered;
const prepare_finished = values.marker_backend_prepare_finished_ms ?? prepare_started;
const submit_queued = values.marker_backend_submit_queued_ms ?? prepare_finished;
const queue_left = values.marker_backend_queue_left_ms ?? submit_queued;
stages.pipeline_3d_prepare_queue_ms = take(Math.max(0, prepare_started - Math.max(scene_finished, queue_entered)));
let preparation_window = Math.max(0, prepare_finished - prepare_started);
const take_preparation = (key: string) => { const value = Math.min(preparation_window, Math.max(0, values[key] ?? 0)); preparation_window -= value; return take(value); };
stages.pipeline_3d_backend_apply_ms = take_preparation("backend_apply_ms");
stages.pipeline_3d_backend_plan_ms = take_preparation("backend_plan_ms");
stages.pipeline_3d_backend_execute_ms = take_preparation("backend_execute_ms");
stages.pipeline_3d_backend_commands_ms = take(preparation_window);
stages.pipeline_3d_backend_queue_ms = take(Math.max(0, queue_left - submit_queued));
const gpu_submitted = values.marker_gpu_submitted_ms ?? queue_left;
let backend_window = Math.max(0, gpu_submitted - queue_left);
const take_backend = (key: string) => { const value = Math.min(backend_window, Math.max(0, values[key] ?? 0)); backend_window -= value; return take(value); };
stages.pipeline_3d_backend_apply_ms = take_backend("backend_apply_ms");
stages.pipeline_3d_backend_plan_ms = take_backend("backend_plan_ms");
stages.pipeline_3d_backend_execute_ms = take_backend("backend_execute_ms");
stages.pipeline_3d_backend_submit_ms = take_backend("backend_submit_ms");
stages.pipeline_3d_backend_commands_ms = take(backend_window);
let submit_window = Math.max(0, gpu_submitted - queue_left);
const measured_submit = Math.min(submit_window, Math.max(0, values.backend_submit_ms ?? 0));
stages.pipeline_3d_backend_submit_ms = take(measured_submit);
submit_window -= measured_submit;
stages.pipeline_3d_submit_handoff_ms = take(submit_window);
let gpu_window = interval("gpu_submitted", "gpu_completed");
const take_gpu = (key: string) => { const value = Math.min(gpu_window, Math.max(0, values[key] ?? 0)); gpu_window -= value; return take(value); };
stages.pipeline_3d_gpu_render_ms = take_gpu("gpu_render_ms");
@@ -386,7 +394,8 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
let stopped = false;
let timer = 0;
let diagnostics_timer = 0;
let frame_pending = false;
const maximum_in_flight = plot.dimension === "3D" ? 3 : 1;
const in_flight_requests = new Set<number>();
let manual_frame_pending = false;
let next_request_id = 1;
let previous_request_time = 0;
@@ -397,7 +406,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
let samples: Frame_Sample[] = [];
const request_started_at = new Map<number, number>();
const presentation_callbacks = new Set<number>();
const pacing = {mode: "fixed_rate" as Frame_Pacing_Mode, fixed_rate_fps: 30, minimum_latency_headroom: 1.25};
const pacing = {mode: "maximum_rate" as Frame_Pacing_Mode, fixed_rate_fps: 30, minimum_latency_headroom: 1.25};
const local_policy_overrides = new Set<Frame_Policy_Event["key"]>();
const socket = new ReconnectingWebSocket(socket_url(plot.websocket), [], {
minReconnectionDelay: 300,
@@ -428,7 +437,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
const request_frame = (manual: boolean) => {
clear_timer();
if (stopped) return;
if (frame_pending) { if (manual) manual_frame_pending = true; return; }
if (in_flight_requests.size >= maximum_in_flight) { if (manual) manual_frame_pending = true; return; }
const retry = () => { if (manual) manual_frame_pending = true; timer = window.setTimeout(() => request_frame(manual), 250); };
if (socket.readyState !== WebSocket.OPEN) { retry(); return; }
const canvas = canvas_ref.current;
@@ -446,9 +455,10 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
socket.send(JSON.stringify({...message, request_id, delivery,
pixel_format: requested_pixel_format}));
request_started_at.set(request_id, started_at);
frame_pending = true;
in_flight_requests.add(request_id);
manual_frame_pending = false;
previous_request_time = started_at;
schedule_next();
};
const synchronize_pacing = (metadata: Frame_Metadata) => {
const next = metadata.pacing;
@@ -511,7 +521,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
const complete_frame = (pair: {value: Frame_Metadata; received_at: number}, completed_at: number, canvas_upload_ms: number, presentation: boolean) => {
const started_at = request_started_at.get(pair.value.correlation_id);
if (started_at === undefined) return;
frame_pending = false;
in_flight_requests.delete(pair.value.correlation_id);
request_started_at.delete(pair.value.correlation_id);
latest_metadata = pair.value;
synchronize_pacing(pair.value);
@@ -536,7 +546,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
const canvas_upload_ms = renderer.draw(bytes, pair.value);
if (canvas_upload_ms !== null) complete_frame(pair, completed_at, canvas_upload_ms, true);
else {
frame_pending = false;
in_flight_requests.delete(pair.value.correlation_id);
request_started_at.delete(pair.value.correlation_id);
schedule_next();
}
@@ -575,15 +585,15 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
};
const on_delivery_change = (event: Event) => {
const detail = (event as CustomEvent<{plot_id: string}>).detail;
if (detail?.plot_id === plot.id && !frame_pending) schedule_next(true);
if (detail?.plot_id === plot.id && in_flight_requests.size === 0) schedule_next(true);
};
window.addEventListener("aethera-frame-policy", on_policy_change);
window.addEventListener("aethera-manual-frame", on_manual_frame);
window.addEventListener("aethera-reset-camera", on_camera_reset);
window.addEventListener("aethera-reset-frame-diagnostics", on_diagnostics_reset);
window.addEventListener("aethera-frame-delivery", on_delivery_change);
socket.onopen = () => { frame_pending = false; pending_metadata = null; request_started_at.clear(); set_status("LIVE"); request_frame(true); };
socket.onclose = () => { clear_timer(); frame_pending = false; pending_metadata = null; request_started_at.clear(); if (!stopped) set_status("CONNECTING"); };
socket.onopen = () => { in_flight_requests.clear(); pending_metadata = null; request_started_at.clear(); set_status("LIVE"); request_frame(true); };
socket.onclose = () => { clear_timer(); in_flight_requests.clear(); pending_metadata = null; request_started_at.clear(); if (!stopped) set_status("CONNECTING"); };
socket.onmessage = event => {
if (typeof event.data === "string") {
try {
@@ -894,16 +904,18 @@ const pipeline_2d_definitions: Pipeline_Stage_Definition[] = [
];
const pipeline_3d_definitions: Pipeline_Stage_Definition[] = [
pipeline_common_start,
["pipeline_3d_event_ms", "3D 事件入队", "Scene 将输入事件提交到三维 Render Domain 的耗时。"],
["pipeline_3d_event_ms", "3D 事件入队", "Scene 将输入事件提交到本 Scene 独立 Datoviz 准备域的耗时。"],
["pipeline_3d_prepare_ms", "3D Visual Prepare", "将 Visual items 转换为不可变 Prepared_Visual GPU 字段的耗时;数据变化时执行。"],
["pipeline_3d_submit_graph_ms", "3D Submit 图", "Submit 依赖图把 Prepared_Visual 入队到异步后端的耗时。"],
["pipeline_3d_scene_coordination_ms", "3D Scene 编排", "三维 Scene 同步阶段中除事件、Prepare、Submit 外的依赖图编排耗时。"],
["pipeline_3d_backend_queue_ms", "Render Domain 排队", "Scene 提交结束后等待单线程 Datoviz Render Domain 接管的耗时。"],
["pipeline_3d_prepare_queue_ms", "准备域排队", "Scene 发布不可变 Visual 快照后,等待本 Scene 独立 Datoviz CPU 准备线程的耗时。"],
["pipeline_3d_backend_apply_ms", "Datoviz Apply", "把本帧 Visual 与 Scene 参数应用到 Datoviz 对象的 CPU 耗时。"],
["pipeline_3d_backend_plan_ms", "Datoviz Plan", "Datoviz 生成本帧 GPU 命令计划的 CPU 耗时。"],
["pipeline_3d_backend_execute_ms", "Datoviz Execute", "Datoviz 执行命令构建的 CPU 耗时。"],
["pipeline_3d_backend_submit_ms", "GPU 提交", "将已构建命令提交到 GPU 队列的 CPU 耗时。"],
["pipeline_3d_backend_commands_ms", "后端命令衔接", "Render Domain 接管到 GPU 提交之间未落在四个 Datoviz trace 字段中的命令衔接时间。"],
["pipeline_3d_backend_commands_ms", "准备域其余工作", "每 Scene 准备域中除 Apply、Plan、Execute 外的命令录制和交互处理耗时。"],
["pipeline_3d_backend_queue_ms", "GPU 提交域排队", "命令已录制完成后,等待同 GPU 单线程 Queue Submit 域的耗时。"],
["pipeline_3d_backend_submit_ms", "GPU Queue Submit", "共享提交域执行已经录制好的 Vulkan Queue Submit 的 CPU 耗时。"],
["pipeline_3d_submit_handoff_ms", "提交衔接", "共享提交域接管命令到 fence 开始监视之间未落入 Queue Submit 测量的轻量衔接。"],
["pipeline_3d_gpu_render_ms", "GPU Render", "GPU 执行渲染通道的设备时间。"],
["pipeline_3d_gpu_transition_ms", "GPU 资源转换", "GPU 图像布局和资源状态转换的设备时间。"],
["pipeline_3d_gpu_copy_ms", "GPU 回读复制", "GPU 将渲染结果复制到可回读资源的设备时间。"],
@@ -959,7 +971,8 @@ function Frame_Timeline_Chart({diagnostics, dimension, paused, on_context_menu}:
] : [
total_series,
["pipeline_3d_prepare_ms", "Visual Prepare", "#62a8ff"],
["pipeline_3d_backend_queue_ms", "后端排队", "#f4bd63"],
["pipeline_3d_prepare_queue_ms", "准备域排队", "#f4bd63"],
["pipeline_3d_backend_queue_ms", "GPU 提交排队", "#ef9f55"],
["pipeline_3d_gpu_render_ms", "GPU Render", "#ff7d9c"],
pixel_delivery ? ["pipeline_3d_readback_ms", "CPU 回读", "#b998ff"] : ["pipeline_3d_gpu_sync_ms", "GPU 同步", "#b998ff"]
];