架构微调
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
include_guard(GLOBAL)
|
||||
block()
|
||||
set(datoviz_opts
|
||||
-DDVZ_BUILD_CORE=ON
|
||||
-DDVZ_BUILD_CONTROLLER=ON
|
||||
-DDVZ_BUILD_VK=ON
|
||||
-DDVZ_BUILD_CANVAS=ON
|
||||
-DDVZ_BUILD_DRP2=ON
|
||||
-DDVZ_BUILD_WEBGPU=OFF
|
||||
-DDVZ_BUILD_SCENE=ON
|
||||
-DDVZ_BUILD_APP=ON
|
||||
-DDVZ_BUILD_GUI=OFF
|
||||
-DDVZ_BUILD_TESTING=OFF
|
||||
-DDVZ_BUILD_EXAMPLES=OFF
|
||||
-DDVZ_INSTALL=ON
|
||||
-DDVZ_VENDORED_DEPS=ON
|
||||
-DDVZ_ENABLE_CUDA=OFF
|
||||
-DDVZ_ENABLE_KVAZAAR=OFF
|
||||
-DDVZ_ENABLE_QT_BRIDGE=OFF
|
||||
-DDVZ_ENABLE_SHADERC=OFF
|
||||
-DDVZ_WITH_FREETYPE=OFF
|
||||
-DDVZ_WITH_MSDF_ATLAS=OFF
|
||||
-DDVZ_WITH_MSDF_SVG=OFF
|
||||
-DDVZ_WITH_ZLIB=OFF
|
||||
-DDVZ_WITH_GLFW=ON
|
||||
)
|
||||
_register_git_cmake_library(renderive::datoviz
|
||||
"https://github.com/datoviz/datoviz.git"
|
||||
"v0.4.0rc2"
|
||||
)
|
||||
rcl_cmake_library_set_cmake_options(renderive::datoviz ${datoviz_opts})
|
||||
if (WIN32 AND NOT MINGW)
|
||||
rcl_cmake_library_set_deps(renderive::datoviz renderive::pthreads4w)
|
||||
endif ()
|
||||
rcl_get_effective_install_dir(renderive::datoviz DATOVIZ_ROOT)
|
||||
set(DATOVIZ_CMAKE_DIR "${DATOVIZ_ROOT}/lib/cmake/datoviz")
|
||||
rcl_cmake_library_set_other_use_opt(renderive::datoviz
|
||||
"-Ddatoviz_DIR=\"${DATOVIZ_CMAKE_DIR}\""
|
||||
"-DDatoviz_DIR=\"${DATOVIZ_CMAKE_DIR}\""
|
||||
)
|
||||
rcl_cmake_library_set_init_script(renderive::datoviz
|
||||
"set(DATOVIZ_ROOT \"${DATOVIZ_ROOT}\")"
|
||||
"set(datoviz_DIR \"${DATOVIZ_CMAKE_DIR}\")"
|
||||
"set(Datoviz_DIR \"${DATOVIZ_CMAKE_DIR}\")"
|
||||
)
|
||||
rcl_cmake_library_set_clear_script(renderive::datoviz
|
||||
"unset(DATOVIZ_ROOT)"
|
||||
"unset(datoviz_DIR)"
|
||||
"unset(Datoviz_DIR)"
|
||||
)
|
||||
endblock()
|
||||
@@ -0,0 +1,36 @@
|
||||
include_guard(GLOBAL)
|
||||
block()
|
||||
_register_git_cmake_library(renderive::pthreads4w
|
||||
"https://github.com/GerHobbelt/pthread-win32.git"
|
||||
"v3.0.3.1"
|
||||
)
|
||||
rcl_get_effective_install_dir(renderive::pthreads4w PTHREADS4W_ROOT)
|
||||
set(PTHREADS4W_INCLUDE_DIR "${PTHREADS4W_ROOT}/include")
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(PTHREADS4W_LIBRARY "${PTHREADS4W_ROOT}/lib/libpthreadVC3d.lib")
|
||||
else ()
|
||||
set(PTHREADS4W_LIBRARY "${PTHREADS4W_ROOT}/lib/libpthreadVC3.lib")
|
||||
endif ()
|
||||
set(PTHREADS4W_CONFIG_DIR "${CMAKE_CURRENT_LIST_DIR}/pthreads4w_config")
|
||||
set(pthreads4w_opts
|
||||
-DENABLE_TESTS=OFF
|
||||
"-DDLLDEST=${PTHREADS4W_ROOT}/bin"
|
||||
"-DLIBDEST=${PTHREADS4W_ROOT}/lib"
|
||||
"-DHDRDEST=${PTHREADS4W_ROOT}/include"
|
||||
)
|
||||
rcl_cmake_library_set_cmake_options(renderive::pthreads4w ${pthreads4w_opts})
|
||||
rcl_cmake_library_set_other_use_opt(renderive::pthreads4w
|
||||
"-DPThreads4W_DIR=\"${PTHREADS4W_CONFIG_DIR}\""
|
||||
"-DPThreads4W_INCLUDE_DIR=\"${PTHREADS4W_INCLUDE_DIR}\""
|
||||
"-DPThreads4W_LIBRARY=\"${PTHREADS4W_LIBRARY}\""
|
||||
)
|
||||
rcl_cmake_library_set_init_script(renderive::pthreads4w
|
||||
"set(PThreads4W_DIR \"${PTHREADS4W_CONFIG_DIR}\")"
|
||||
"set(PThreads4W_INCLUDE_DIR \"${PTHREADS4W_INCLUDE_DIR}\")"
|
||||
)
|
||||
rcl_cmake_library_set_clear_script(renderive::pthreads4w
|
||||
"unset(PThreads4W_DIR)"
|
||||
"unset(PThreads4W_INCLUDE_DIR)"
|
||||
"unset(PThreads4W_LIBRARY)"
|
||||
)
|
||||
endblock()
|
||||
+35
@@ -1,4 +1,7 @@
|
||||
include_guard(GLOBAL)
|
||||
#rcl_init("renderive")
|
||||
#include(${CMAKE_CURRENT_LIST_DIR}/cmake/pthreads4w.cmake)
|
||||
#include(${CMAKE_CURRENT_LIST_DIR}/cmake/datoviz.cmake)
|
||||
set(renderive_dependencies
|
||||
global::GTest
|
||||
global::blend2d
|
||||
@@ -6,8 +9,10 @@ set(renderive_dependencies
|
||||
global::mpmcqueue
|
||||
global::drogon
|
||||
global::libdatachannel
|
||||
# renderive::datoviz
|
||||
${Psc_Core_dependencies}
|
||||
)
|
||||
#rcl_generate_registered_cmake_library()
|
||||
rcl_load_dependency_environment(${renderive_dependencies})
|
||||
library_is_installed_with_rely(renderive_dependencies_installed ${renderive_dependencies})
|
||||
if (NOT renderive_dependencies_installed)
|
||||
@@ -131,3 +136,33 @@ if (NOT TARGET Adminive::Nlohmann)
|
||||
unset(renderive_saved_build_testing)
|
||||
endif ()
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/web.cmake)
|
||||
set(renderive_package_includes
|
||||
"^.*$"
|
||||
)
|
||||
set(renderive_package_excludes
|
||||
# "^[.]git/.*$"
|
||||
# "^[.]idea/.*$"
|
||||
"^data/.*$"
|
||||
"^performance/.*$"
|
||||
"^cmake-build-[^/]*/.*$"
|
||||
"^(.*/)?node_modules/.*$"
|
||||
"^(.*/)?.git/.*$"
|
||||
"^(.*/)?wwwroot/.*$"
|
||||
"^(.*/)?dist/.*$"
|
||||
"^(.*/)?htdocs/.*$"
|
||||
"^(.*/)?model/.*$"
|
||||
"^(.*/)?.idea/.*$"
|
||||
"^(.*/)?.playwright-cli/.*$"
|
||||
"^(.*/)?performance/.*$"
|
||||
"^(.*/)?dll_source_lzy/.*$"
|
||||
"^.*[.]tsbuildinfo$"
|
||||
"^.*[.]zip$"
|
||||
"^.*[.]otf$"
|
||||
)
|
||||
_add_project_zip_target(
|
||||
renderive_package_zip
|
||||
"${CMAKE_CURRENT_LIST_DIR}/renderive_package.zip"
|
||||
"${CMAKE_SOURCE_DIR}"
|
||||
renderive_package_includes
|
||||
renderive_package_excludes
|
||||
)
|
||||
@@ -279,8 +279,14 @@ public:
|
||||
}
|
||||
|
||||
void set_state(Gallery_State state) {
|
||||
const bool render_schedule_changed = frame_mode_ == Gallery_Frame_Mode::Low_Latency &&
|
||||
(bool_value(state_, "frequency_limit_enabled") != bool_value(state, "frequency_limit_enabled") ||
|
||||
number_value(state_, "max_render_fps") != number_value(state, "max_render_fps") ||
|
||||
bool_value(state_, "consumer_feedback_enabled") != bool_value(state, "consumer_feedback_enabled"));
|
||||
state_ = std::move(state);
|
||||
apply_state();
|
||||
if (render_schedule_changed)
|
||||
render_history_.clear();
|
||||
}
|
||||
|
||||
void resize(Size size) {
|
||||
@@ -1572,75 +1578,47 @@ struct Gallery_Plot_Session::Impl {
|
||||
void run_automatic_renderer(std::stop_token stop) {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
std::unique_lock lock(mutex);
|
||||
auto next_deadline = Clock::now();
|
||||
std::uint64_t active_revision = std::numeric_limits<std::uint64_t>::max();
|
||||
std::optional<Clock::time_point> last_render_started;
|
||||
std::uint64_t active_generation = std::numeric_limits<std::uint64_t>::max();
|
||||
while (!stop.stop_requested()) {
|
||||
scheduler_condition.wait(lock, [this, &stop] {
|
||||
return stop.stop_requested() ||
|
||||
(scene && scene->can_render_automatically());
|
||||
return stop.stop_requested() || (scene && scene->can_render_automatically());
|
||||
});
|
||||
if (stop.stop_requested())
|
||||
break;
|
||||
if (active_generation != scene_generation) {
|
||||
active_generation = scene_generation;
|
||||
last_render_started.reset();
|
||||
}
|
||||
if (foreground_waiters.load(std::memory_order_acquire) != 0) {
|
||||
scheduler_condition.wait(lock, [this, &stop] {
|
||||
return stop.stop_requested() ||
|
||||
foreground_waiters.load(std::memory_order_acquire) == 0;
|
||||
return stop.stop_requested() || foreground_waiters.load(std::memory_order_acquire) == 0;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto revision = scheduler_revision;
|
||||
const auto cycle_started = Clock::now();
|
||||
if (active_revision != revision) {
|
||||
active_revision = revision;
|
||||
next_deadline = cycle_started;
|
||||
}
|
||||
(void)scene->render_latest_frame();
|
||||
const auto interval = std::chrono::nanoseconds(
|
||||
std::max<std::uint64_t>(1, scene->kernel_refresh_interval_ns()));
|
||||
if (next_deadline + interval < cycle_started)
|
||||
next_deadline = cycle_started;
|
||||
next_deadline += interval;
|
||||
const auto deadline = next_deadline;
|
||||
// A render that already consumed the whole interval used to let the
|
||||
// worker immediately reacquire the session mutex. Heavy scenes
|
||||
// (notably Constellation) could then starve pixel pulls, observer
|
||||
// requests and shutdown indefinitely. Explicitly hand the mutex to
|
||||
// registered foreground waiters; otherwise yield without using a
|
||||
// timed wait (which Windows may round to ~15 ms).
|
||||
if (Clock::now() >= deadline) {
|
||||
if (foreground_waiters.load(std::memory_order_acquire) != 0) {
|
||||
scheduler_condition.wait(lock, [this, &stop] {
|
||||
return stop.stop_requested() ||
|
||||
foreground_waiters.load(std::memory_order_acquire) == 0;
|
||||
const auto interval = std::chrono::nanoseconds(std::max<std::uint64_t>(1, scene->kernel_refresh_interval_ns()));
|
||||
const auto now = Clock::now();
|
||||
const auto deadline = last_render_started ? *last_render_started + interval : now;
|
||||
if (now < deadline) {
|
||||
const auto spin_window = std::min(interval / 4, std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::microseconds(250)));
|
||||
const auto coarse_deadline = deadline > now + spin_window ? deadline - spin_window : now;
|
||||
if (now < coarse_deadline) {
|
||||
scheduler_condition.wait_until(lock, coarse_deadline, [this, &stop, revision, active_generation] {
|
||||
return stop.stop_requested() || scheduler_revision != revision || scene_generation != active_generation ||
|
||||
foreground_waiters.load(std::memory_order_acquire) != 0 || !scene || !scene->can_render_automatically();
|
||||
});
|
||||
} else {
|
||||
lock.unlock();
|
||||
std::this_thread::yield();
|
||||
lock.lock();
|
||||
continue;
|
||||
}
|
||||
lock.unlock();
|
||||
while (!stop.stop_requested() && Clock::now() < deadline)
|
||||
std::this_thread::yield();
|
||||
lock.lock();
|
||||
continue;
|
||||
}
|
||||
const auto spin_window = std::min(
|
||||
interval / 4,
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::microseconds(250)));
|
||||
const auto coarse_deadline = deadline > cycle_started + spin_window ?
|
||||
deadline - spin_window : cycle_started;
|
||||
if (Clock::now() < coarse_deadline) {
|
||||
const bool interrupted = scheduler_condition.wait_until(
|
||||
lock, coarse_deadline, [this, &stop, revision] {
|
||||
return stop.stop_requested() || scheduler_revision != revision ||
|
||||
foreground_waiters.load(std::memory_order_acquire) != 0;
|
||||
});
|
||||
if (interrupted)
|
||||
continue;
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
while (!stop.stop_requested() && Clock::now() < deadline)
|
||||
std::this_thread::yield();
|
||||
lock.lock();
|
||||
const auto started = Clock::now();
|
||||
(void)scene->render_latest_frame();
|
||||
last_render_started = started;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -975,6 +975,85 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres
|
||||
observer.at("limit_state") == "consumer_limited");
|
||||
}
|
||||
|
||||
TEST(RenderiveWebGallery, FrequencyLimitCannotBeBypassedByForegroundRescheduleEvents) {
|
||||
Gallery_Plot_Session session(true);
|
||||
ASSERT_EQ(response_json(session.handle(gallery_request(
|
||||
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
|
||||
"case_state");
|
||||
const auto patched = patch_controls(session, {
|
||||
{"frequency_limit_enabled", true},
|
||||
{"max_render_fps", 30.0},
|
||||
{"consumer_feedback_enabled", false}
|
||||
});
|
||||
ASSERT_EQ(patched.at("type"), "case_state");
|
||||
const auto& patched_observer = patched.at("telemetry").at("kernel_observer");
|
||||
EXPECT_TRUE(patched_observer.at("frequency_limit_enabled").get<bool>());
|
||||
EXPECT_FALSE(patched_observer.at("consumer_feedback_enabled").get<bool>());
|
||||
EXPECT_EQ(patched_observer.at("target_interval_ns"), 33'333'333);
|
||||
ASSERT_TRUE(wait_for_condition([&session] {
|
||||
return successful_render_count(session) >= 3;
|
||||
}));
|
||||
const auto pointer = Web_Event_Adapter::decode(
|
||||
R"({"category":"event","type":"pointer_move","x":320,"y":180,"button":"none","buttons":0,"modifiers":0})");
|
||||
ASSERT_TRUE(pointer.has_value());
|
||||
const auto before = successful_render_count(session);
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
const auto deadline = started + std::chrono::milliseconds(450);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
EXPECT_FALSE(session.handle(*pointer).has_value());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
const auto elapsed = std::chrono::steady_clock::now() - started;
|
||||
const auto after = successful_render_count(session);
|
||||
const auto rendered = after - before;
|
||||
EXPECT_GT(rendered, 0U);
|
||||
EXPECT_LE(rendered, 16U);
|
||||
const auto telemetry = observe_telemetry(session);
|
||||
const auto& observer = telemetry.at("kernel_observer");
|
||||
EXPECT_EQ(observer.at("target_interval_ns"), 33'333'333);
|
||||
EXPECT_GE(observer.at("next_refresh_interval_ns").get<std::uint64_t>(), 33'333'333U);
|
||||
EXPECT_GE(elapsed, std::chrono::milliseconds(450));
|
||||
}
|
||||
|
||||
TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundRescheduleEvents) {
|
||||
Gallery_Plot_Session session(true);
|
||||
ASSERT_EQ(response_json(session.handle(gallery_request(
|
||||
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
|
||||
"case_state");
|
||||
ASSERT_EQ(patch_controls(session, {
|
||||
{"frequency_limit_enabled", false},
|
||||
{"consumer_feedback_enabled", true}
|
||||
}).at("type"), "case_state");
|
||||
auto observed = response_json(session.handle(gallery_request(
|
||||
Gallery_Request_Kind::Observe,
|
||||
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":25,"presentation_fps":25,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":40,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})")));
|
||||
auto observer = observed.at("telemetry").at("kernel_observer");
|
||||
EXPECT_FALSE(observer.at("frequency_limit_enabled").get<bool>());
|
||||
EXPECT_TRUE(observer.at("consumer_feedback_enabled").get<bool>());
|
||||
EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000);
|
||||
ASSERT_TRUE(wait_for_condition([&session] {
|
||||
return successful_render_count(session) >= 3;
|
||||
}));
|
||||
const auto pointer = Web_Event_Adapter::decode(
|
||||
R"({"category":"event","type":"pointer_move","x":320,"y":180,"button":"none","buttons":0,"modifiers":0})");
|
||||
ASSERT_TRUE(pointer.has_value());
|
||||
const auto before = successful_render_count(session);
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
const auto deadline = started + std::chrono::milliseconds(450);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
EXPECT_FALSE(session.handle(*pointer).has_value());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
const auto after = successful_render_count(session);
|
||||
const auto rendered = after - before;
|
||||
EXPECT_GT(rendered, 0U);
|
||||
EXPECT_LE(rendered, 14U);
|
||||
observed = observe_telemetry(session);
|
||||
observer = observed.at("kernel_observer");
|
||||
EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000);
|
||||
EXPECT_GE(observer.at("next_refresh_interval_ns").get<std::uint64_t>(), 40'000'000U);
|
||||
}
|
||||
|
||||
TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedbackWithoutTransportFpsCap) {
|
||||
Gallery_Plot_Session session(true);
|
||||
ASSERT_EQ(response_json(session.handle(gallery_request(
|
||||
|
||||
+16
-4
@@ -498,6 +498,7 @@ function openMenu(card, x, y) {
|
||||
}
|
||||
function closeMenu() { elements.menu.hidden = true; activeCard = null; }
|
||||
function renderControl(item) {
|
||||
const card = activeCard;
|
||||
const row = document.createElement("div"); row.className = "control-row";
|
||||
const copy = document.createElement("div"); copy.className = "control-copy";
|
||||
const label = document.createElement("label"); label.textContent = item.label;
|
||||
@@ -513,11 +514,22 @@ function renderControl(item) {
|
||||
if (item.input === "number") { input.min = item.minimum; input.max = item.maximum; input.step = item.step || "any"; }
|
||||
}
|
||||
input.className = "control-input"; input.title = item.description || item.api;
|
||||
input.addEventListener("change", () => {
|
||||
let submitTimer = 0;
|
||||
const submit = () => {
|
||||
clearTimeout(submitTimer); submitTimer = 0;
|
||||
const value = item.input === "boolean" ? input.checked : item.input === "number" ? Number(input.value) : input.value;
|
||||
activeCard.send("gallery_patch", {patch: {[item.id]: value}});
|
||||
elements.menuStatus.textContent = `调用 ${item.api}`;
|
||||
});
|
||||
if (item.input === "number" && (input.value === "" || !Number.isFinite(value))) return;
|
||||
card.send("gallery_patch", {patch: {[item.id]: value}});
|
||||
elements.menuStatus.textContent = `提交 ${item.api} · 等待后端回读`;
|
||||
};
|
||||
if (item.input === "number") {
|
||||
input.addEventListener("input", () => {
|
||||
clearTimeout(submitTimer);
|
||||
submitTimer = setTimeout(submit, 120);
|
||||
});
|
||||
input.addEventListener("change", submit);
|
||||
input.addEventListener("keydown", event => { if (event.key === "Enter") submit(); });
|
||||
} else input.addEventListener("change", submit);
|
||||
row.append(copy, input); return row;
|
||||
}
|
||||
function renderAction(item) {
|
||||
|
||||
Reference in New Issue
Block a user