This commit is contained in:
2026-08-15 19:58:39 +08:00
parent 8bfaeeb265
commit 26bbc29a16
29 changed files with 622 additions and 201 deletions
+15 -5
View File
@@ -85,18 +85,27 @@ void Capture_Repository::begin_session(Capture_Session_Id session_id,
return session.session_id == session_id;
}))
throw std::invalid_argument("capture session already exists");
while (sessions_.size() >= retained_session_count) {
const auto completed = std::find_if(sessions_.begin(), sessions_.end(),
[](const Capture_Session& session) { return !session.active(); });
if (completed == sessions_.end())
throw std::logic_error("capture repository retention is occupied by active sessions");
sessions_.erase(completed);
}
sessions_.push_back({session_id, requested_count, {}});
sessions_.back().frames.reserve(requested_count);
}
void Capture_Repository::publish(Capture_Session_Id session_id,
std::shared_ptr<const Frame_Snapshot> snapshot,
std::shared_ptr<const Render_Plan> render_plan,
Frame_Analysis analysis) {
if (!snapshot)
throw std::invalid_argument("captured frame snapshot is null");
if (!snapshot || !render_plan)
throw std::invalid_argument("captured frame snapshot or render plan is null");
if (analysis.frame_id != snapshot->frame_id ||
analysis.render_plan_version != snapshot->render_plan_version)
throw std::invalid_argument("captured frame analysis does not match snapshot");
analysis.render_plan_version != snapshot->render_plan_version ||
render_plan->version != snapshot->render_plan_version)
throw std::invalid_argument("captured frame analysis does not match snapshot or render plan");
std::lock_guard lock(mutex_);
const auto iterator = std::find_if(sessions_.begin(), sessions_.end(),
[session_id](const auto& session) { return session.session_id == session_id; });
@@ -104,7 +113,8 @@ void Capture_Repository::publish(Capture_Session_Id session_id,
throw std::invalid_argument("capture session does not exist");
if (!iterator->active())
throw std::logic_error("capture session is complete");
iterator->frames.push_back({std::move(snapshot), std::move(analysis)});
iterator->frames.push_back({std::move(snapshot), std::move(render_plan),
std::move(analysis)});
}
std::optional<Capture_Session> Capture_Repository::session(
+4
View File
@@ -8,6 +8,7 @@
#include <vector>
#include "Capture_Types.hpp"
#include "renderive/render_graph/Frame_Analysis.hpp"
#include "renderive/render_graph/Render_Plan.hpp"
class Capture_Controller {
public:
@@ -39,6 +40,7 @@ private:
struct Captured_Frame {
std::shared_ptr<const Frame_Snapshot> snapshot;
std::shared_ptr<const Render_Plan> render_plan;
Frame_Analysis analysis;
};
@@ -59,10 +61,12 @@ public:
void begin_session(Capture_Session_Id session_id, std::size_t requested_count);
void publish(Capture_Session_Id session_id,
std::shared_ptr<const Frame_Snapshot> snapshot,
std::shared_ptr<const Render_Plan> render_plan,
Frame_Analysis analysis);
[[nodiscard]] std::optional<Capture_Session> session(Capture_Session_Id session_id) const;
[[nodiscard]] std::vector<Capture_Session_Id> sessions() const;
private:
static constexpr std::size_t retained_session_count = 16;
mutable std::mutex mutex_;
std::vector<Capture_Session> sessions_;
};
@@ -112,6 +112,8 @@ std::shared_ptr<const Render_Plan> Render_Plan_History::publish(Render_Graph gra
plan->version = next_version_++;
plan->graph = std::move(graph);
plans_.push_back(plan);
if (plans_.size() > retained_plan_count)
plans_.erase(plans_.begin());
return plan;
}
@@ -66,6 +66,7 @@ public:
[[nodiscard]] std::shared_ptr<const Render_Plan> current() const;
[[nodiscard]] std::shared_ptr<const Render_Plan> find(Render_Plan_Version version) const;
private:
static constexpr std::size_t retained_plan_count = 64;
mutable std::mutex mutex_;
std::vector<std::shared_ptr<const Render_Plan>> plans_;
Render_Plan_Version next_version_{1};
+44 -5
View File
@@ -1,11 +1,13 @@
#include "Scene_Base.hpp"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <memory_resource>
#include <queue>
#include <semaphore>
#include <stdexcept>
#include <string>
#include <unordered_map>
@@ -38,7 +40,19 @@ public:
wait();
}
void submit(Operation operation) {
operations_.push(std::move(operation));
const auto started = std::chrono::steady_clock::now();
if (!slots_.try_acquire()) {
backpressure_count_.fetch_add(1, std::memory_order_relaxed);
slots_.acquire();
const auto waited = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - started).count();
if (waited > 0)
backpressure_wait_ns_.fetch_add(static_cast<std::uint64_t>(waited), std::memory_order_relaxed);
}
const std::size_t queued = queued_.fetch_add(1, std::memory_order_relaxed) + 1;
update_peak(peak_queued_, queued);
if (!operations_.try_push(std::move(operation)))
std::terminate();
schedule();
}
void wait() noexcept {
@@ -48,7 +62,21 @@ public:
} catch (...) {
}
}
[[nodiscard]] Scene_Execution_Statistics statistics() const noexcept {
return {
static_cast<std::size_t>(default_capacity),
queued_.load(std::memory_order_relaxed),
peak_queued_.load(std::memory_order_relaxed),
backpressure_count_.load(std::memory_order_relaxed),
backpressure_wait_ns_.load(std::memory_order_relaxed)
};
}
private:
static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept {
std::size_t current = peak.load(std::memory_order_relaxed);
while (current < value && !peak.compare_exchange_weak(
current, value, std::memory_order_relaxed)) {}
}
void schedule() {
if (scheduled_.exchange(true, std::memory_order_acq_rel))
return;
@@ -57,19 +85,26 @@ private:
}
void execute_one() {
Operation operation;
if (operations_.try_pop(operation))
if (operations_.try_pop(operation)) {
queued_.fetch_sub(1, std::memory_order_relaxed);
slots_.release();
operation();
}
scheduled_.store(false, std::memory_order_release);
if (!operations_.empty())
schedule();
}
static constexpr std::size_t default_capacity = 64;
static constexpr std::ptrdiff_t default_capacity = 64;
std::counting_semaphore<default_capacity> slots_{default_capacity};
oneapi::tbb::flow::graph graph_;
oneapi::tbb::concurrent_bounded_queue<Operation> operations_;
oneapi::tbb::flow::function_node<Message, Message> node_;
std::atomic_size_t queued_{};
std::atomic_size_t peak_queued_{};
std::atomic_uint64_t backpressure_count_{};
std::atomic_uint64_t backpressure_wait_ns_{};
std::atomic_bool scheduled_{};
};
class Scene_Base::Render_Execution_Scope {
public:
explicit Render_Execution_Scope(Scene_Base& scene) noexcept
@@ -799,6 +834,10 @@ std::vector<Plan_Version_Statistics> Scene_Base::plan_version_statistics(
return analyze_plan_versions(analyses);
}
Scene_Execution_Statistics Scene_Base::execution_statistics() const noexcept {
return execution_context_->statistics();
}
std::pmr::memory_resource& Scene_Base::memory_resource() const noexcept {
return memory_domain_->resource();
}
@@ -1273,7 +1312,7 @@ void Scene_Base::execute_render_graph(Render_Task& task) {
}
if (completed) {
capture_repository_.publish(
capture_ticket.session_id, completed,
capture_ticket.session_id, completed, compiled.plan,
analyze_frame(*compiled.plan, *completed));
}
capture_controller_.finish_frame(capture_ticket, true);
@@ -3,6 +3,7 @@
#include <atomic>
#include <condition_variable>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <functional>
@@ -59,6 +60,14 @@ public:
const Scene_Render_Context& context) = 0;
};
struct Scene_Execution_Statistics {
std::size_t capacity{};
std::size_t queued{};
std::size_t peak_queued{};
std::uint64_t backpressure_count{};
std::uint64_t backpressure_wait_ns{};
};
class Scene_Base {
private:
struct Render_Completion;
@@ -171,6 +180,7 @@ public:
Capture_Session_Id session_id) const;
[[nodiscard]] std::vector<Plan_Version_Statistics> plan_version_statistics(
Capture_Session_Id session_id) const;
[[nodiscard]] Scene_Execution_Statistics execution_statistics() const noexcept;
[[nodiscard]] std::pmr::memory_resource& memory_resource() const noexcept;
[[nodiscard]] std::pmr::memory_resource& upstream_memory_resource() const noexcept;
@@ -1,6 +1,7 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <functional>
namespace renderive::scheduling {
struct Scheduler_Statistics {
std::size_t concurrency{};
@@ -15,4 +16,5 @@ struct Scheduler_Statistics {
};
[[nodiscard]] std::size_t scheduler_concurrency() noexcept;
[[nodiscard]] Scheduler_Statistics scheduler_statistics() noexcept;
void enqueue_task(std::function<void()> task);
}
@@ -95,4 +95,7 @@ std::size_t scheduler_concurrency() noexcept {
Scheduler_Statistics scheduler_statistics() noexcept {
return detail::OneTBB_Runtime::instance().statistics();
}
void enqueue_task(std::function<void()> task) {
detail::OneTBB_Runtime::instance().enqueue(std::move(task));
}
}
@@ -138,7 +138,7 @@ TEST(render_dag_test, capture_controller_and_repository_capture_exact_requested_
execution->worker_id = static_cast<std::uint32_t>(slot);
execution->status = Node_Execution_Status::complete;
}
repository.publish(session_id, snapshot, analyze_frame(*plan, *snapshot));
repository.publish(session_id, snapshot, plan, analyze_frame(*plan, *snapshot));
}
const auto session = repository.session(session_id);
@@ -232,3 +232,49 @@ TEST(render_dag_test, concurrent_workers_reserve_exactly_one_capture_slot_each)
EXPECT_EQ(captured.load(std::memory_order_relaxed), requested);
EXPECT_FALSE(controller.state().enabled());
}
TEST(render_dag_test, render_plan_history_has_bounded_retention) {
Render_Plan_History history;
std::vector<Render_Plan_Version> versions;
for (std::uint64_t index = 0; index < 70; ++index) {
Render_Graph_Builder builder;
builder.emplace(1000 + index, 2000 + index, "Node", Render_Node_Kind::prepare);
versions.push_back(history.publish(std::move(builder).finish())->version);
}
EXPECT_FALSE(history.find(versions.front()));
EXPECT_TRUE(history.find(versions.back()));
ASSERT_TRUE(history.current());
EXPECT_EQ(history.current()->version, versions.back());
}
TEST(render_dag_test, capture_repository_retains_only_recent_completed_sessions) {
Capture_Repository repository;
Render_Graph_Builder builder;
builder.emplace(3000, 4000, "Node", Render_Node_Kind::prepare);
Render_Plan_History history;
const auto plan = history.publish(std::move(builder).finish());
for (Capture_Session_Id session_id = 1; session_id <= 20; ++session_id) {
repository.begin_session(session_id, 1);
auto snapshot = std::make_shared<Frame_Snapshot>();
snapshot->frame_id = session_id;
snapshot->render_plan_version = plan->version;
snapshot->render_start_ns = session_id * 100;
snapshot->render_end_ns = snapshot->render_start_ns + 10;
snapshot->node_executions.resize(1);
auto& execution = snapshot->node_executions.front();
execution.node_id = plan->graph.nodes.front().node_id;
execution.ready_time_ns = snapshot->render_start_ns;
execution.start_time_ns = snapshot->render_start_ns;
execution.cpu_end_time_ns = snapshot->render_end_ns;
execution.end_time_ns = snapshot->render_end_ns;
execution.status = Node_Execution_Status::complete;
repository.publish(session_id, snapshot, plan, analyze_frame(*plan, *snapshot));
}
const auto sessions = repository.sessions();
ASSERT_EQ(sessions.size(), 16U);
EXPECT_EQ(sessions.front(), 5U);
EXPECT_EQ(sessions.back(), 20U);
EXPECT_FALSE(repository.session(1));
EXPECT_TRUE(repository.session(20));
}
+8 -3
View File
@@ -21,9 +21,14 @@ if (NOT TARGET Renderive_render_2D)
rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_Qt requires Renderive_render_2D")
return()
endif ()
set(Renderive_Qt_source_dir "${CMAKE_CURRENT_LIST_DIR}")
append_glob_source(Renderive_Qt_sources "${Renderive_Qt_source_dir}")
list(FILTER Renderive_Qt_sources EXCLUDE REGEX "/tests/")
set(Renderive_Qt_source_dirs
"${CMAKE_CURRENT_LIST_DIR}/bridge"
"${CMAKE_CURRENT_LIST_DIR}/plot"
)
set(Renderive_Qt_sources)
foreach (Renderive_Qt_source_dir IN LISTS Renderive_Qt_source_dirs)
append_glob_source(Renderive_Qt_sources "${Renderive_Qt_source_dir}")
endforeach ()
add_library(Renderive_Qt STATIC ${Renderive_Qt_sources})
set_target_properties(Renderive_Qt PROPERTIES AUTOMOC ON)
target_include_directories(Renderive_Qt PUBLIC
+1 -2
View File
@@ -19,9 +19,8 @@ if (NOT TARGET Renderive_Kernel)
rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_2D requires Renderive_Kernel")
return()
endif ()
set(Renderive_render_2D_source_dir "${CMAKE_CURRENT_LIST_DIR}")
set(Renderive_render_2D_source_dir "${CMAKE_CURRENT_LIST_DIR}/render_2D")
append_glob_source(Renderive_render_2D_sources "${Renderive_render_2D_source_dir}")
list(FILTER Renderive_render_2D_sources EXCLUDE REGEX "/tests/")
add_library(Renderive_render_2D STATIC ${Renderive_render_2D_sources})
target_include_directories(Renderive_render_2D PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>"
+12 -9
View File
@@ -1,7 +1,6 @@
# datoviz
include(${CMAKE_CURRENT_LIST_DIR}/cmake/export.cmake)
set(Renderive_render_3D_dependencies
render_3D::PThreads4W
render_3D::cglm
render_3D::Vulkan-Hpp
render_3D::VulkanMemoryAllocator
@@ -13,6 +12,9 @@ set(Renderive_render_3D_dependencies
render_3D::tinyxml2
global::zlib
)
if (WIN32)
list(APPEND Renderive_render_3D_dependencies render_3D::PThreads4W)
endif ()
if (RENDERIVE_BUILD_TESTS)
list(APPEND Renderive_render_3D_dependencies global::GTest)
endif ()
@@ -25,10 +27,13 @@ if (Renderive_render_3D_missing)
endif ()
rcl_load_dependency_environment(${Renderive_render_3D_dependencies})
find_package(Vulkan REQUIRED)
find_package(PThreads4W QUIET)
if (NOT PThreads4W_FOUND)
rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_3D PThreads4W installation is incomplete. Rebuild render_3D::PThreads4W")
return()
find_package(Threads REQUIRED)
if (WIN32)
find_package(PThreads4W QUIET)
if (NOT PThreads4W_FOUND)
rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_3D PThreads4W installation is incomplete. Rebuild render_3D::PThreads4W")
return()
endif ()
endif ()
find_package(cglm CONFIG REQUIRED)
find_package(volk CONFIG REQUIRED)
@@ -89,10 +94,8 @@ if (NOT TARGET Renderive_Kernel)
return()
endif ()
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/datoviz" "${CMAKE_CURRENT_BINARY_DIR}/datoviz")
set(Renderive_render_3D_source_dir "${CMAKE_CURRENT_LIST_DIR}")
set(Renderive_render_3D_source_dir "${CMAKE_CURRENT_LIST_DIR}/render_3D")
append_glob_source(Renderive_render_3D_sources "${Renderive_render_3D_source_dir}")
list(FILTER Renderive_render_3D_sources EXCLUDE REGEX "/tests/")
list(FILTER Renderive_render_3D_sources EXCLUDE REGEX "/datoviz/")
add_library(Renderive_render_3D STATIC ${Renderive_render_3D_sources})
target_include_directories(Renderive_render_3D PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>"
@@ -100,7 +103,7 @@ target_include_directories(Renderive_render_3D PUBLIC
target_compile_features(Renderive_render_3D PUBLIC cxx_std_20)
target_link_libraries(Renderive_render_3D
PUBLIC Renderive_Kernel
PRIVATE ${Renderive_render_3D_datoviz_targets} tinyobjloader::tinyobjloader
PRIVATE ${Renderive_render_3D_datoviz_targets} tinyobjloader::tinyobjloader Threads::Threads
)
if (MSVC)
# Datoviz is embedded from OBJECT libraries in this static archive. Its Windows headers
+13 -4
View File
@@ -138,11 +138,20 @@ if(TARGET Vulkan::Headers)
else()
set(Renderive_render_3D_datoviz_vulkan_target Vulkan::Vulkan)
endif()
# CORE/commonDatoviz 线沿 PThreads4W
if(WIN32)
if(NOT TARGET PThreads4W::PThreads4W)
message(FATAL_ERROR "Datoviz requires PThreads4W::PThreads4W on Windows")
endif()
set(Renderive_render_3D_datoviz_thread_target PThreads4W::PThreads4W)
else()
find_package(Threads REQUIRED)
set(Renderive_render_3D_datoviz_thread_target Threads::Threads)
endif()
# CORE/commonDatoviz 使线
Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_common_sources "${Renderive_render_3D_datoviz_src_dir}/common")
add_library(Renderive_render_3D_datoviz_common OBJECT ${Renderive_render_3D_datoviz_common_sources})
target_include_directories(Renderive_render_3D_datoviz_common PUBLIC "${Renderive_render_3D_datoviz_include_dir}" INTERFACE "${Renderive_render_3D_datoviz_src_dir}/common")
target_link_libraries(Renderive_render_3D_datoviz_common PUBLIC PThreads4W::PThreads4W)
target_link_libraries(Renderive_render_3D_datoviz_common PUBLIC ${Renderive_render_3D_datoviz_thread_target})
Renderive_render_3D_datoviz_configure_target(Renderive_render_3D_datoviz_common)
# CORE/fileio Datoviz fileio + fpng 使 ZLIB
Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_fileio_sources "${Renderive_render_3D_datoviz_src_dir}/fileio")
@@ -162,13 +171,13 @@ Renderive_render_3D_datoviz_configure_target(Renderive_render_3D_datoviz_geom)
Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_math_sources "${Renderive_render_3D_datoviz_src_dir}/math")
add_library(Renderive_render_3D_datoviz_math OBJECT ${Renderive_render_3D_datoviz_math_sources})
target_include_directories(Renderive_render_3D_datoviz_math PUBLIC "${Renderive_render_3D_datoviz_include_dir}" "${Renderive_render_3D_datoviz_src_dir}/common")
target_link_libraries(Renderive_render_3D_datoviz_math PUBLIC cglm::cglm PThreads4W::PThreads4W)
target_link_libraries(Renderive_render_3D_datoviz_math PUBLIC cglm::cglm ${Renderive_render_3D_datoviz_thread_target})
Renderive_render_3D_datoviz_configure_target(Renderive_render_3D_datoviz_math)
# CORE/thread线线 Datoviz runtime
Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_thread_sources "${Renderive_render_3D_datoviz_src_dir}/thread")
add_library(Renderive_render_3D_datoviz_thread OBJECT ${Renderive_render_3D_datoviz_thread_sources})
target_include_directories(Renderive_render_3D_datoviz_thread PUBLIC "${Renderive_render_3D_datoviz_include_dir}" "${Renderive_render_3D_datoviz_src_dir}/common")
target_link_libraries(Renderive_render_3D_datoviz_thread PUBLIC PThreads4W::PThreads4W)
target_link_libraries(Renderive_render_3D_datoviz_thread PUBLIC ${Renderive_render_3D_datoviz_thread_target})
Renderive_render_3D_datoviz_configure_target(Renderive_render_3D_datoviz_thread)
# CONTROLLER/input Datoviz window/glfw
Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_input_sources "${Renderive_render_3D_datoviz_src_dir}/input")
+1 -1
View File
@@ -53,7 +53,7 @@ function(Renderive_render_3D_datoviz_add_test_runner target source_var component
target_link_libraries(${target} PRIVATE
${ARGN}
Renderive_render_3D_datoviz_testing
PThreads4W::PThreads4W)
${Renderive_render_3D_datoviz_thread_target})
if(Renderive_render_3D_datoviz_msvc_system_library_dirs)
target_link_directories(${target} PRIVATE
${Renderive_render_3D_datoviz_msvc_system_library_dirs})
+17
View File
@@ -106,6 +106,7 @@ struct Scene_Model {
[[nodiscard]] virtual std::shared_ptr<const Pixel_Frame> latest_frame()
const = 0;
[[nodiscard]] virtual Frame_Status frame_status() const = 0;
[[nodiscard]] virtual Runtime_Statistics runtime_statistics() const noexcept = 0;
[[nodiscard]] virtual Scene_Base& scene() noexcept = 0;
[[nodiscard]] virtual const Scene_Base& scene() const noexcept = 0;
};
@@ -295,6 +296,18 @@ struct Basic_Point_Scene final
return result;
}
[[nodiscard]] Runtime_Statistics runtime_statistics() const noexcept override {
const auto render = render_domain->statistics();
const auto completion = detail::Gpu_Completion_Service::instance().statistics();
return {
{render.capacity, render.admitted, render.peak_admitted, render.queued,
render.peak_queued, render.backpressure_count, render.backpressure_wait_ns},
{completion.capacity, completion.in_flight, completion.peak_in_flight,
completion.watched, completion.peak_watched, completion.backpressure_count,
completion.backpressure_wait_ns}
};
}
[[nodiscard]] Scene_Base& scene() noexcept override { return *this; }
[[nodiscard]] const Scene_Base& scene() const noexcept override {
return *this;
@@ -544,6 +557,10 @@ Frame_Status Point_Scene::frame_status() const {
return impl_->model->frame_status();
}
Runtime_Statistics Point_Scene::runtime_statistics() const noexcept {
return impl_->model->runtime_statistics();
}
Scene_Base& Point_Scene::render_scene() noexcept {
return impl_->model->scene();
}
+16
View File
@@ -68,6 +68,21 @@ struct Pixel_Frame {
std::vector<std::byte> rgba8;
};
struct Runtime_Admission_Statistics {
std::size_t capacity{};
std::size_t active{};
std::size_t peak_active{};
std::size_t queued{};
std::size_t peak_queued{};
std::uint64_t backpressure_count{};
std::uint64_t backpressure_wait_ns{};
};
struct Runtime_Statistics {
Runtime_Admission_Statistics render_domain;
Runtime_Admission_Statistics gpu_completion;
};
struct Frame_Status {
Frame_Mode mode{Frame_Mode::Low_Latency};
double frequency_hz{};
@@ -123,6 +138,7 @@ public:
[[nodiscard]] std::shared_ptr<const Pixel_Frame> latest_frame() const;
[[nodiscard]] Frame_Status frame_status() const;
[[nodiscard]] Runtime_Statistics runtime_statistics() const noexcept;
[[nodiscard]] Scene_Base& render_scene() noexcept;
[[nodiscard]] const Scene_Base& render_scene() const noexcept;
@@ -1,5 +1,4 @@
#include "Gpu_Completion_Service.h"
#include <algorithm>
#include <stdexcept>
#include <string>
#include <utility>
@@ -28,6 +27,7 @@ void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence)
if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE)
std::terminate();
auto pending = std::exchange(pending_, {});
auto* const service = pending->service;
{
std::lock_guard lock(pending->mutex);
if (pending->status != Pending_Fence::Status::reserved)
@@ -36,9 +36,11 @@ void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence)
pending->fence = fence;
if (pending->observe)
pending->watched_at = std::chrono::steady_clock::now();
const std::size_t watched = service->watched_.fetch_add(1, std::memory_order_relaxed) + 1;
update_peak(service->peak_watched_, watched);
pending->status = Pending_Fence::Status::watched;
}
pending->service->wake();
service->wake();
}
void Gpu_Completion_Service::Reservation::cancel() noexcept {
if (!pending_)
@@ -47,6 +49,10 @@ void Gpu_Completion_Service::Reservation::cancel() noexcept {
cancel_reserved(pending);
pending->service->wake();
}
void Gpu_Completion_Service::update_peak(std::atomic_size_t& peak, std::size_t value) noexcept {
std::size_t current = peak.load(std::memory_order_relaxed);
while (current < value && !peak.compare_exchange_weak(current, value, std::memory_order_relaxed)) {}
}
void Gpu_Completion_Service::cancel_reserved(const std::shared_ptr<Pending_Fence>& pending) noexcept {
if (!pending)
return;
@@ -54,6 +60,23 @@ void Gpu_Completion_Service::cancel_reserved(const std::shared_ptr<Pending_Fence
if (pending->status == Pending_Fence::Status::reserved)
pending->status = Pending_Fence::Status::canceled;
}
void Gpu_Completion_Service::acquire_slot() {
const auto started = std::chrono::steady_clock::now();
if (!slots_.try_acquire()) {
backpressure_count_.fetch_add(1, std::memory_order_relaxed);
slots_.acquire();
const auto waited = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - started).count();
if (waited > 0)
backpressure_wait_ns_.fetch_add(static_cast<std::uint64_t>(waited), std::memory_order_relaxed);
}
const std::size_t in_flight = in_flight_.fetch_add(1, std::memory_order_relaxed) + 1;
update_peak(peak_in_flight_, in_flight);
}
void Gpu_Completion_Service::release_slot() noexcept {
in_flight_.fetch_sub(1, std::memory_order_relaxed);
slots_.release();
}
Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion completion, bool observe) {
if (!completion)
throw std::invalid_argument("GPU completion callback is empty");
@@ -63,27 +86,39 @@ Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion c
pending->completion = std::move(completion);
pending->observe = observe;
pending->service = this;
slots_.acquire();
try {
pending_.push(pending);
} catch (...) {
slots_.release();
throw;
acquire_slot();
if (!pending_.try_push(pending)) {
release_slot();
throw std::logic_error("GPU completion admission invariant violated");
}
wake();
return Reservation(std::move(pending));
}
Gpu_Completion_Service::Statistics Gpu_Completion_Service::statistics() const noexcept {
return {
static_cast<std::size_t>(default_capacity),
in_flight_.load(std::memory_order_relaxed),
peak_in_flight_.load(std::memory_order_relaxed),
watched_.load(std::memory_order_relaxed),
peak_watched_.load(std::memory_order_relaxed),
backpressure_count_.load(std::memory_order_relaxed),
backpressure_wait_ns_.load(std::memory_order_relaxed)
};
}
void Gpu_Completion_Service::wake() noexcept {
wake_.notify_one();
wake_generation_.fetch_add(1, std::memory_order_release);
wake_condition_.notify_one();
}
void Gpu_Completion_Service::run() noexcept {
std::vector<std::shared_ptr<Pending_Fence>> active;
active.reserve(static_cast<std::size_t>(default_capacity));
for (;;) {
const std::uint64_t wake_generation = wake_generation_.load(std::memory_order_acquire);
std::shared_ptr<Pending_Fence> incoming;
while (pending_.try_pop(incoming))
active.push_back(std::move(incoming));
bool progressed{};
bool has_watched{};
for (auto iterator = active.begin(); iterator != active.end();) {
auto& pending = *iterator;
VkDevice device{VK_NULL_HANDLE};
@@ -104,7 +139,7 @@ void Gpu_Completion_Service::run() noexcept {
}
if (status == Pending_Fence::Status::canceled) {
iterator = active.erase(iterator);
slots_.release();
release_slot();
progressed = true;
continue;
}
@@ -112,13 +147,14 @@ void Gpu_Completion_Service::run() noexcept {
if (stopping_.load(std::memory_order_acquire)) {
cancel_reserved(pending);
iterator = active.erase(iterator);
slots_.release();
release_slot();
progressed = true;
continue;
}
++iterator;
continue;
}
has_watched = true;
const VkResult result = vkGetFenceStatus(device, fence);
if (result == VK_NOT_READY) {
++iterator;
@@ -129,9 +165,11 @@ void Gpu_Completion_Service::run() noexcept {
completion = std::move(pending->completion);
pending->status = Pending_Fence::Status::canceled;
}
watched_.fetch_sub(1, std::memory_order_relaxed);
Result completion_result;
if (observe) {
const auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - watched_at).count();
const auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - watched_at).count();
completion_result.wait_duration_ns = duration > 0 ? static_cast<std::uint64_t>(duration) : 0;
}
if (result != VK_SUCCESS) {
@@ -146,19 +184,24 @@ void Gpu_Completion_Service::run() noexcept {
} catch (...) {
}
iterator = active.erase(iterator);
slots_.release();
release_slot();
progressed = true;
}
if (stopping_.load(std::memory_order_acquire) && active.empty() && pending_.empty())
return;
if (!progressed) {
std::unique_lock lock(wait_mutex_);
if (active.empty())
wake_.wait(lock, [this] {
return stopping_.load(std::memory_order_acquire) || !pending_.empty();
});
else
wake_.wait_for(lock, poll_interval);
if (progressed)
continue;
std::unique_lock lock(wait_mutex_);
if (wake_generation_.load(std::memory_order_acquire) != wake_generation)
continue;
if (has_watched) {
wake_condition_.wait_for(lock, poll_interval, [this, wake_generation] {
return wake_generation_.load(std::memory_order_acquire) != wake_generation;
});
} else {
wake_condition_.wait(lock, [this, wake_generation] {
return wake_generation_.load(std::memory_order_acquire) != wake_generation;
});
}
}
}
@@ -20,6 +20,15 @@ public:
std::exception_ptr error;
std::uint64_t wait_duration_ns{};
};
struct Statistics {
std::size_t capacity{};
std::size_t in_flight{};
std::size_t peak_in_flight{};
std::size_t watched{};
std::size_t peak_watched{};
std::uint64_t backpressure_count{};
std::uint64_t backpressure_wait_ns{};
};
using Completion = std::function<void(Result)>;
class Reservation final {
public:
@@ -40,6 +49,7 @@ public:
Gpu_Completion_Service(const Gpu_Completion_Service&) = delete;
Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete;
[[nodiscard]] Reservation prepare(Completion completion, bool observe);
[[nodiscard]] Statistics statistics() const noexcept;
private:
struct Pending_Fence {
enum class Status {
@@ -58,7 +68,10 @@ private:
};
Gpu_Completion_Service();
~Gpu_Completion_Service();
static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept;
static void cancel_reserved(const std::shared_ptr<Pending_Fence>& pending) noexcept;
void acquire_slot();
void release_slot() noexcept;
void wake() noexcept;
void run() noexcept;
static constexpr std::ptrdiff_t default_capacity = 1024;
@@ -66,7 +79,14 @@ private:
std::counting_semaphore<default_capacity> slots_{default_capacity};
oneapi::tbb::concurrent_bounded_queue<std::shared_ptr<Pending_Fence>> pending_;
std::mutex wait_mutex_;
std::condition_variable wake_;
std::condition_variable wake_condition_;
std::atomic_uint64_t wake_generation_{};
std::atomic_size_t in_flight_{};
std::atomic_size_t peak_in_flight_{};
std::atomic_size_t watched_{};
std::atomic_size_t peak_watched_{};
std::atomic_uint64_t backpressure_count_{};
std::atomic_uint64_t backpressure_wait_ns_{};
std::atomic_bool stopping_{};
std::thread thread_;
};
+67 -19
View File
@@ -1,10 +1,11 @@
#include "Render_Domain.h"
#include <mutex>
#include <unordered_map>
namespace renderive::render_3d::detail {
namespace {
struct Render_Domain_Registry {
std::mutex mutex;
std::unordered_map<std::uint32_t, std::shared_ptr<Render_Domain>> domains;
std::unordered_map<std::uint32_t, std::weak_ptr<Render_Domain>> domains;
};
Render_Domain_Registry& registry() {
static Render_Domain_Registry value;
@@ -15,12 +16,12 @@ Render_Domain::Prepared_Task::~Prepared_Task() {
release();
}
Render_Domain::Prepared_Task::Prepared_Task(Prepared_Task&& other) noexcept
: domain_(std::exchange(other.domain_, nullptr)), task_(std::move(other.task_)) {}
: domain_(std::move(other.domain_)), task_(std::move(other.task_)) {}
Render_Domain::Prepared_Task& Render_Domain::Prepared_Task::operator=(Prepared_Task&& other) noexcept {
if (this == &other)
return *this;
release();
domain_ = std::exchange(other.domain_, nullptr);
domain_ = std::move(other.domain_);
task_ = std::move(other.task_);
return *this;
}
@@ -28,16 +29,17 @@ void Render_Domain::Prepared_Task::release() noexcept {
if (!domain_)
return;
task_.reset();
domain_->slots_.release();
domain_ = nullptr;
domain_->release_admission();
domain_.reset();
}
std::shared_ptr<Render_Domain> Render_Domain::acquire(std::uint32_t gpu_index) {
auto& storage = registry();
std::lock_guard lock(storage.mutex);
if (const auto found = storage.domains.find(gpu_index); found != storage.domains.end())
return found->second;
auto& entry = storage.domains[gpu_index];
if (auto domain = entry.lock())
return domain;
auto domain = std::shared_ptr<Render_Domain>(new Render_Domain());
storage.domains.emplace(gpu_index, domain);
entry = domain;
return domain;
}
Render_Domain::Render_Domain() {
@@ -45,36 +47,82 @@ Render_Domain::Render_Domain() {
thread_ = std::thread([this] { run(); });
}
Render_Domain::~Render_Domain() {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
stopping_.store(true, std::memory_order_release);
slots_.acquire();
if (!tasks_.try_push(std::unique_ptr<Task>{}))
std::terminate();
if (thread_.joinable())
thread_.join();
}
void Render_Domain::update_peak(std::atomic_size_t& peak, std::size_t value) noexcept {
std::size_t current = peak.load(std::memory_order_relaxed);
while (current < value && !peak.compare_exchange_weak(current, value, std::memory_order_relaxed)) {}
}
void Render_Domain::acquire_admission() {
const auto started = std::chrono::steady_clock::now();
if (!slots_.try_acquire()) {
backpressure_count_.fetch_add(1, std::memory_order_relaxed);
slots_.acquire();
const auto waited = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - started).count();
if (waited > 0)
backpressure_wait_ns_.fetch_add(static_cast<std::uint64_t>(waited), std::memory_order_relaxed);
}
const std::size_t admitted = admitted_.fetch_add(1, std::memory_order_relaxed) + 1;
update_peak(peak_admitted_, admitted);
}
void Render_Domain::release_admission() noexcept {
admitted_.fetch_sub(1, std::memory_order_relaxed);
slots_.release();
}
Render_Domain::Prepared_Task Render_Domain::prepare(std::function<void()> function) {
if (!function)
throw std::invalid_argument("render domain task is empty");
if (stopping_.load(std::memory_order_acquire))
throw std::runtime_error("render domain is stopping");
acquire_admission();
try {
return Prepared_Task(shared_from_this(), std::make_unique<Task>(std::move(function)));
} catch (...) {
release_admission();
throw;
}
}
void Render_Domain::post(std::function<void()> function) {
post(prepare(std::move(function)));
}
void Render_Domain::post(Prepared_Task task) noexcept {
if (!task.task_ || task.domain_ != this)
std::terminate();
std::lock_guard lock(mutex_);
if (stopping_)
if (!task.task_ || task.domain_.get() != this || stopping_.load(std::memory_order_acquire))
std::terminate();
if (!tasks_.try_push(std::move(task.task_)))
std::terminate();
task.domain_ = nullptr;
const std::size_t queued = queued_.fetch_add(1, std::memory_order_relaxed) + 1;
update_peak(peak_queued_, queued);
task.domain_.reset();
}
Render_Domain::Statistics Render_Domain::statistics() const noexcept {
return {
static_cast<std::size_t>(default_capacity),
admitted_.load(std::memory_order_relaxed),
peak_admitted_.load(std::memory_order_relaxed),
queued_.load(std::memory_order_relaxed),
peak_queued_.load(std::memory_order_relaxed),
backpressure_count_.load(std::memory_order_relaxed),
backpressure_wait_ns_.load(std::memory_order_relaxed)
};
}
void Render_Domain::run() {
current_domain_ = this;
for (;;) {
std::unique_ptr<Task> task;
tasks_.pop(task);
slots_.release();
if (!task)
if (!task) {
slots_.release();
current_domain_ = nullptr;
return;
}
queued_.fetch_sub(1, std::memory_order_relaxed);
release_admission();
task->function();
}
}
+37 -18
View File
@@ -1,9 +1,11 @@
#pragma once
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <semaphore>
#include <stdexcept>
#include <thread>
@@ -11,12 +13,21 @@
#include <utility>
#include <oneapi/tbb/concurrent_queue.h>
namespace renderive::render_3d::detail {
class Render_Domain final {
class Render_Domain final : public std::enable_shared_from_this<Render_Domain> {
struct Task {
explicit Task(std::function<void()> value) : function(std::move(value)) {}
std::function<void()> function;
};
public:
struct Statistics {
std::size_t capacity{};
std::size_t admitted{};
std::size_t peak_admitted{};
std::size_t queued{};
std::size_t peak_queued{};
std::uint64_t backpressure_count{};
std::uint64_t backpressure_wait_ns{};
};
class Prepared_Task final {
public:
Prepared_Task() = default;
@@ -26,10 +37,10 @@ public:
Prepared_Task(Prepared_Task&& other) noexcept;
Prepared_Task& operator=(Prepared_Task&& other) noexcept;
private:
Prepared_Task(Render_Domain* domain, std::unique_ptr<Task> task) noexcept
: domain_(domain), task_(std::move(task)) {}
Prepared_Task(std::shared_ptr<Render_Domain> domain, std::unique_ptr<Task> task) noexcept
: domain_(std::move(domain)), task_(std::move(task)) {}
void release() noexcept;
Render_Domain* domain_{};
std::shared_ptr<Render_Domain> domain_;
std::unique_ptr<Task> task_;
friend class Render_Domain;
};
@@ -37,22 +48,20 @@ public:
~Render_Domain();
Render_Domain(const Render_Domain&) = delete;
Render_Domain& operator=(const Render_Domain&) = delete;
[[nodiscard]] Prepared_Task prepare(std::function<void()> function) {
if (!function)
throw std::invalid_argument("render domain task is empty");
slots_.acquire();
try {
return Prepared_Task(this, std::make_unique<Task>(std::move(function)));
} catch (...) {
slots_.release();
throw;
}
}
[[nodiscard]] Prepared_Task prepare(std::function<void()> function);
void post(std::function<void()> function);
void post(Prepared_Task task) noexcept;
template <class Function>
auto invoke(Function&& function) -> std::invoke_result_t<Function> {
using Result = std::invoke_result_t<Function>;
if (current_domain_ == this) {
if constexpr (std::is_void_v<Result>) {
std::invoke(std::forward<Function>(function));
return;
} else {
return std::invoke(std::forward<Function>(function));
}
}
auto task = std::make_shared<std::packaged_task<Result()>>(std::forward<Function>(function));
auto result = task->get_future();
post([task] { (*task)(); });
@@ -61,14 +70,24 @@ public:
else
return result.get();
}
[[nodiscard]] Statistics statistics() const noexcept;
private:
Render_Domain();
static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept;
void acquire_admission();
void release_admission() noexcept;
void run();
static constexpr std::ptrdiff_t default_capacity = 64;
inline static thread_local Render_Domain* current_domain_{};
std::counting_semaphore<default_capacity> slots_{default_capacity};
oneapi::tbb::concurrent_bounded_queue<std::unique_ptr<Task>> tasks_;
std::mutex mutex_;
bool stopping_{};
std::atomic_size_t admitted_{};
std::atomic_size_t peak_admitted_{};
std::atomic_size_t queued_{};
std::atomic_size_t peak_queued_{};
std::atomic_uint64_t backpressure_count_{};
std::atomic_uint64_t backpressure_wait_ns_{};
std::atomic_bool stopping_{};
std::thread thread_;
};
}
@@ -1,12 +1,12 @@
#include "render_3D/detail/Gpu_Completion_Service.h"
#include "render_3D/detail/Render_Domain.h"
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <thread>
#include <utility>
namespace renderive::render_3d::detail {
namespace {
static_assert(noexcept(std::declval<Gpu_Completion_Service::Reservation&>().watch(VK_NULL_HANDLE, VK_NULL_HANDLE)));
static_assert(noexcept(std::declval<Render_Domain&>().post(std::declval<Render_Domain::Prepared_Task>())));
TEST(GpuCompletionService, UsesOneProcessWideService) {
EXPECT_EQ(&Gpu_Completion_Service::instance(), &Gpu_Completion_Service::instance());
}
@@ -20,29 +20,18 @@ TEST(GpuCompletionService, AbandonedReservationDoesNotComplete) {
}
EXPECT_EQ(completion_count.load(std::memory_order_relaxed), 0);
}
TEST(RenderDomain, SharesDomainPerGpuIndex) {
auto first = Render_Domain::acquire(0);
auto second = Render_Domain::acquire(0);
auto other = Render_Domain::acquire(1);
EXPECT_EQ(first, second);
EXPECT_NE(first, other);
}
TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) {
auto domain = Render_Domain::acquire(0);
std::atomic<bool> executed{};
auto task = domain->prepare([&] {
executed.store(true, std::memory_order_release);
});
domain->post(std::move(task));
domain->invoke([] {});
EXPECT_TRUE(executed.load(std::memory_order_acquire));
}
TEST(RenderDomain, AbandonedPreparedTasksReleaseReservedCapacity) {
auto domain = Render_Domain::acquire(2);
for (std::size_t index = 0; index < 128; ++index) {
auto task = domain->prepare([] {});
TEST(GpuCompletionService, CanceledReservationWakesIdleService) {
auto& service = Gpu_Completion_Service::instance();
const auto baseline = service.statistics().in_flight;
{
auto reservation = service.prepare([](Gpu_Completion_Service::Result) {}, false);
EXPECT_EQ(service.statistics().in_flight, baseline + 1);
}
domain->invoke([] {});
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
while (service.statistics().in_flight != baseline &&
std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
EXPECT_EQ(service.statistics().in_flight, baseline);
}
}
}
+47
View File
@@ -0,0 +1,47 @@
#include "render_3D/detail/Render_Domain.h"
#include <gtest/gtest.h>
#include <atomic>
#include <utility>
namespace renderive::render_3d::detail {
namespace {
static_assert(noexcept(std::declval<Render_Domain&>().post(std::declval<Render_Domain::Prepared_Task>())));
TEST(RenderDomain, SharesDomainPerGpuIndex) {
auto first = Render_Domain::acquire(0x7ffffffcU);
auto second = Render_Domain::acquire(0x7ffffffcU);
auto other = Render_Domain::acquire(0x7ffffffbU);
EXPECT_EQ(first, second);
EXPECT_NE(first, other);
}
TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) {
auto domain = Render_Domain::acquire(0x7ffffffaU);
std::atomic<bool> executed{};
auto task = domain->prepare([&] {
executed.store(true, std::memory_order_release);
});
domain->post(std::move(task));
domain->invoke([] {});
EXPECT_TRUE(executed.load(std::memory_order_acquire));
}
TEST(RenderDomain, AbandonedPreparedTasksReleaseReservedCapacity) {
auto domain = Render_Domain::acquire(0x7ffffff9U);
for (std::size_t index = 0; index < 128; ++index) {
auto task = domain->prepare([] {});
}
domain->invoke([] {});
}
TEST(RenderDomain, NestedInvokeExecutesInlineOnTheAffinityThread) {
auto domain = Render_Domain::acquire(0x7ffffff8U);
const int value = domain->invoke([domain] {
return domain->invoke([] { return 42; });
});
EXPECT_EQ(value, 42);
}
TEST(RenderDomain, ReportsBoundedAdmissionStatistics) {
auto domain = Render_Domain::acquire(0x7ffffff7U);
const auto statistics = domain->statistics();
EXPECT_EQ(statistics.capacity, 64U);
EXPECT_TRUE(statistics.admitted <= statistics.capacity);
EXPECT_TRUE(statistics.queued <= statistics.capacity);
}
}
}
+5 -5
View File
@@ -348,7 +348,7 @@ inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene)
using namespace gallery_capture_detail;
const auto controller = scene.capture_state();
Json sessions = Json::array();
std::set<Render_Plan_Version> referenced_plans;
std::map<Render_Plan_Version, std::shared_ptr<const Render_Plan>> referenced_plans;
for (Capture_Session_Id id : scene.capture_sessions()) {
const auto session = scene.capture_session(id);
if (!session)
@@ -356,7 +356,7 @@ inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene)
Json frames = Json::array();
for (const auto& frame : session->frames) {
frames.push_back(frame_json(frame));
referenced_plans.insert(frame.snapshot->render_plan_version);
referenced_plans[frame.snapshot->render_plan_version] = frame.render_plan;
}
Json node_statistics = Json::array();
for (const auto& node : scene.node_statistics(id))
@@ -376,11 +376,11 @@ inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene)
});
}
if (const auto current = scene.render_plan_snapshot())
referenced_plans.insert(current->version);
referenced_plans[current->version] = current;
Json plans = Json::array();
std::shared_ptr<const Render_Plan> previous;
for (Render_Plan_Version version : referenced_plans) {
const auto plan = scene.find_render_plan(version);
for (const auto& [version, captured_plan] : referenced_plans) {
auto plan = captured_plan ? captured_plan : scene.find_render_plan(version);
if (!plan)
continue;
Json value = plan_json(scene, *plan);
+136 -80
View File
@@ -3,6 +3,7 @@
#include "common/Gallery_Scene_Interface.h"
#include "render_2D/Gallery_Scene2D.h"
#include "render_3D/Gallery_Scene3D.h"
#include <renderive/scheduling/Scheduler.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <atomic>
@@ -10,12 +11,13 @@
#include <cmath>
#include <condition_variable>
#include <cstdint>
#include <limits>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <type_traits>
#include <utility>
namespace renderive::web {
@@ -123,26 +125,84 @@ std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene(std::uint64_t sessio
return make_gallery_scene_2d(session_id, std::move(case_id), mode, automatic_low_latency);
}
}
struct Gallery_Plot_Session::Impl {
struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Session::Impl> {
using Clock = std::chrono::steady_clock;
class Automatic_Render_Scheduler final {
public:
static Automatic_Render_Scheduler& instance() {
static Automatic_Render_Scheduler scheduler;
return scheduler;
}
void arm(std::uint64_t session_id, std::weak_ptr<Impl> session, Clock::time_point deadline) {
std::lock_guard lock(mutex_);
if (const auto current = entries_.find(session_id); current != entries_.end()) {
schedule_.erase(current->second);
entries_.erase(current);
}
const auto iterator = schedule_.emplace(deadline, Entry{session_id, std::move(session)});
entries_.emplace(session_id, iterator);
condition_.notify_one();
}
void disarm(std::uint64_t session_id) noexcept {
std::lock_guard lock(mutex_);
const auto current = entries_.find(session_id);
if (current == entries_.end())
return;
schedule_.erase(current->second);
entries_.erase(current);
condition_.notify_one();
}
private:
struct Entry {
std::uint64_t session_id{};
std::weak_ptr<Impl> session;
};
using Schedule = std::multimap<Clock::time_point, Entry>;
Automatic_Render_Scheduler() : thread_([this](std::stop_token stop) { run(stop); }) {}
~Automatic_Render_Scheduler() {
thread_.request_stop();
condition_.notify_all();
}
void run(std::stop_token stop) noexcept {
std::unique_lock lock(mutex_);
while (!stop.stop_requested()) {
if (schedule_.empty()) {
condition_.wait(lock, [this, &stop] {
return stop.stop_requested() || !schedule_.empty();
});
continue;
}
const auto deadline = schedule_.begin()->first;
if (condition_.wait_until(lock, deadline) != std::cv_status::timeout)
continue;
if (schedule_.empty() || schedule_.begin()->first > Clock::now())
continue;
auto iterator = schedule_.begin();
Entry entry = std::move(iterator->second);
entries_.erase(entry.session_id);
schedule_.erase(iterator);
lock.unlock();
if (auto session = entry.session.lock())
session->automatic_render_due();
lock.lock();
}
}
std::mutex mutex_;
std::condition_variable condition_;
Schedule schedule_;
std::unordered_map<std::uint64_t, Schedule::iterator> entries_;
std::jthread thread_;
};
explicit Impl(bool enable_automatic_low_latency)
: automatic_low_latency(enable_automatic_low_latency),
session_id(next_session_id()) {}
: automatic_low_latency(enable_automatic_low_latency), session_id(next_session_id()) {}
~Impl() {
render_worker.request_stop();
scheduler_condition.notify_all();
if (automatic_low_latency)
Automatic_Render_Scheduler::instance().disarm(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);
}
void ensure_render_worker() {
if (!automatic_low_latency || render_worker.joinable())
return;
render_worker = std::jthread(
[this](std::stop_token stop) {
run_automatic_renderer(stop);
});
}
bool update_client_metrics(std::string_view message) {
if (!scene)
return false;
@@ -204,57 +264,59 @@ struct Gallery_Plot_Session::Impl {
return true;
}
[[nodiscard]] std::unique_lock<std::mutex> acquire_foreground_lock() {
foreground_waiters.fetch_add(1, std::memory_order_release);
scheduler_condition.notify_all();
std::unique_lock lock(mutex);
foreground_waiters.fetch_sub(1, std::memory_order_release);
scheduler_condition.notify_all();
return lock;
return std::unique_lock<std::mutex>(mutex);
}
void run_automatic_renderer(std::stop_token stop) {
using Clock = std::chrono::steady_clock;
std::unique_lock lock(mutex);
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());
void arm_automatic_render_locked() noexcept {
if (!automatic_low_latency)
return;
auto& scheduler = Automatic_Render_Scheduler::instance();
if (!scene || !scene->can_render_automatically() || render_task_pending) {
scheduler.disarm(session_id);
return;
}
try {
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();
scheduler.arm(session_id, weak_from_this(), deadline);
} catch (...) {
scheduler.disarm(session_id);
}
}
void automatic_render_due() {
std::uint64_t generation{};
{
std::lock_guard lock(mutex);
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 {
renderive::scheduling::enqueue_task([self = std::move(self), generation] {
self->run_automatic_render(generation);
});
if (stop.stop_requested())
break;
if (active_generation != scene_generation) {
active_generation = scene_generation;
last_render_started.reset();
} catch (...) {
std::lock_guard lock(mutex);
render_task_pending = false;
arm_automatic_render_locked();
}
}
void run_automatic_render(std::uint64_t generation) noexcept {
const auto started = Clock::now();
try {
std::lock_guard lock(mutex);
if (generation == scene_generation && scene && scene->can_render_automatically()) {
(void)scene->render_latest_frame();
last_render_started = started;
}
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;
});
continue;
}
const auto revision = scheduler_revision;
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();
});
continue;
}
lock.unlock();
while (!stop.stop_requested() && Clock::now() < deadline)
std::this_thread::yield();
lock.lock();
continue;
}
const auto started = Clock::now();
(void)scene->render_latest_frame();
last_render_started = started;
render_task_pending = false;
arm_automatic_render_locked();
} catch (...) {
std::lock_guard lock(mutex);
render_task_pending = false;
arm_automatic_render_locked();
}
}
static bool affects_render_schedule(const Web_Event& event) {
@@ -278,13 +340,11 @@ struct Gallery_Plot_Session::Impl {
}
std::unique_ptr<Gallery_Scene_Interface> scene;
std::mutex mutex;
std::condition_variable scheduler_condition;
std::uint64_t scheduler_revision{};
std::optional<Clock::time_point> last_render_started;
std::uint64_t scene_generation{};
bool automatic_low_latency{};
bool render_task_pending{};
std::uint64_t session_id{};
std::atomic<std::uint32_t> foreground_waiters{};
std::jthread render_worker;
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()};
@@ -297,8 +357,8 @@ struct Gallery_Plot_Session::Impl {
};
scene = make_gallery_scene(session_id, open->case_id, open->frame_mode, automatic_low_latency);
++scene_generation;
if (scene->frame_mode() == Gallery_Frame_Mode::Low_Latency)
ensure_render_worker();
last_render_started.reset();
arm_automatic_render_locked();
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(),
@@ -313,10 +373,8 @@ struct Gallery_Plot_Session::Impl {
Gallery_Protocol::error_json("请先发送 gallery_open")
};
if (request.kind == Gallery_Request_Kind::Observe) {
if (update_client_metrics(request.message)) {
++scheduler_revision;
scheduler_condition.notify_all();
}
if (update_client_metrics(request.message))
arm_automatic_render_locked();
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::observer_json(
@@ -324,10 +382,8 @@ struct Gallery_Plot_Session::Impl {
};
}
if (request.kind == Gallery_Request_Kind::Refresh) {
if (update_client_metrics(request.message)) {
++scheduler_revision;
scheduler_condition.notify_all();
}
if (update_client_metrics(request.message))
arm_automatic_render_locked();
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::case_json_from_controls(
@@ -382,6 +438,8 @@ struct Gallery_Plot_Session::Impl {
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();
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::case_json_from_controls(id, scene->controls().dump(),
@@ -455,16 +513,14 @@ struct Gallery_Plot_Session::Impl {
},
event);
if (reschedule)
++scheduler_revision;
arm_automatic_render_locked();
}
if (reschedule)
scheduler_condition.notify_all();
return response;
}
};
Gallery_Plot_Session::Gallery_Plot_Session() : Gallery_Plot_Session(false) {}
Gallery_Plot_Session::Gallery_Plot_Session(bool automatic_low_latency)
: impl_(std::make_unique<Impl>(automatic_low_latency)) {}
: impl_(std::make_shared<Impl>(automatic_low_latency)) {}
Gallery_Plot_Session::~Gallery_Plot_Session() = default;
std::optional<Web_Response> Gallery_Plot_Session::handle(const Web_Event& event) {
return impl_->handle(event);
+1 -1
View File
@@ -19,7 +19,7 @@ public:
private:
struct Impl;
std::unique_ptr<Impl> impl_;
std::shared_ptr<Impl> impl_;
};
} // namespace renderive::web
@@ -630,6 +630,7 @@ public:
render_performance.automatic_low_latency_scheduler =
automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency;
const Gallery_Client_Performance client_performance = client_performance_;
const auto scene_execution = plot_.render_scene().execution_statistics();
nlohmann::json observer_data =
adminive::to_frontend_json<nlohmann::json>(observer);
nlohmann::json consumer_feedback_data =
@@ -656,6 +657,13 @@ public:
{"client_performance", std::move(client_performance_data)},
{"scheduler", adminive::to_frontend_json<nlohmann::json>(
renderive::scheduling::scheduler_statistics())},
{"queue_pressure",
{{"scene_execution",
{{"capacity", scene_execution.capacity},
{"queued", scene_execution.queued},
{"peak_queued", scene_execution.peak_queued},
{"backpressure_count", scene_execution.backpressure_count},
{"backpressure_wait_ns", scene_execution.backpressure_wait_ns}}}}},
{"renderable_observers", controls_.observers(plot_.render_scene())},
{"performance_capture", gallery_performance_capture_json(plot_.render_scene())},
{"last_action_result", last_action_result_}
@@ -440,6 +440,8 @@ public:
[[nodiscard]] std::string telemetry_json() const override {
const auto status = scene_.frame_status();
const auto now = std::chrono::steady_clock::now();
const auto scene_execution = scene_.render_scene().execution_statistics();
const auto runtime = scene_.runtime_statistics();
return nlohmann::json{
{"kernel_observer",
{{"mode", mode_id(frame_mode_)},
@@ -464,6 +466,29 @@ public:
client_performance_.websocket_buffered_bytes}}},
{"scheduler", adminive::to_frontend_json<nlohmann::json>(
renderive::scheduling::scheduler_statistics())},
{"queue_pressure",
{{"scene_execution",
{{"capacity", scene_execution.capacity},
{"queued", scene_execution.queued},
{"peak_queued", scene_execution.peak_queued},
{"backpressure_count", scene_execution.backpressure_count},
{"backpressure_wait_ns", scene_execution.backpressure_wait_ns}}},
{"render_domain",
{{"capacity", runtime.render_domain.capacity},
{"active", runtime.render_domain.active},
{"peak_active", runtime.render_domain.peak_active},
{"queued", runtime.render_domain.queued},
{"peak_queued", runtime.render_domain.peak_queued},
{"backpressure_count", runtime.render_domain.backpressure_count},
{"backpressure_wait_ns", runtime.render_domain.backpressure_wait_ns}}},
{"gpu_completion",
{{"capacity", runtime.gpu_completion.capacity},
{"active", runtime.gpu_completion.active},
{"peak_active", runtime.gpu_completion.peak_active},
{"queued", runtime.gpu_completion.queued},
{"peak_queued", runtime.gpu_completion.peak_queued},
{"backpressure_count", runtime.gpu_completion.backpressure_count},
{"backpressure_wait_ns", runtime.gpu_completion.backpressure_wait_ns}}}}},
{"session_id", session_id_},
{"performance_capture",
gallery_performance_capture_json(scene_.render_scene())},
@@ -1,2 +1,2 @@
import {Stack,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {Json_Viewer} from "../common/json_viewer";
export function Performance_Panel({telemetry}:{telemetry:Gallery_Telemetry}) {return <Stack spacing={1}><Typography variant="h6"></Typography><Json_Viewer value={{performance:telemetry.performance,scheduler:telemetry.scheduler,client_performance:telemetry.client_performance,consumer_feedback:telemetry.consumer_feedback}}/></Stack>;}
export function Performance_Panel({telemetry}:{telemetry:Gallery_Telemetry}) {return <Stack spacing={1}><Typography variant="h6"></Typography><Json_Viewer value={{performance:telemetry.performance,scheduler:telemetry.scheduler,queue_pressure:telemetry.queue_pressure,client_performance:telemetry.client_performance,consumer_feedback:telemetry.consumer_feedback}}/></Stack>;}
+1 -1
View File
@@ -26,7 +26,7 @@ export interface Gallery_Performance extends Record<string, Json_Value> {}
export interface Gallery_Kernel_Observer extends Record<string, Json_Value> {}
export interface Gallery_Scheduler_Statistics {concurrency: number; active_workers: number; peak_workers: number; active_external_threads: number; peak_external_threads: number; worker_entry_count: number; worker_exit_count: number; external_entry_count: number; external_exit_count: number;}
export interface Gallery_Renderable_Observer extends Gallery_Resource {}
export interface Gallery_Telemetry {[key: string]: unknown; scheduler?: Gallery_Scheduler_Statistics; renderable_observers?: Gallery_Renderable_Observer[]; performance_capture?: Gallery_Performance_Capture;}
export interface Gallery_Telemetry {[key: string]: unknown; scheduler?: Gallery_Scheduler_Statistics; queue_pressure?: Record<string, unknown>; renderable_observers?: Gallery_Renderable_Observer[]; performance_capture?: Gallery_Performance_Capture;}
export interface Gallery_Render_Node {node_id: number; id?: string | number; name: string; label?: string; owner: string; kind: string;}
export interface Gallery_Render_Edge {from: number | string; to: number | string;}