完美一版
This commit is contained in:
@@ -19,7 +19,7 @@ Kernel Event -----------------+ +-- immutable Prepared_Point
|
||||
|
|
||||
Datoviz apply / emit / DRP2 / GPU
|
||||
|
|
||||
immutable Pixel_Frame
|
||||
externally owned Frame_3D
|
||||
```
|
||||
|
||||
oneTBB worker 不调用 `dvz_*`。Datoviz FramePlan 继续负责 GPU upload/render/copy/readback 依赖,Renderive RenderPlan 只负责应用级 CPU prepare 依赖和末端单一 render node。
|
||||
@@ -39,7 +39,7 @@ oneTBB worker 不调用 `dvz_*`。Datoviz FramePlan 继续负责 GPU upload/rend
|
||||
- positions、colors、sizes、sigma、angles、radii、normals 等派生数组只存在于 `Prepared_Point`,不保存为 Visual 成员。
|
||||
- `Point_Scene` 复用 Kernel `Scene3D_Context`、frame-control、RenderPlan、oneTBB Flow Graph、capture 和 observer。
|
||||
- Pointer、wheel、key 直接保留 Kernel Event 语义并投递到 `Render_Domain`,没有第二套输入命令或收集队列。
|
||||
- 后端只应用 prepared data,并负责 Datoviz/Vulkan 资源和不可变 `Pixel_Frame` 输出。
|
||||
- 后端只应用 prepared data,并负责 Datoviz/Vulkan 资源和写入调用方持有的 `Frame_3D`。
|
||||
|
||||
## 唯一状态源检查
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "Frame_3D.hpp"
|
||||
#include <stdexcept>
|
||||
namespace aethera::render_3d {
|
||||
struct Frame_3D::Private {
|
||||
Extent extent{}; /* 本帧 GPU 读回图像尺寸。 */
|
||||
std::vector<std::byte> pixels{}; /* 本帧按 RGBA8 连续存储的 GPU 读回像素。 */
|
||||
};
|
||||
Frame_3D::Frame_3D(Frame_Identity identity) : Render_Frame(identity), d(std::make_unique<Private>()) {}
|
||||
Frame_3D::~Frame_3D() = default;
|
||||
Extent Frame_3D::extent() const noexcept { return d->extent; }
|
||||
std::span<const std::byte> Frame_3D::pixels() const noexcept { return d->pixels; }
|
||||
void detail::Frame_3D_Access::assign_pixels(Frame_3D* frame, Extent extent, std::vector<std::byte> pixels) {
|
||||
if (!frame) throw std::invalid_argument("Render_Scene_3D requires a non-null external frame");
|
||||
frame->d->extent = extent;
|
||||
frame->d->pixels = std::move(pixels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "Types.hpp"
|
||||
#include <frame.hpp>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
namespace aethera::render_3d {
|
||||
class Frame_3D;
|
||||
namespace detail {
|
||||
struct Frame_3D_Access {
|
||||
static void assign_pixels(Frame_3D* frame, Extent extent, std::vector<std::byte> pixels);
|
||||
};
|
||||
}
|
||||
class Frame_3D final : public Render_Frame {
|
||||
public:
|
||||
explicit Frame_3D(Frame_Identity identity);
|
||||
~Frame_3D() override;
|
||||
[[nodiscard]] Extent extent() const noexcept;
|
||||
[[nodiscard]] std::span<const std::byte> pixels() const noexcept;
|
||||
private:
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d; /* 本帧最终三维读回结果的唯一所有权。 */
|
||||
friend struct detail::Frame_3D_Access;
|
||||
};
|
||||
}
|
||||
@@ -67,12 +67,6 @@ struct Point_Style {
|
||||
Point_Aspect aspect{Point_Aspect::filled}; /* 点内部和边缘的组合方式。 */
|
||||
bool operator==(const Point_Style&) const = default;
|
||||
};
|
||||
struct Pixel_Frame {
|
||||
Extent extent{}; /* 像素帧尺寸。 */
|
||||
std::uint64_t sequence{}; /* Scene 分配的单调帧序号。 */
|
||||
std::vector<std::byte> rgba8{}; /* 按 RGBA8 连续存储的只读发布内容。 */
|
||||
bool operator==(const Pixel_Frame&) const = default;
|
||||
};
|
||||
namespace detail {
|
||||
[[nodiscard]] bool finite(Coordinate_3D value) noexcept;
|
||||
[[nodiscard]] bool finite(Vec2 value) noexcept;
|
||||
|
||||
@@ -4,40 +4,127 @@
|
||||
#include "Render_Domain.hpp"
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
namespace aethera::render_3d::detail {
|
||||
namespace {
|
||||
float wheel_step(double pixel, double angle) { return static_cast<float>(pixel != 0.0 ? pixel : angle / 120.0); }
|
||||
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.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_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 Pending { std::optional<Datoviz_Visual_Backend::Pending_Frame> frame{}; };
|
||||
struct Submission { Prepared_Visual visual{}; Scene_3D_Parameters parameters{}; };
|
||||
struct Pending {
|
||||
std::optional<Datoviz_Visual_Backend::Pending_Frame> backend_frame{}; /* 已提交并等待 GPU fence 的后端帧。 */
|
||||
Frame_3D* output{}; /* 调用方拥有且保证存活到完成回调的输出帧。 */
|
||||
};
|
||||
struct Submission {
|
||||
Prepared_Visual visual{}; /* 本帧不可变的 CPU Prepared 数据。 */
|
||||
Scene_3D_Parameters parameters{}; /* 本帧 viewport 与清屏参数。 */
|
||||
Frame_3D* output{}; /* 调用方拥有的最终输出帧。 */
|
||||
};
|
||||
std::shared_ptr<Render_Domain> render_domain; /* GPU 索引对应的单线程 Datoviz 域。 */
|
||||
std::unique_ptr<Datoviz_Visual_Backend> backend; /* 只允许 render_domain 线程访问。 */
|
||||
mutable std::mutex render_mutex; /* 保护合并帧意图、在途标记和完成回调。 */
|
||||
std::optional<Submission> pending_submission{}; /* 在途期间覆盖保存的最新绘制意图。 */
|
||||
std::deque<Submission> pending_submissions{}; /* 唯一 Frame Target 忙碌时等待的外部帧 FIFO。 */
|
||||
Frame_Callback frame_callback{}; /* 完成帧的唯一发布出口。 */
|
||||
bool frame_in_flight{}; /* 是否已有帧占用唯一 Frame Target。 */
|
||||
mutable std::mutex failure_mutex; /* 保护最后一次 Unknown Failure。 */
|
||||
std::exception_ptr failure{}; /* 后端隔离边界捕获的最后一次 Unknown Failure。 */
|
||||
std::atomic_uint64_t next_sequence{1}; /* 下一完成帧使用的单调序号。 */
|
||||
std::atomic_bool available{}; /* 后端是否可接受事件和绘制。 */
|
||||
Implementation(std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, Scene_3D_Parameters initial) : render_domain(Render_Domain::acquire(gpu_index)) { const auto initialized = render_domain->invoke([this, gpu_index, validation_enabled, visual_family, initial] { backend = std::make_unique<Datoviz_Visual_Backend>(gpu_index, validation_enabled, visual_family, initial); }); if (!initialized) throw std::runtime_error("render domain stopped during 3D backend initialization"); 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(); }
|
||||
void fail(std::exception_ptr value) noexcept;
|
||||
void render(const Prepared_Visual& visual, Scene_3D_Parameters parameters);
|
||||
void render(const Prepared_Visual& visual, 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 complete(std::shared_ptr<const Pixel_Frame> frame);
|
||||
void complete(Frame_3D* frame);
|
||||
};
|
||||
Async_Render_Backend::Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, Scene_3D_Parameters parameters) : implementation_(std::make_shared<Implementation>(gpu_index, validation_enabled, visual_family, parameters)) {}
|
||||
Async_Render_Backend::~Async_Render_Backend() = default;
|
||||
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); std::lock_guard lock(render_mutex); frame_in_flight = false; pending_submission.reset(); }
|
||||
void Async_Render_Backend::Implementation::render(const Prepared_Visual& visual, Scene_3D_Parameters parameters) { if (!available.load(std::memory_order_acquire)) return; std::optional<Submission> submission; { std::lock_guard lock(render_mutex); pending_submission = Submission{visual, parameters}; if (frame_in_flight) return; frame_in_flight = true; submission = std::move(pending_submission); pending_submission.reset(); } submit(std::move(*submission)); }
|
||||
void Async_Render_Backend::Implementation::submit(Submission submission) { const auto sequence = next_sequence.fetch_add(1, std::memory_order_relaxed); auto self = shared_from_this(); auto prepared = std::make_shared<Prepared_Visual>(std::move(submission.visual)); auto pending = std::make_shared<Pending>(); const auto parameters = submission.parameters; const auto queued = render_domain->try_post([self, parameters, sequence, prepared, pending] { auto reservation = Gpu_Completion_Service::instance().prepare([self, pending](Gpu_Completion_Service::Result result) { self->finish(pending, std::move(result)); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }, false); if (!reservation) { self->complete({}); return; } pending->frame = self->backend->submit(parameters, *prepared, sequence, false); if (!pending->frame) { self->complete({}); return; } reservation.reservation.watch(pending->frame->device, pending->frame->fence); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); if (queued != Render_Domain::Try_Post_Result::queued) { std::lock_guard lock(render_mutex); pending_submission = Submission{*prepared, parameters}; frame_in_flight = false; } }
|
||||
void Async_Render_Backend::Implementation::finish(std::shared_ptr<Pending> pending, Gpu_Completion_Service::Result completion) noexcept { auto self = shared_from_this(); try { const auto result = render_domain->post([self, pending = std::move(pending), completion] { std::shared_ptr<const Pixel_Frame> frame; if (pending->frame) { if (completion.error == Gpu_Completion_Service::Completion_Error::none) frame = self->backend->collect(std::move(*pending->frame)).frame; else self->backend->discard(std::move(*pending->frame)); } self->complete(std::move(frame)); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); if (result != Render_Domain::Admission_Result::none) fail(std::make_exception_ptr(std::runtime_error("render domain stopped before GPU completion collection"))); } catch (...) { fail(std::current_exception()); } }
|
||||
void Async_Render_Backend::Implementation::complete(std::shared_ptr<const Pixel_Frame> frame) { Frame_Callback callback; std::optional<Submission> next; { std::lock_guard lock(render_mutex); callback = frame_callback; frame_in_flight = false; if (pending_submission && available.load(std::memory_order_acquire)) { frame_in_flight = true; next = std::move(pending_submission); pending_submission.reset(); } } if (frame && callback) callback(std::move(frame)); if (next) submit(std::move(*next)); }
|
||||
void Async_Render_Backend::render(const Prepared_Visual& visual, Scene_3D_Parameters parameters) { implementation_->render(visual, parameters); }
|
||||
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); std::lock_guard lock(render_mutex); frame_in_flight = false; pending_submissions.clear(); }
|
||||
void Async_Render_Backend::Implementation::render(const Prepared_Visual& visual, 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;
|
||||
{
|
||||
std::lock_guard lock(render_mutex);
|
||||
if (frame_in_flight) { pending_submissions.push_back(Submission{visual, parameters, frame}); return; }
|
||||
frame_in_flight = true;
|
||||
submission = Submission{visual, parameters, frame};
|
||||
}
|
||||
submit(std::move(*submission));
|
||||
}
|
||||
void Async_Render_Backend::Implementation::submit(Submission submission) {
|
||||
auto self = shared_from_this();
|
||||
auto prepared = std::make_shared<Prepared_Visual>(std::move(submission.visual));
|
||||
auto pending = std::make_shared<Pending>(Pending{std::nullopt, submission.output});
|
||||
const auto parameters = submission.parameters;
|
||||
const auto sequence = submission.output->identity().sequence;
|
||||
const auto queued = render_domain->try_post([self, parameters, sequence, prepared, pending] {
|
||||
pending->output->mark(Frame_Trace_Marker::backend_queue_left);
|
||||
auto reservation = Gpu_Completion_Service::instance().prepare([self, pending](Gpu_Completion_Service::Result result) { self->finish(pending, std::move(result)); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }, true);
|
||||
if (!reservation) { self->complete(pending->output); return; }
|
||||
pending->backend_frame = self->backend->submit(parameters, *prepared, sequence, true);
|
||||
if (!pending->backend_frame) { self->complete(pending->output); return; }
|
||||
record_datoviz_trace(pending->output, pending->backend_frame->trace);
|
||||
pending->output->mark(Frame_Trace_Marker::gpu_submitted);
|
||||
reservation.reservation.watch(pending->backend_frame->device, pending->backend_frame->fence);
|
||||
}, [self](std::exception_ptr value) { self->fail(std::move(value)); });
|
||||
if (queued != Render_Domain::Try_Post_Result::queued) {
|
||||
std::lock_guard lock(render_mutex);
|
||||
pending_submissions.push_front(Submission{*prepared, parameters, submission.output});
|
||||
frame_in_flight = false;
|
||||
}
|
||||
}
|
||||
void Async_Render_Backend::Implementation::finish(std::shared_ptr<Pending> pending, Gpu_Completion_Service::Result completion) noexcept {
|
||||
pending->output->mark(Frame_Trace_Marker::gpu_completed);
|
||||
pending->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 = std::move(pending), completion] {
|
||||
if (pending->backend_frame) {
|
||||
if (completion.error == Gpu_Completion_Service::Completion_Error::none) {
|
||||
pending->output->mark(Frame_Trace_Marker::readback_started);
|
||||
auto completed = self->backend->collect(std::move(*pending->backend_frame));
|
||||
pending->output->mark(Frame_Trace_Marker::readback_finished);
|
||||
record_datoviz_trace(pending->output, completed.trace);
|
||||
Frame_3D_Access::assign_pixels(pending->output, completed.extent, std::move(completed.pixels));
|
||||
} else self->backend->discard(std::move(*pending->backend_frame));
|
||||
}
|
||||
self->complete(pending->output);
|
||||
}, [self](std::exception_ptr value) { self->fail(std::move(value)); });
|
||||
if (result != Render_Domain::Admission_Result::none) fail(std::make_exception_ptr(std::runtime_error("render domain stopped before GPU completion collection")));
|
||||
} catch (...) { fail(std::current_exception()); }
|
||||
}
|
||||
void Async_Render_Backend::Implementation::complete(Frame_3D* frame) {
|
||||
Frame_Callback callback;
|
||||
std::optional<Submission> next;
|
||||
{
|
||||
std::lock_guard lock(render_mutex);
|
||||
callback = frame_callback;
|
||||
frame_in_flight = false;
|
||||
if (!pending_submissions.empty() && available.load(std::memory_order_acquire)) {
|
||||
frame_in_flight = true;
|
||||
next = std::move(pending_submissions.front());
|
||||
pending_submissions.pop_front();
|
||||
}
|
||||
}
|
||||
if (frame && callback) { frame->mark(Frame_Trace_Marker::callback_started); callback(frame); frame->mark(Frame_Trace_Marker::callback_finished); }
|
||||
if (next) submit(std::move(*next));
|
||||
}
|
||||
void Async_Render_Backend::render(const Prepared_Visual& visual, Scene_3D_Parameters parameters, Frame_3D* frame) { implementation_->render(visual, parameters, frame); }
|
||||
void Async_Render_Backend::set_frame_callback(Frame_Callback callback) { std::lock_guard lock(implementation_->render_mutex); implementation_->frame_callback = std::move(callback); }
|
||||
bool Async_Render_Backend::available() const noexcept { return implementation_->available.load(std::memory_order_acquire); }
|
||||
Async_Render_Backend::Dispatch_Event_Result Async_Render_Backend::dispatch_event(std::shared_ptr<const Event> event, Extent viewport) {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
#pragma once
|
||||
#include "../base/Frame_3D.hpp"
|
||||
#include "Backend_Types.hpp"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
namespace aethera::render_3d::detail {
|
||||
class Async_Render_Backend final {
|
||||
public:
|
||||
using Frame_Callback = std::function<void(std::shared_ptr<const Pixel_Frame>)>;
|
||||
using Frame_Callback = std::function<void(Frame_3D*)>;
|
||||
Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, Scene_3D_Parameters parameters);
|
||||
~Async_Render_Backend();
|
||||
Async_Render_Backend(const Async_Render_Backend&) = delete;
|
||||
Async_Render_Backend& operator=(const Async_Render_Backend&) = delete;
|
||||
/* 记录最新绘制意图并无等待提交;在途期间的多次调用自动合并为一次后继帧。 */
|
||||
void render(const Prepared_Visual& visual, Scene_3D_Parameters parameters);
|
||||
/* 将绘制意图无等待提交到调用方拥有的帧;frame 必须存活到完成回调返回。 */
|
||||
void render(const Prepared_Visual& visual, Scene_3D_Parameters parameters, Frame_3D* frame);
|
||||
/* 设置完成帧出口;回调在 Datoviz Render Domain 线程执行。 */
|
||||
void set_frame_callback(Frame_Callback callback);
|
||||
[[nodiscard]] bool available() const noexcept;
|
||||
|
||||
@@ -1039,11 +1039,7 @@ Datoviz_Visual_Backend::Completed_Frame Datoviz_Visual_Backend::collect(
|
||||
auto collection = target_->collect();
|
||||
if (pending.trace.observed) pending.trace.readback_ns = trace_now_ns() - readback_started;
|
||||
pending.trace.gpu = std::move(collection.gpu_timing);
|
||||
auto output = std::make_shared<Pixel_Frame>();
|
||||
output->extent = pending.extent;
|
||||
output->sequence = pending.sequence;
|
||||
output->rgba8 = std::move(collection.pixels);
|
||||
return {std::move(output), std::move(pending.trace)};
|
||||
return {pending.extent, std::move(collection.pixels), std::move(pending.trace)};
|
||||
}
|
||||
void Datoviz_Visual_Backend::discard(Pending_Frame pending) {
|
||||
require_domain();
|
||||
|
||||
@@ -25,8 +25,9 @@ public:
|
||||
Datoviz_Frame_Trace trace; /* 本帧可选诊断数据。 */
|
||||
};
|
||||
struct Completed_Frame {
|
||||
std::shared_ptr<const Pixel_Frame> frame; /* 已完成并可跨线程发布的像素帧。 */
|
||||
Datoviz_Frame_Trace trace; /* 已补全 GPU 和读回阶段的诊断数据。 */
|
||||
Extent extent{}; /* 已完成 GPU 读回的像素尺寸。 */
|
||||
std::vector<std::byte> pixels{}; /* 已完成 GPU 读回的连续 RGBA8 像素。 */
|
||||
Datoviz_Frame_Trace trace; /* 已补全 GPU 和读回阶段的原始诊断数据。 */
|
||||
};
|
||||
Datoviz_Visual_Backend(std::uint32_t gpu_index, bool validation_enabled,
|
||||
Visual_Family visual_family, const Scene_3D_Parameters& initial_scene);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
namespace aethera::render_3d {
|
||||
bool Render_Scene_3D::Prop::operator==(const Prop&) const = default;
|
||||
bool Render_Scene_3D::State::operator==(const State&) const = default;
|
||||
void Render_Scene_3D::render() { static_cast<Private&>(*d).dispatch->render(this); }
|
||||
Render_Scene_3D::Render_Result Render_Scene_3D::render(Frame_3D* frame) { return static_cast<Private&>(*d).dispatch->render(this, frame); }
|
||||
void Render_Scene_3D::set_frame_callback(Frame_Callback callback) { static_cast<Private&>(*d).dispatch->set_frame_callback(this, std::move(callback)); }
|
||||
void Render_Scene_3D::dispatch_event(std::unique_ptr<Event> event) { static_cast<Private&>(*d).dispatch->push_event(this, std::move(event)); }
|
||||
void Render_Scene_3D::activate_view() { static_cast<Private&>(*d).dispatch->set_active(this, true); }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "../base/Frame_3D.hpp"
|
||||
#include "../detail/Backend_Types.hpp"
|
||||
#include "../visual/Visuals.hpp"
|
||||
#include <scene.hpp>
|
||||
@@ -13,7 +14,7 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene, Dependency_Graph_Type<Submi
|
||||
struct Prop : Prev_Prop {
|
||||
Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */
|
||||
Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */
|
||||
bool view_active{true}; /* 是否接受 render() 产生新的完成帧。 */
|
||||
bool view_active{true}; /* 是否接受 render(frame*) 产生新的完成帧。 */
|
||||
bool operator==(const Prop&) const;
|
||||
};
|
||||
struct State : Prev_State {
|
||||
@@ -35,10 +36,11 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene, Dependency_Graph_Type<Submi
|
||||
std::uint32_t gpu_index{}; /* Datoviz 使用的物理 GPU 下标。 */
|
||||
bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */
|
||||
};
|
||||
using Frame_Callback = std::function<void(std::shared_ptr<const Pixel_Frame>)>;
|
||||
/* 触发绘制;在途期间的多次调用合并为一个使用最新数据的后继帧。 */
|
||||
void render();
|
||||
/* 安装完成帧回调;回调由 Datoviz Render Domain 线程调用。 */
|
||||
enum class Render_Result { submitted, view_inactive, empty_viewport, backend_unavailable };
|
||||
using Frame_Callback = std::function<void(Frame_3D*)>;
|
||||
/* 无等待提交到调用方拥有的帧;frame 必须存活到完成回调返回。 */
|
||||
[[nodiscard]] Render_Result render(Frame_3D* frame);
|
||||
/* 安装完成帧回调;回调由 Datoviz Render Domain 线程传回同一外部帧。 */
|
||||
void set_frame_callback(Frame_Callback callback);
|
||||
/* 无锁提交事件所有权;下一次 Prepare 开始时无等待转交 Datoviz 渲染域。 */
|
||||
void dispatch_event(std::unique_ptr<Event> event);
|
||||
|
||||
@@ -7,10 +7,11 @@ template <Attached Object>
|
||||
struct Scene_Paint_Context {
|
||||
std::shared_ptr<Async_Render_Backend> backend{}; /* Scene 拥有、异步命令延长生命周期的后端。 */
|
||||
Object* scene{}; /* 仅在 Scene 拥有本上下文期间读取当前 Prop。 */
|
||||
Frame_3D* frame{}; /* 当前 Submit 图对应的外部帧;process 返回后清空。 */
|
||||
};
|
||||
}
|
||||
struct Render_Scene_3D::Private : Prev_Private {
|
||||
using Render_Run = void (*)(Root*);
|
||||
using Render_Run = Render_Result (*)(Root*, Frame_3D*);
|
||||
using Callback_Run = void (*)(Root*, Frame_Callback);
|
||||
using Event_Run = void (*)(Root*, std::unique_ptr<Event>);
|
||||
using Active_Run = void (*)(Root*, bool);
|
||||
@@ -22,6 +23,7 @@ struct Render_Scene_3D::Private : Prev_Private {
|
||||
};
|
||||
std::shared_ptr<detail::Async_Render_Backend> backend{}; /* Scene 拥有的异步后端;已入队命令自行延长实现寿命。 */
|
||||
std::shared_ptr<void> paint_context{}; /* Paint 节点读取 Scene 当前 Prop 的生命周期门闩。 */
|
||||
Frame_3D* active_frame{}; /* 当前同步 process 借用的外部帧;提交完成后清空。 */
|
||||
const Dispatch* dispatch{}; /* 最终 Scene 类型对应的静态公开分派表。 */
|
||||
/* Builder 内部初始化后端;必须在绑定 Visual Paint 目标之前调用一次。 */
|
||||
template <Attached Object> void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family);
|
||||
@@ -31,18 +33,85 @@ struct Render_Scene_3D::Private : Prev_Private {
|
||||
template <Attached Object, typename Callback> void process(Object* object, Callback&& callback) requires std::invocable<Callback>;
|
||||
/* Prepare 开始时交换公共 Scene 事件批次,并无等待提交到 Datoviz 渲染域。 */
|
||||
void dispatch_events(Extent viewport);
|
||||
template <Attached Object> [[nodiscard]] Render_Result render(Object* object, Frame_3D* frame);
|
||||
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
|
||||
};
|
||||
template <typename Object> template <Attached Visual_Object>
|
||||
Render_Scene_3D::Builder<Object>::Builder(Visual_Object* visual_value, std::uint32_t gpu_index_value, bool validation_enabled_value) : Base(), visual(visual_value), visual_family(Visual_Object::Attached_Object::Specification::family), gpu_index(gpu_index_value), validation_enabled(validation_enabled_value) {
|
||||
bind_visual = [](Root* root, std::shared_ptr<void> context) { auto* object = static_cast<Visual_Object*>(root); Base::private_access(object).template get<typename Visual_Object::Attached_Object::Base_Tag>().bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) { auto& submission = *static_cast<detail::Scene_Paint_Context<Object>*>(raw_context); const auto& prop = submission.scene->template read_prop<Render_Scene_3D::Base_Tag>(); submission.backend->render(prepared, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); }); };
|
||||
bind_visual = [](Root* root, std::shared_ptr<void> context) { auto* object = static_cast<Visual_Object*>(root); Base::private_access(object).template get<typename Visual_Object::Attached_Object::Base_Tag>().bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) { auto& submission = *static_cast<detail::Scene_Paint_Context<Object>*>(raw_context); const auto& prop = submission.scene->template read_prop<Render_Scene_3D::Base_Tag>(); submission.backend->render(prepared, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}, submission.frame); }); };
|
||||
attach_visual = [](Object* scene, Root* root) { auto* object = static_cast<Visual_Object*>(root); return scene->template edit_dependency_graph<Prepare_Data_Tag, Submit_Tag>([&](auto& prepare, auto& submit) { prepare.add(object); submit.add(object); }); };
|
||||
}
|
||||
template <typename Object>
|
||||
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Render_Scene_3D::Builder<Object>::build() { if (!visual || !bind_visual || !attach_visual) throw std::invalid_argument("Render_Scene_3D requires one Visual"); auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto scene = std::move(result).value(); auto& private_data = Base::private_access(scene.get()).template get<Render_Scene_3D::Base_Tag>(); private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, visual_family); bind_visual(visual, private_data.paint_context); auto graph_result = attach_visual(scene.get(), visual); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(scene); }
|
||||
template <Attached Object> void Render_Scene_3D::Private::initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family) { const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>(); backend = std::make_shared<detail::Async_Render_Backend>(gpu_index, validation_enabled, visual_family, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); paint_context = std::make_shared<detail::Scene_Paint_Context<Object>>(detail::Scene_Paint_Context<Object>{backend, object}); }
|
||||
template <Attached Object> void Render_Scene_3D::Private::initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family) { const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>(); backend = std::make_shared<detail::Async_Render_Backend>(gpu_index, validation_enabled, visual_family, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); paint_context = std::make_shared<detail::Scene_Paint_Context<Object>>(detail::Scene_Paint_Context<Object>{backend, object, nullptr}); }
|
||||
inline void Render_Scene_3D::Private::dispatch_events(Extent viewport) { this->consume_events([&](const std::shared_ptr<Event>& event) { const auto result = backend->dispatch_event(event, viewport); return result != detail::Async_Render_Backend::Dispatch_Event_Result::queue_full && result != detail::Async_Render_Backend::Dispatch_Event_Result::backend_unavailable; }); }
|
||||
template <Attached Object, typename Callback> void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable<Callback> { const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>(); if (!prop.view_active || prop.viewport.empty() || !backend || !backend->available()) return; dispatch_events(prop.viewport); Prev_Private::process(object, [&](const Scene::Private::Result&) { const auto prepare = object->template current_dependency_graph<Prepare_Data_Tag>(); prepare.for_each_bound([](Renderable* renderable, Renderable::Private& data) { if (data.dispatch->state.get(renderable)->prepare_executed) renderable->template mark_dirty<Submit_Tag>(); }); const auto submit = object->template current_dependency_graph<Submit_Tag>(); const auto result = submit.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) { auto* data = view.private_data(node); if (!data) return; Root* root = node.object; auto* dispatch = data->dispatch; auto& state = *dispatch->state.get(root); state.paint_graph_rebuilt = false; state.paint_execution_time_ns = 0; const bool dirty = root->template dirty<Submit_Tag>(); state.paint_dirty = dirty; state.paint_executed = dispatch->paint.predicate(root, dirty); if (!state.paint_executed) return; if (dispatch->paint.builder) { if (!data->paint_graph) data->paint_graph = std::make_unique<tf::Taskflow>(); const bool rebuild = dispatch->paint.rebuild_predicate(root); if (!data->paint_graph_built || rebuild) { *data->paint_graph = dispatch->paint.builder(root); data->paint_graph_built = true; state.paint_graph_rebuilt = true; } state.paint_task_count = data->paint_graph->num_tasks(); if (!data->paint_graph->empty()) aethera::detail::run_taskflow(*data->paint_graph); } else { state.paint_task_count = dispatch->paint.run ? 1 : 0; if (dispatch->paint.run) dispatch->paint.run(root); } root->template take_dirty<Submit_Tag>(); dispatch->state.notify(root); }); if (!result) throw std::logic_error("render scene submit graph became invalid during submission"); }); std::invoke(std::forward<Callback>(callback)); }
|
||||
template <Attached Object> const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root) { auto* object = static_cast<Object*>(root); object->process([] {}); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, std::unique_ptr<Event> event) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).push_event(std::move(event)); }, [](Root* root, bool active) { static_cast<Object*>(root)->template set<&Prop::view_active>(active); }}; return value; }
|
||||
template <Attached Object, typename Callback>
|
||||
void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable<Callback> {
|
||||
Frame_3D* frame = active_frame;
|
||||
if (!frame) throw std::logic_error("3D Scene process has no external frame");
|
||||
const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>();
|
||||
if (!prop.view_active || prop.viewport.empty() || !backend || !backend->available()) return;
|
||||
frame->mark(Frame_Trace_Marker::scene_render_started);
|
||||
frame->mark(Frame_Trace_Marker::event_dispatch_started);
|
||||
dispatch_events(prop.viewport);
|
||||
frame->mark(Frame_Trace_Marker::event_dispatch_finished);
|
||||
auto context = std::static_pointer_cast<detail::Scene_Paint_Context<Object>>(paint_context);
|
||||
struct Frame_Context_Scope {
|
||||
Frame_3D*& target; /* Visual Submit 回调读取的当前外部帧槽位。 */
|
||||
Frame_3D* previous{}; /* 嵌套调用前的帧;析构时恢复。 */
|
||||
~Frame_Context_Scope() { target = previous; }
|
||||
} frame_context_scope{context->frame, context->frame};
|
||||
context->frame = frame;
|
||||
Prev_Private::process(object, frame, [&](const Scene::Private::Result&) {
|
||||
frame->mark(Frame_Trace_Marker::paint_started);
|
||||
const auto prepare = object->template current_dependency_graph<Prepare_Data_Tag>();
|
||||
prepare.for_each_bound([](Renderable* renderable, Renderable::Private& data) { if (data.dispatch->state.get(renderable)->prepare_executed) renderable->template mark_dirty<Submit_Tag>(); });
|
||||
const auto submit = object->template current_dependency_graph<Submit_Tag>();
|
||||
const auto result = submit.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) {
|
||||
auto* data = view.private_data(node);
|
||||
if (!data) return;
|
||||
Root* root = node.object;
|
||||
auto* dispatch = data->dispatch;
|
||||
auto& state = *dispatch->state.get(root);
|
||||
state.paint_graph_rebuilt = false;
|
||||
state.paint_execution_time_ns = 0;
|
||||
const bool dirty = root->template dirty<Submit_Tag>();
|
||||
state.paint_dirty = dirty;
|
||||
state.paint_executed = dispatch->paint.predicate(root, dirty);
|
||||
if (!state.paint_executed) return;
|
||||
if (dispatch->paint.builder) {
|
||||
if (!data->paint_graph) data->paint_graph = std::make_unique<tf::Taskflow>();
|
||||
const bool rebuild = dispatch->paint.rebuild_predicate(root);
|
||||
if (!data->paint_graph_built || rebuild) { *data->paint_graph = dispatch->paint.builder(root); data->paint_graph_built = true; state.paint_graph_rebuilt = true; }
|
||||
state.paint_task_count = data->paint_graph->num_tasks();
|
||||
if (!data->paint_graph->empty()) aethera::detail::run_taskflow(*data->paint_graph);
|
||||
} else { state.paint_task_count = dispatch->paint.run ? 1 : 0; if (dispatch->paint.run) dispatch->paint.run(root); }
|
||||
root->template take_dirty<Submit_Tag>();
|
||||
dispatch->state.notify(root);
|
||||
});
|
||||
if (!result) throw std::logic_error("render scene submit graph became invalid during submission");
|
||||
frame->mark(Frame_Trace_Marker::paint_finished);
|
||||
});
|
||||
std::invoke(std::forward<Callback>(callback));
|
||||
}
|
||||
template <Attached Object>
|
||||
Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, Frame_3D* frame) {
|
||||
if (!frame) throw std::invalid_argument("Render_Scene_3D requires a non-null external frame");
|
||||
frame->mark(Frame_Trace_Marker::scene_render_requested);
|
||||
struct Active_Frame_Scope {
|
||||
Frame_3D*& target; /* 最终 Private 的同步 process 帧槽位。 */
|
||||
Frame_3D* previous{}; /* 嵌套调用前的帧;析构时恢复。 */
|
||||
~Active_Frame_Scope() { target = previous; }
|
||||
} active_frame_scope{active_frame, active_frame};
|
||||
active_frame = frame;
|
||||
bool submitted{};
|
||||
object->process([&] { submitted = true; frame->mark(Frame_Trace_Marker::scene_render_finished); });
|
||||
if (submitted) return Render_Result::submitted;
|
||||
const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>();
|
||||
if (!prop.view_active) return Render_Result::view_inactive;
|
||||
if (prop.viewport.empty()) return Render_Result::empty_viewport;
|
||||
return Render_Result::backend_unavailable;
|
||||
}
|
||||
template <Attached Object> const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root, Frame_3D* frame) { auto* object = static_cast<Object*>(root); return static_cast<typename Object::Private&>(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, std::unique_ptr<Event> event) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).push_event(std::move(event)); }, [](Root* root, bool active) { static_cast<Object*>(root)->template set<&Prop::view_active>(active); }}; return value; }
|
||||
template <Attached Object> void Render_Scene_3D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ struct Basic_Visual<Spec>::Private : Basic_Visual::Prev_Private {
|
||||
void bind_paint_target(std::shared_ptr<void> context, Enqueue_Paint enqueue);
|
||||
/* CRTP 数据能力:从当前 Prop 构建后端字段,不访问其他缓冲角色。 */
|
||||
template <Attached Object> void prepare_data(Object* object);
|
||||
/* CRTP 子图能力:返回只负责提交异步后端工作的单节点图;节点不会等待 GPU。 */
|
||||
/* CRTP 子图能力:每帧提交一次后端渲染;比较双槽 revision 后直接使用最新 Prepared 数据。 */
|
||||
template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& prop);
|
||||
/* CRTP 覆盖:Visual 未绑定 Scene 后端时不执行 Paint。 */
|
||||
/* CRTP 覆盖:绑定 Scene 后每帧执行 Paint;dirty 只表示 Prepared 数据是否更新,不控制帧生成。 */
|
||||
template <Attached Object> [[nodiscard]] bool should_paint(Object* object, const State& state, bool dirty);
|
||||
/* CRTP Prop hook:本层字段改变时标记 Prepare。 */
|
||||
template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> props);
|
||||
@@ -42,8 +42,8 @@ template <typename Spec> bool Basic_Visual<Spec>::State::operator==(const State&
|
||||
template <typename Spec> bool Basic_Visual<Spec>::Private::valid_items(const std::vector<Item>& items) { return std::ranges::all_of(items, [](const Item& item) { return Spec::valid(item); }); }
|
||||
template <typename Spec> void Basic_Visual<Spec>::Private::bind_paint_target(std::shared_ptr<void> context, Enqueue_Paint enqueue) { paint_target = {std::move(context), enqueue}; }
|
||||
template <typename Spec> template <Attached Object> void Basic_Visual<Spec>::Private::prepare_data(Object* object) { using Visual_Tag = typename Basic_Visual::Base_Tag; const auto& prop = object->template read_prop<Visual_Tag>(); if (!valid_items(prop.items) || !detail::finite(prop.transform)) throw std::invalid_argument("3D visual property contains invalid data"); auto& output = object->template pending_buffer<detail::Prepared_Visual_Tag>(); output = {}; output.family = Spec::family; output.transform = prop.transform; output.visible = prop.visible; output.depth_test = prop.depth_test; output.revision = next_revision++; if constexpr (requires { prop.style; }) output.point_style = prop.style; Spec::prepare(prop.items, output); object->template update_state<&State::prepared_item_count, &State::prepared_revision>([&](State_Access<typename Object::State> states) { auto& state = states.template get<Visual_Tag>(); state.prepared_item_count = output.positions.size(); state.prepared_revision = output.revision; }); }
|
||||
template <typename Spec> template <Attached Object> tf::Taskflow Basic_Visual<Spec>::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { if (auto context = paint_target.context.lock(); context && paint_target.enqueue) paint_target.enqueue(context.get(), object->template pending_buffer<detail::Prepared_Visual_Tag>()); }).name("render_3d.paint.enqueue"); return graph; }
|
||||
template <typename Spec> template <Attached Object> bool Basic_Visual<Spec>::Private::should_paint(Object*, const State&, bool dirty) { return dirty && paint_target.enqueue != nullptr && !paint_target.context.expired(); }
|
||||
template <typename Spec> template <Attached Object> tf::Taskflow Basic_Visual<Spec>::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { if (auto context = paint_target.context.lock(); context && paint_target.enqueue) { const auto& pending = object->template pending_buffer<detail::Prepared_Visual_Tag>(); const auto& current = object->template current_buffer<detail::Prepared_Visual_Tag>(); paint_target.enqueue(context.get(), pending.revision >= current.revision ? pending : current); } }).name("render_3d.paint.enqueue"); return graph; }
|
||||
template <typename Spec> template <Attached Object> bool Basic_Visual<Spec>::Private::should_paint(Object*, const State&, bool) { return paint_target.enqueue != nullptr && !paint_target.context.expired(); }
|
||||
template <typename Spec> template <typename Object, typename Owner, typename Member, typename Prop_Type> void Basic_Visual<Spec>::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
|
||||
template <typename Spec> template <typename Object, typename Prop_Type, typename State_Type> void Basic_Visual<Spec>::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { using Visual_Tag = typename Basic_Visual::Base_Tag; pending_states.template get<Visual_Tag>().item_count = static_cast<const Prop&>(*current_prop).items.size(); }
|
||||
template <typename Spec> template <Attached Object> const typename Basic_Visual<Spec>::Private::Dispatch& Basic_Visual<Spec>::Private::dispatch_for() { using Visual_Tag = typename Basic_Visual::Base_Tag; static const Dispatch value{[](Root* root, std::vector<Item> items) { if (!Private::valid_items(items)) return Update_Items_Result::invalid_data; static_cast<Object*>(root)->template set<&Prop::items>(std::move(items)); return Update_Items_Result::updated; }, [](Root* root, const std::function<void(std::vector<Item>&)>& edit) { if (!edit) return Edit_Items_Result::empty_edit; auto* object = static_cast<Object*>(root); auto items = object->template read_prop<Visual_Tag>().items; edit(items); if (!Private::valid_items(items)) return Edit_Items_Result::invalid_data; object->template set<&Prop::items>(std::move(items)); return Edit_Items_Result::updated; }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Visual_Tag>().items.size(); }}; return value; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <render_3D/Render_3D.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
@@ -57,14 +58,30 @@ TEST(Render_3D_Scene, Paint_Submits_Without_Waiting_For_Gpu) {
|
||||
ASSERT_TRUE(scene_result.has_value());
|
||||
auto scene = std::move(scene_result).value();
|
||||
std::atomic_bool frame_ready{};
|
||||
std::shared_ptr<const Pixel_Frame> frame;
|
||||
scene->set_frame_callback([&](std::shared_ptr<const Pixel_Frame> value) { frame = std::move(value); frame_ready.store(true, std::memory_order_release); });
|
||||
Frame_3D frame{Frame_Identity{1, 0}};
|
||||
Frame_3D unchanged_frame{Frame_Identity{2, 0}};
|
||||
Frame_3D exchanged_unchanged_frame{Frame_Identity{3, 0}};
|
||||
Frame_3D* completed_frame{};
|
||||
scene->set_frame_callback([&](Frame_3D* value) { completed_frame = value; frame_ready.store(true, std::memory_order_release); });
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
scene->render();
|
||||
EXPECT_EQ(scene->render(&frame), Render_Scene_3D::Render_Result::submitted);
|
||||
const auto submit_duration = std::chrono::steady_clock::now() - started;
|
||||
EXPECT_LT(submit_duration, std::chrono::seconds(1));
|
||||
for (std::size_t attempt = 0; attempt != 500 && !frame_ready.load(std::memory_order_acquire); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_NE(frame, nullptr);
|
||||
EXPECT_EQ(completed_frame, &frame);
|
||||
EXPECT_FALSE(frame.pixels().empty());
|
||||
frame_ready.store(false, std::memory_order_release);
|
||||
completed_frame = nullptr;
|
||||
EXPECT_EQ(scene->render(&unchanged_frame), Render_Scene_3D::Render_Result::submitted);
|
||||
for (std::size_t attempt = 0; attempt != 500 && !frame_ready.load(std::memory_order_acquire); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_EQ(completed_frame, &unchanged_frame);
|
||||
EXPECT_FALSE(unchanged_frame.pixels().empty());
|
||||
frame_ready.store(false, std::memory_order_release);
|
||||
completed_frame = nullptr;
|
||||
EXPECT_EQ(scene->render(&exchanged_unchanged_frame), Render_Scene_3D::Render_Result::submitted);
|
||||
for (std::size_t attempt = 0; attempt != 500 && !frame_ready.load(std::memory_order_acquire); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_EQ(completed_frame, &exchanged_unchanged_frame);
|
||||
EXPECT_TRUE(std::ranges::equal(unchanged_frame.pixels(), exchanged_unchanged_frame.pixels()));
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what();
|
||||
|
||||
Reference in New Issue
Block a user