修复webserver死锁

This commit is contained in:
2026-08-17 20:38:43 +08:00
parent 47cf1af7a5
commit 03433a412a
16 changed files with 160 additions and 349 deletions
+1
View File
@@ -11,3 +11,4 @@
/webapp_gallery/.playwright-cli/
/logs/
/deadlock_capture/
/*.zip
+52
View File
@@ -0,0 +1,52 @@
$ErrorActionPreference = "Stop"
$envScript = "D:\ae\proj\Renderive\render_3D\env.ps1"
$targetExe = "D:\ae\proj\Renderive\cmake-build-vs2022_debug\web_server\Renderive_Web_Server.exe"
$workingDir = Split-Path -Parent $targetExe
if (-not (Test-Path -LiteralPath $envScript))
{
throw "Environment script not found: $envScript"
}
if (-not (Test-Path -LiteralPath $targetExe))
{
throw "Target executable not found: $targetExe"
}
Write-Host ""
Write-Host "Loading environment:"
Write-Host " $envScript"
# Dot-source env.ps1 so its environment changes are applied to this PowerShell process.
. $envScript
# Make sure DLLs next to the executable can also be resolved.
if (($env:Path -split ';') -notcontains $workingDir)
{
$env:Path = "$workingDir;$env:Path"
}
Write-Host ""
Write-Host "Working directory:"
Write-Host " $workingDir"
Write-Host ""
Write-Host "Starting:"
Write-Host " $targetExe"
Write-Host ""
Push-Location $workingDir
try
{
# Forward any arguments passed to this script to Renderive_Web_Server.exe.
& $targetExe @args
$exitCode = $LASTEXITCODE
}
finally
{
Pop-Location
}
Write-Host ""
Write-Host "Renderive_Web_Server exit code: $exitCode"
exit $exitCode
+36 -235
View File
@@ -1,30 +1,19 @@
#include <renderive/error/Error_Policy.hpp>
#include "Gallery_Plot_Session.h"
#include "Gallery_Protocol.h"
#include "common/Gallery_Scene_Interface.h"
#include "render_2D/Gallery_Scene2D.h"
#include "render_3D/Gallery_Scene3D.h"
#include <renderive/scheduling/Scheduler.hpp>
#include <trantor/net/EventLoopThread.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <exception>
#include <functional>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
namespace renderive::web {
namespace {
Gallery_View_Input gallery_input(const Event& event) {
@@ -124,78 +113,14 @@ Gallery_Key_Input gallery_input(const Key_Event& event) {
input.auto_repeat = event.auto_repeat;
return input;
}
std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency) {
std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode) {
if (case_id.starts_with("datoviz_"))
return make_gallery_scene_3d(session_id, std::move(case_id), mode, automatic_low_latency);
return make_gallery_scene_2d(session_id, std::move(case_id), mode, automatic_low_latency);
}
trantor::EventLoop* automatic_render_loop() {
static trantor::EventLoopThread thread("RenderiveAutomaticRender");
static const bool started = [] {
thread.run();
return true;
}();
static_cast<void>(started);
return thread.getLoop();
return make_gallery_scene_3d(session_id, std::move(case_id), mode);
return make_gallery_scene_2d(session_id, std::move(case_id), mode);
}
}
namespace detail {
class Automatic_Render_Executor final {
public:
Automatic_Render_Executor() {
const std::size_t worker_count = renderive::scheduling::scheduler_concurrency();
workers_.reserve(worker_count);
for (std::size_t index = 0; index < worker_count; ++index) {
workers_.emplace_back([this](std::stop_token stop) {
run(stop);
});
}
}
~Automatic_Render_Executor() {
for (auto& worker : workers_)
worker.request_stop();
condition_.notify_all();
}
void enqueue(std::function<void()> task) {
{
std::lock_guard lock(mutex_);
tasks_.push_back(std::move(task));
}
condition_.notify_one();
}
private:
void run(std::stop_token stop) {
for (;;) {
std::function<void()> task;
{
std::unique_lock lock(mutex_);
if (!condition_.wait(lock, stop, [this] {
return !tasks_.empty();
}))
return;
task = std::move(tasks_.front());
tasks_.pop_front();
}
task();
}
}
std::mutex mutex_;
std::condition_variable_any condition_;
std::deque<std::function<void()>> tasks_;
std::vector<std::jthread> workers_;
};
Automatic_Render_Executor& automatic_render_executor() {
static Automatic_Render_Executor executor;
return executor;
}
}
struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Session::Impl> {
using Clock = std::chrono::steady_clock;
explicit Impl(bool enable_automatic_low_latency)
: automatic_low_latency(enable_automatic_low_latency), session_id(next_session_id()) {}
~Impl() {
disarm_automatic_render();
}
struct Gallery_Plot_Session::Impl {
Impl() : session_id(next_session_id()) {}
static std::uint64_t next_session_id() noexcept {
static std::atomic<std::uint64_t> next{1};
return next.fetch_add(1, std::memory_order_relaxed);
@@ -263,105 +188,9 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Se
[[nodiscard]] std::unique_lock<std::mutex> acquire_foreground_lock() {
return std::unique_lock<std::mutex>(mutex);
}
void disarm_automatic_render() {
++automatic_timer_revision;
if (automatic_timer == trantor::InvalidTimerId)
return;
automatic_render_loop()->invalidateTimer(automatic_timer);
automatic_timer = trantor::InvalidTimerId;
}
void arm_automatic_render_locked() {
if (!automatic_low_latency)
return;
disarm_automatic_render();
if (!scene || !scene->can_render_automatically() || render_task_pending)
return;
const auto interval = std::chrono::nanoseconds(
std::max<std::uint64_t>(1, scene->kernel_refresh_interval_ns()));
const auto deadline = last_render_started ? *last_render_started + interval : Clock::now();
const auto delay = std::max(Clock::duration::zero(), deadline - Clock::now());
const std::uint64_t revision = automatic_timer_revision;
automatic_timer = automatic_render_loop()->runAfter(
std::chrono::duration<double>(delay).count(),
[session = weak_from_this(), revision] {
if (auto value = session.lock())
value->automatic_render_due(revision);
});
}
void automatic_render_due(std::uint64_t revision) {
std::uint64_t generation{};
{
std::lock_guard lock(mutex);
if (revision != automatic_timer_revision)
return;
automatic_timer = trantor::InvalidTimerId;
++automatic_timer_revision;
if (!automatic_low_latency || !scene || !scene->can_render_automatically() || render_task_pending)
return;
render_task_pending = true;
generation = scene_generation;
}
auto self = shared_from_this();
try {
detail::automatic_render_executor().enqueue([self = std::move(self), generation] {
self->run_automatic_render(generation);
});
} catch (...) {
std::lock_guard lock(mutex);
render_task_pending = false;
automatic_render_exception = ::renderive::error::capture(
"arming automatic gallery render", std::current_exception());
disarm_automatic_render();
}
}
void run_automatic_render(std::uint64_t generation) {
const auto started = Clock::now();
try {
std::lock_guard lock(mutex);
if (generation == scene_generation && scene && scene->can_render_automatically()) {
const auto error = scene->render_latest_frame();
if (error == Gallery_Render_Result::none)
last_render_started = started;
}
render_task_pending = false;
arm_automatic_render_locked();
} catch (...) {
std::lock_guard lock(mutex);
render_task_pending = false;
automatic_render_exception = ::renderive::error::capture(
"running automatic gallery render", std::current_exception());
disarm_automatic_render();
}
}
static bool affects_render_schedule(const Web_Event& event) {
return std::visit(
[](const auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, Frame_Request>) {
return false;
}
else if constexpr (std::is_same_v<T, Gallery_Request>) {
return value.kind != Gallery_Request_Kind::Catalog &&
value.kind != Gallery_Request_Kind::Observe &&
value.kind != Gallery_Request_Kind::Refresh &&
value.kind != Gallery_Request_Kind::Reset_Monitoring;
}
else {
return true;
}
},
event);
}
std::unique_ptr<Gallery_Scene_Interface> scene;
std::mutex mutex;
std::optional<Clock::time_point> last_render_started;
std::uint64_t scene_generation{};
bool automatic_low_latency{};
bool render_task_pending{};
std::exception_ptr automatic_render_exception;
std::uint64_t session_id{};
trantor::TimerId automatic_timer{trantor::InvalidTimerId};
std::uint64_t automatic_timer_revision{};
std::optional<Web_Response> handle_gallery(const Gallery_Request& request) {
if (request.kind == Gallery_Request_Kind::Catalog)
return Web_Response{Web_Response_Type::Json, Gallery_Protocol::catalog_json()};
@@ -372,10 +201,7 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Se
Web_Response_Type::Json,
Gallery_Protocol::error_json("未知的 gallery case 或 frame_mode", "case")
};
scene = make_gallery_scene(session_id, open->case_id, open->frame_mode, automatic_low_latency);
++scene_generation;
last_render_started.reset();
arm_automatic_render_locked();
scene = make_gallery_scene(session_id, open->case_id, open->frame_mode);
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(),
@@ -390,8 +216,7 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Se
Gallery_Protocol::error_json("请先发送 gallery_open")
};
if (request.kind == Gallery_Request_Kind::Observe) {
if (update_client_metrics(request.message))
arm_automatic_render_locked();
static_cast<void>(update_client_metrics(request.message));
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::observer_json(
@@ -399,8 +224,7 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Se
};
}
if (request.kind == Gallery_Request_Kind::Refresh) {
if (update_client_metrics(request.message))
arm_automatic_render_locked();
static_cast<void>(update_client_metrics(request.message));
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::case_json_from_controls(
@@ -443,8 +267,7 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Se
Web_Response_Type::Json,
Gallery_Protocol::error_json("gallery_action 格式无效")
};
if (!Gallery_Protocol::action_available(scene->case_id(), scene->frame_mode(),
action->id))
if (!Gallery_Protocol::action_available(scene->case_id(), action->id))
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::error_json(
@@ -453,10 +276,7 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Se
if (action->id == "reset") {
const std::string id = scene->case_id();
const auto mode = scene->frame_mode();
scene = make_gallery_scene(session_id, id, mode, automatic_low_latency);
++scene_generation;
last_render_started.reset();
arm_automatic_render_locked();
scene = make_gallery_scene(session_id, id, mode);
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::case_json_from_controls(id, scene->controls().dump(),
@@ -485,7 +305,7 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Se
std::optional<std::string> pixels;
{
auto lock = acquire_foreground_lock();
if (!scene)
if (!scene || !scene->request_frame())
return std::nullopt;
const auto encode_started = std::chrono::steady_clock::now();
pixels = scene->encode_latest_pixels();
@@ -499,55 +319,36 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Se
return Web_Response{Web_Response_Type::Pixels, std::move(*pixels)};
}
std::optional<Web_Response> handle(const Web_Event& event) {
{
std::exception_ptr exception;
{
std::lock_guard lock(mutex);
exception = std::exchange(automatic_render_exception, {});
}
if (exception)
::renderive::error::unexpected(
"automatic gallery render", exception);
}
if (std::holds_alternative<Frame_Request>(event))
return handle_frame_request();
const bool reschedule = affects_render_schedule(event);
std::optional<Web_Response> response;
{
auto lock = acquire_foreground_lock();
response = std::visit(
[this](const auto& value) -> std::optional<Web_Response> {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, Gallery_Request>) {
return handle_gallery(value);
}
else if constexpr (std::is_same_v<T, Frame_Request>) {
return std::nullopt;
}
else if constexpr (std::is_same_v<T, Viewport_Resize>) {
if (scene)
scene->resize(value.size.width, value.size.height);
}
else if constexpr (std::is_same_v<T, Event>) {
if (scene)
scene->dispatch(Gallery_Input_Event{gallery_input(value)});
}
else if constexpr (Event_Object<T>) {
if (scene)
scene->dispatch(Gallery_Input_Event{gallery_input(value)});
}
auto lock = acquire_foreground_lock();
return std::visit(
[this](const auto& value) -> std::optional<Web_Response> {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, Gallery_Request>) {
return handle_gallery(value);
}
else if constexpr (std::is_same_v<T, Frame_Request>) {
return std::nullopt;
},
event);
if (reschedule)
arm_automatic_render_locked();
}
return response;
}
else if constexpr (std::is_same_v<T, Viewport_Resize>) {
if (scene)
scene->resize(value.size.width, value.size.height);
}
else if constexpr (std::is_same_v<T, Event>) {
if (scene)
scene->dispatch(Gallery_Input_Event{gallery_input(value)});
}
else if constexpr (Event_Object<T>) {
if (scene)
scene->dispatch(Gallery_Input_Event{gallery_input(value)});
}
return std::nullopt;
},
event);
}
};
Gallery_Plot_Session::Gallery_Plot_Session() : Gallery_Plot_Session(false) {}
Gallery_Plot_Session::Gallery_Plot_Session(bool automatic_low_latency)
: impl_(std::make_shared<Impl>(automatic_low_latency)) {}
Gallery_Plot_Session::Gallery_Plot_Session() : impl_(std::make_shared<Impl>()) {}
Gallery_Plot_Session::~Gallery_Plot_Session() = default;
std::optional<Web_Response> Gallery_Plot_Session::handle(const Web_Event& event) {
return impl_->handle(event);
-1
View File
@@ -10,7 +10,6 @@ namespace renderive::web {
class Gallery_Plot_Session final {
public:
Gallery_Plot_Session();
explicit Gallery_Plot_Session(bool automatic_low_latency);
~Gallery_Plot_Session();
Gallery_Plot_Session(const Gallery_Plot_Session&) = delete;
Gallery_Plot_Session& operator=(const Gallery_Plot_Session&) = delete;
+29 -22
View File
@@ -56,7 +56,7 @@ Json parse_object(std::string_view text) {
return value.is_object() ? std::move(value) : Json::object();
}
Json actions() {
return Json::array({
Json result = Json::array({
{{"id", "frame_strategy_low_latency"}, {"label", "Low latency"},
{"api", "Renderable_Editor::replace_frame_control_strategy"},
{"group", "Frame strategy"}, {"request_frame", true}},
@@ -65,18 +65,25 @@ Json actions() {
{"group", "Frame strategy"}, {"request_frame", true}},
{{"id", "frame_strategy_playback"}, {"label", "Playback"},
{"api", "Renderable_Editor::replace_frame_control_strategy"},
{"group", "Frame strategy"}, {"request_frame", true}},
{{"id", "mode_render"}, {"label", "Render frame"},
{"api", "Scene::render"}, {"group", "Frame"}, {"request_frame", true}},
{{"id", "capture_next_frame"}, {"label", "Capture next frame"},
{"api", "Scene_Base::capture_next_frame"}, {"group", "Performance Capture"},
{"request_frame", true}},
{{"id", "capture_frames"}, {"label", "Capture N frames"},
{"api", "Scene_Base::capture_frames"}, {"group", "Performance Capture"},
{"argument_input", "number"}, {"argument_default", 20}, {"request_frame", true}},
{{"id", "reset"}, {"label", "Reset scene"},
{"api", "Scene::Builder"}, {"group", "Scene"}, {"request_frame", true}}
{"group", "Frame strategy"}, {"request_frame", true}}
});
result.push_back({
{"id", "capture_next_frame"}, {"label", "Capture next frame"},
{"api", "Scene_Base::capture_next_frame"},
{"group", "Performance Capture"}, {"request_frame", true}
});
result.push_back({
{"id", "capture_frames"}, {"label", "Capture N frames"},
{"api", "Scene_Base::capture_frames"}, {"group", "Performance Capture"},
{"argument_input", "number"}, {"argument_default", 20},
{"request_frame", true}
});
result.push_back({
{"id", "reset"}, {"label", "Reset scene"},
{"api", "Scene::Builder"}, {"group", "Scene"},
{"request_frame", true}
});
return result;
}
template <class T>
Gallery_Request_Result<T> fail(Gallery_Request_Error error) {
@@ -93,7 +100,7 @@ std::string Gallery_Protocol::catalog_json() {
{"category", item.category}, {"description", "Live Renderive scene"},
{"order", index},
{"control_count_by_mode", {{"manual", 0}, {"low_latency", 0}, {"playback", 0}}},
{"action_count_by_mode", {{"manual", 7}, {"low_latency", 7}, {"playback", 7}}}
{"action_count_by_mode", {{"manual", 6}, {"low_latency", 6}, {"playback", 6}}}
});
}
Json field_mode{{"label", "Mode"}, {"source", "kernel_observer.mode"}};
@@ -119,14 +126,14 @@ std::string Gallery_Protocol::catalog_json() {
{"frame_button_label", "Render frame"}},
{{"id", "playback"}, {"title", "Playback"}, {"strategy", "Flow"},
{"description", "Ordered frame playback"}, {"order", 2}, {"accent", "#84b3ce"},
{"automatic", false}, {"request_after_response", false},
{"automatic", true}, {"request_after_response", false},
{"request_on_animation_frame", true}, {"observer_visible", true},
{"frame_button_label", "Render frame"}}
})},
{"cases", std::move(published_cases)},
{"coverage", {{"case_count", cases.size()}, {"page_count", 1},
{"canvas_count", cases.size()}, {"manual_control_count", 0},
{"manual_action_count", cases.size() * 7}}},
{"manual_action_count", cases.size() * 6}}},
{"dashboard", {
{"performance", {{"fields", Json::array({
{{"label", "Rendered"}, {"source", "performance.successful_render_count"},
@@ -232,13 +239,13 @@ Gallery_Protocol::control_patch_request(std::string_view message) {
}
bool Gallery_Protocol::action_available(std::string_view case_id,
Gallery_Frame_Mode,
std::string_view action_id) {
return is_case(case_id) &&
(action_id == "reset" || action_id == "mode_render" ||
action_id == "capture_next_frame" || action_id == "capture_frames" ||
action_id == "frame_strategy_low_latency" ||
action_id == "frame_strategy_manual" ||
action_id == "frame_strategy_playback");
if (!is_case(case_id))
return false;
return action_id == "reset" || action_id == "capture_next_frame" ||
action_id == "capture_frames" ||
action_id == "frame_strategy_low_latency" ||
action_id == "frame_strategy_manual" ||
action_id == "frame_strategy_playback";
}
} // namespace renderive::web
+1 -1
View File
@@ -51,6 +51,6 @@ public:
[[nodiscard]] static Gallery_Request_Result<Gallery_Action_Request> action_request(std::string_view message);
[[nodiscard]] static Gallery_Request_Result<Gallery_Control_Patch_Request> control_patch_request(
std::string_view message);
[[nodiscard]] static bool action_available(std::string_view case_id, Gallery_Frame_Mode frame_mode, std::string_view action_id);
[[nodiscard]] static bool action_available(std::string_view case_id, std::string_view action_id);
};
} // namespace renderive::web
@@ -14,7 +14,7 @@ namespace renderive::web {
void Gallery_WebSocket_Controller::handleNewConnection(
const drogon::HttpRequestPtr&,
const drogon::WebSocketConnectionPtr& connection) {
connection->setContext(std::make_shared<Gallery_Plot_Session>(true));
connection->setContext(std::make_shared<Gallery_Plot_Session>());
connection->setPingMessage("renderive-gallery", std::chrono::seconds(20));
LOG_INFO << "Renderive Gallery WebSocket connected: "
<< connection->peerAddr().toIpPort();
@@ -6,20 +6,10 @@
#include <nlohmann/json.hpp>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
namespace renderive::web {
enum class Gallery_Render_Result : std::uint8_t {
none,
inactive,
frame_unavailable,
cancelled,
deadline_exceeded,
backend_failure,
shutting_down
};
class Gallery_Scene_Interface {
public:
virtual ~Gallery_Scene_Interface() = default;
@@ -27,13 +17,11 @@ public:
[[nodiscard]] virtual nlohmann::json controls() const = 0;
[[nodiscard]] virtual Gallery_Frame_Mode frame_mode() const noexcept = 0;
virtual void reset_monitoring() = 0;
[[nodiscard]] virtual bool can_render_automatically() const noexcept = 0;
[[nodiscard]] virtual std::uint64_t kernel_refresh_interval_ns() const = 0;
virtual void set_client_metrics(Gallery_Client_Performance metrics) noexcept = 0;
[[nodiscard]] virtual adminive::Update_Result apply_patch(std::string_view target, const nlohmann::json& patch) = 0;
virtual void resize(int width, int height) = 0;
virtual void dispatch(const Gallery_Input_Event& event) = 0;
[[nodiscard]] virtual Gallery_Render_Result render_latest_frame() = 0;
[[nodiscard]] virtual bool request_frame() = 0;
[[nodiscard]] virtual std::optional<std::string> encode_latest_pixels() = 0;
virtual void record_pixel_response(std::chrono::steady_clock::time_point request_started, std::chrono::steady_clock::time_point encode_started, std::chrono::steady_clock::time_point encode_finished, std::size_t pixel_bytes) = 0;
[[nodiscard]] virtual std::string action(const Gallery_Action_Request& request, bool& recognized) = 0;
+10 -34
View File
@@ -70,9 +70,8 @@ Time_Of_Day current_time_of_day() {
class Gallery_Scene2D final : public Gallery_Scene_Interface {
public:
Gallery_Scene2D(std::string case_id, Gallery_Frame_Mode mode,
bool automatic)
: case_id_(std::move(case_id)), mode_(mode), automatic_(automatic) {
Gallery_Scene2D(std::string case_id, Gallery_Frame_Mode mode)
: case_id_(std::move(case_id)), mode_(mode) {
rebuild();
}
@@ -93,14 +92,6 @@ public:
Gallery_Frame_Mode frame_mode() const noexcept override { return mode_; }
void reset_monitoring() override { render_count_ = 0; }
bool can_render_automatically() const noexcept override {
return automatic_ && mode_ == Gallery_Frame_Mode::Low_Latency;
}
std::uint64_t kernel_refresh_interval_ns() const override {
return scene_->observer().next_refresh_interval_ns;
}
void set_client_metrics(Gallery_Client_Performance metrics) noexcept override {
client_ = metrics;
}
@@ -129,18 +120,15 @@ public:
}, event);
}
Gallery_Render_Result render_latest_frame() override {
bool request_frame() override {
update_samples();
const auto result = scene_->render_frame(true);
if (!result) return Gallery_Render_Result::backend_failure;
if (*result == Plot_Render_Status::view_inactive)
return Gallery_Render_Result::inactive;
if (*result != Plot_Render_Status::rendered)
return Gallery_Render_Result::frame_unavailable;
if (!result || *result != Plot_Render_Status::rendered)
return false;
if (scene_->wait_for_render() != Scene_Render_Result::none)
return Gallery_Render_Result::backend_failure;
return false;
++render_count_;
return Gallery_Render_Result::none;
return true;
}
std::optional<std::string> encode_latest_pixels() override {
@@ -178,14 +166,9 @@ public:
mode_ = next;
return "frame strategy replaced";
}
if (request.id == "mode_cycle" || request.id == "mode_render") {
static_cast<void>(render_latest_frame());
return "frame rendered";
}
if (request.id == "capture_next_frame") {
const auto capture = scene_->capture_next_frame();
if (capture) static_cast<void>(render_latest_frame());
return capture ? "frame captured" : "capture request rejected";
return capture ? "capture request accepted" : "capture request rejected";
}
if (request.id == "capture_frames") {
std::size_t count = 20;
@@ -193,7 +176,6 @@ public:
count = static_cast<std::size_t>(std::clamp(
std::get<double>(*request.argument), 1.0, 1000.0));
const auto capture = scene_->capture_frames(count);
if (capture) static_cast<void>(render_latest_frame());
return capture ? "frame capture started" : "capture request rejected";
}
recognized = false;
@@ -379,9 +361,6 @@ private:
}
scene_->activate_view();
update_samples();
static_cast<void>(scene_->render_frame(true));
static_cast<void>(scene_->wait_for_render());
}
void update_samples() {
@@ -437,7 +416,6 @@ private:
std::string case_id_;
Gallery_Frame_Mode mode_;
bool automatic_{};
Size viewport_{560, 320};
std::unique_ptr<Render_Scene_2D> scene_;
renderive_Owner<Frequency_Axis> frequency_axis_;
@@ -458,9 +436,7 @@ private:
} // namespace
std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene_2d(
std::uint64_t, std::string case_id, Gallery_Frame_Mode mode,
bool automatic_low_latency) {
return std::make_unique<Gallery_Scene2D>(
std::move(case_id), mode, automatic_low_latency);
std::uint64_t, std::string case_id, Gallery_Frame_Mode mode) {
return std::make_unique<Gallery_Scene2D>(std::move(case_id), mode);
}
} // namespace renderive::web
+1 -1
View File
@@ -4,5 +4,5 @@
#include <memory>
#include <string>
namespace renderive::web {
[[nodiscard]] std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene_2d(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency);
[[nodiscard]] std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene_2d(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode);
}
+8 -25
View File
@@ -69,9 +69,8 @@ Visual_Family visual_family(std::string_view case_id) {
class Gallery_Scene3D final : public Gallery_Scene_Interface {
public:
Gallery_Scene3D(std::string case_id, Gallery_Frame_Mode mode,
bool automatic)
: case_id_(std::move(case_id)), mode_(mode), automatic_(automatic) {
Gallery_Scene3D(std::string case_id, Gallery_Frame_Mode mode)
: case_id_(std::move(case_id)), mode_(mode) {
rebuild();
}
@@ -86,12 +85,6 @@ public:
Gallery_Frame_Mode frame_mode() const noexcept override { return mode_; }
void reset_monitoring() override { render_count_ = 0; }
bool can_render_automatically() const noexcept override {
return automatic_ && mode_ == Gallery_Frame_Mode::Low_Latency;
}
std::uint64_t kernel_refresh_interval_ns() const override {
return scene_->observer().next_refresh_interval_ns;
}
void set_client_metrics(Gallery_Client_Performance metrics) noexcept override {
client_ = metrics;
}
@@ -109,11 +102,11 @@ public:
}
void dispatch(const Gallery_Input_Event&) override {}
Gallery_Render_Result render_latest_frame() override {
bool request_frame() override {
if (scene_->request_frame() != Frame_Request_Result::none)
return Gallery_Render_Result::backend_failure;
return false;
++render_count_;
return Gallery_Render_Result::none;
return true;
}
std::optional<std::string> encode_latest_pixels() override {
@@ -152,14 +145,9 @@ public:
mode_ = next;
return "frame strategy replaced";
}
if (request.id == "mode_cycle" || request.id == "mode_render") {
static_cast<void>(render_latest_frame());
return "frame rendered";
}
if (request.id == "capture_next_frame") {
const auto capture = scene_->capture_next_frame();
if (capture) static_cast<void>(render_latest_frame());
return capture ? "frame captured" : "capture request rejected";
return capture ? "capture request accepted" : "capture request rejected";
}
if (request.id == "capture_frames") {
std::size_t count = 20;
@@ -167,7 +155,6 @@ public:
count = static_cast<std::size_t>(std::clamp(
std::get<double>(*request.argument), 1.0, 1000.0));
const auto capture = scene_->capture_frames(count);
if (capture) static_cast<void>(render_latest_frame());
return capture ? "frame capture started" : "capture request rejected";
}
recognized = false;
@@ -203,12 +190,10 @@ private:
state.visual_family = visual_family(case_id_);
scene_ = Render_Scene_3D::Builder{state}.build(
visual_, make_strategy(mode_));
static_cast<void>(scene_->request_frame());
}
std::string case_id_;
Gallery_Frame_Mode mode_;
bool automatic_{};
Extent extent_{560, 320};
std::shared_ptr<Point_Visual> visual_;
std::unique_ptr<Render_Scene_3D> scene_;
@@ -218,9 +203,7 @@ private:
} // namespace
std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene_3d(
std::uint64_t, std::string case_id, Gallery_Frame_Mode mode,
bool automatic_low_latency) {
return std::make_unique<Gallery_Scene3D>(
std::move(case_id), mode, automatic_low_latency);
std::uint64_t, std::string case_id, Gallery_Frame_Mode mode) {
return std::make_unique<Gallery_Scene3D>(std::move(case_id), mode);
}
} // namespace renderive::web
+1 -1
View File
@@ -4,5 +4,5 @@
#include <memory>
#include <string>
namespace renderive::web {
[[nodiscard]] std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene_3d(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency);
[[nodiscard]] std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene_3d(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode);
}
+2 -2
View File
@@ -46,9 +46,9 @@ TEST(RenderiveWebDatovizGallery, RgbaEncoderPreservesRowsAndRemovesPadding) {
TEST(RenderiveWebDatovizGallery, CurrentThreeDimensionalSceneRendersRgba) {
try {
auto scene = make_gallery_scene_3d(
1, "datoviz_3d", Gallery_Frame_Mode::Low_Latency, false);
1, "datoviz_3d", Gallery_Frame_Mode::Manual);
scene->resize(320, 200);
ASSERT_EQ(scene->render_latest_frame(), Gallery_Render_Result::none);
ASSERT_TRUE(scene->request_frame());
const auto frame = scene->encode_latest_pixels();
ASSERT_TRUE(frame);
ASSERT_GE(frame->size(), pixel_frame_header_size);
+15 -11
View File
@@ -92,6 +92,10 @@ TEST(RenderiveWebGallery, TwoDimensionalGalleryPublishesStateAndPixels) {
const auto& observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("state").at("viewport").at("width"), 560);
EXPECT_EQ(observer.at("renderable_count"), 4U);
EXPECT_EQ(state.at("actions").at("data").size(), 6U);
const auto low_latency_pixels = session.handle(Frame_Request{});
ASSERT_TRUE(low_latency_pixels);
EXPECT_EQ(low_latency_pixels->type, Web_Response_Type::Pixels);
const auto switched = session.handle(Gallery_Request{
Gallery_Request_Kind::Action,
@@ -102,28 +106,28 @@ TEST(RenderiveWebGallery, TwoDimensionalGalleryPublishesStateAndPixels) {
EXPECT_EQ(switched_state.at("telemetry").at("kernel_observer").at("mode"),
"manual");
const std::string render =
R"({"category":"event","type":"gallery_action","action":"mode_render"})";
const auto rendered = session.handle(
Gallery_Request{Gallery_Request_Kind::Action, render});
const auto rendered = session.handle(Frame_Request{});
ASSERT_TRUE(rendered);
EXPECT_EQ(nlohmann::json::parse(rendered->payload).at("type"), "case_state");
EXPECT_EQ(rendered->type, Web_Response_Type::Pixels);
const std::string capture =
R"({"category":"event","type":"gallery_action","action":"capture_next_frame"})";
const auto captured = session.handle(
Gallery_Request{Gallery_Request_Kind::Action, capture});
ASSERT_TRUE(captured);
const auto capture_state = nlohmann::json::parse(captured->payload);
const auto& sessions = capture_state.at("controls")
.at("performance_capture").at("sessions");
ASSERT_EQ(sessions.size(), 1U);
EXPECT_EQ(sessions.front().at("captured_count"), 1U);
const auto pixels = session.handle(Frame_Request{});
ASSERT_TRUE(pixels);
ASSERT_EQ(pixels->type, Web_Response_Type::Pixels);
EXPECT_EQ(pixels->payload.substr(0, 4), "RVP2");
const auto refreshed = session.handle(Gallery_Request{
Gallery_Request_Kind::Refresh,
R"({"category":"event","type":"gallery_refresh"})"});
ASSERT_TRUE(refreshed);
const auto capture_state = nlohmann::json::parse(refreshed->payload);
const auto& sessions = capture_state.at("controls")
.at("performance_capture").at("sessions");
ASSERT_EQ(sessions.size(), 1U);
EXPECT_EQ(sessions.front().at("captured_count"), 1U);
const auto first_pixel = pixels->payload.substr(pixel_frame_header_size, 4);
bool contains_drawn_pixel = false;
for (std::size_t offset = pixel_frame_header_size;
+1 -1
View File
@@ -21,6 +21,6 @@ export function Plot_Card({gallery_case,frame_mode,frame_modes,dashboard,active,
return <Card ref={card_ref} sx={{overflow:"hidden",display:"flex",flexDirection:"column",minWidth:0}} onContextMenu={event=>{event.preventDefault();if(snapshot.ready)on_open_inspector({session});}}>
<CardHeader title={gallery_case.title} subheader={gallery_case.component} avatar={<Chip size="small" label={gallery_case.category}/>} action={<Plot_Status snapshot={snapshot} active={active&&!streams_paused}/>}/>
<Plot_Canvas session={session}/><Performance_Strip telemetry={snapshot.telemetry} dashboard={dashboard}/>{frame_mode.observer_visible&&<Kernel_Observer_Summary telemetry={snapshot.telemetry} dashboard={dashboard}/>}<CardContent sx={{flexGrow:1}}><Typography color="text.secondary">{gallery_case.description}</Typography>{snapshot.notice&&<Typography variant="caption" color="secondary.main">{snapshot.notice}</Typography>}</CardContent>
<CardActions sx={{justifyContent:"space-between",px:2,pb:2}}><Stack direction="row" spacing={2}><Typography variant="caption">{snapshot.controls.length} </Typography><Typography variant="caption">{snapshot.actions.length} </Typography><Typography variant="caption">{snapshot.frame_count} </Typography></Stack><Box><Button size="small" startIcon={<RefreshIcon/>} onClick={()=>session.request_frame(performance.now(),true)}>{frame_mode.frame_button_label}</Button><Button size="small" startIcon={<MoreHorizIcon/>} disabled={!snapshot.ready} onClick={()=>on_open_inspector({session})}></Button></Box></CardActions>
<CardActions sx={{justifyContent:"space-between",px:2,pb:2}}><Stack direction="row" spacing={2}><Typography variant="caption">{snapshot.controls.length} </Typography><Typography variant="caption">{snapshot.actions.length} </Typography><Typography variant="caption">{snapshot.frame_count} </Typography></Stack><Box>{session.frame_mode.id==="manual"&&<Button size="small" startIcon={<RefreshIcon/>} onClick={()=>session.request_frame(performance.now(),true)}>{session.frame_mode.frame_button_label}</Button>}<Button size="small" startIcon={<MoreHorizIcon/>} disabled={!snapshot.ready} onClick={()=>on_open_inspector({session})}></Button></Box></CardActions>
</Card>;
}
@@ -36,7 +36,7 @@ export class Gallery_Plot_Session {
private receive_json(raw: string): void {try{const response=parse_gallery_response(raw);if(!response)return;if(response.type==="error"){this.update({notice:Object.values(response.field_errors??{})[0]??response.message});return;}if(response.type==="observer_state"){this.update({telemetry:response.telemetry,performance_capture:response.telemetry.performance_capture??this.snapshot.performance_capture,frame_count:this.received_frames});return;}if(response.type==="case_state"||response.type==="refresh_state"){const controls=response.controls;const mode_id=typeof response.frame_mode==="string"?response.frame_mode:undefined;const mode=this.frame_modes.find(value=>value.id===mode_id);if(mode)this.current_frame_mode=mode;this.update({ready:true,controls:build_controls(controls?.resources),actions:response.actions?.data??[],telemetry:response.telemetry??{},render_plan:controls?.render_plan??null,performance_capture:controls?.performance_capture??response.telemetry?.performance_capture??null,notice:response.notice??"",frame_count:this.received_frames,protocol_error:""});if(this.stream_active()){this.socket.send("show");this.request_frame(performance.now());}}}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"协议解析失败"});this.socket.close();}}
private receive_pixels(buffer: ArrayBuffer): void {try{const now=performance.now();this.frame_controller.receive_frame(now);const accepted=this.presenter.accept(buffer);this.received_frames++;this.client_performance.record_pixel(now,accepted.sequence,accepted.overwritten);}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"像素协议错误"});this.socket.close();}}
tick(now: number): void {if(!this.stream_active())return;const presented=this.presenter.present();if(presented)this.client_performance.record_presentation(now);if(this.frame_mode.request_on_animation_frame||(this.frame_mode.request_after_response&&presented))this.request_frame(now);if(this.snapshot.ready&&now-this.last_observe_at>=650){this.last_observe_at=now;this.socket.send("gallery_observe",{client_metrics:this.client_performance.snapshot(now,this.socket.buffered_bytes()) as unknown as Json_Value});}}
request_frame(now=performance.now(), explicit=false): boolean {if(!this.snapshot.ready||!this.socket.is_open()||(!explicit&&!this.stream_active())||(explicit&&!this.visible)||(!explicit&&!this.frame_mode.automatic))return false;return this.frame_controller.request_frame(now);}
request_frame(now=performance.now(), explicit=false): boolean {if(!this.snapshot.ready||!this.socket.is_open()||(!explicit&&!this.stream_active())||(explicit&&(!this.visible||this.frame_mode.id!=="manual"))||(!explicit&&!this.frame_mode.automatic))return false;return this.frame_controller.request_frame(now);}
resize(width: number,height: number): void {width=Math.max(240,Math.round(width));height=Math.max(180,Math.round(height));if(width===this.sent_width&&height===this.sent_height)return;this.sent_width=width;this.sent_height=height;if(this.socket.is_open())this.socket.send("resize",{width,height});}
pointer(type: "pointer_move"|"pointer_press"|"pointer_release", payload: Record<string, Json_Value>): void {this.socket.send(type,payload);}
leave(): void {this.socket.send("leave");}