diff --git a/.gitignore b/.gitignore index 757fee3..97ec3b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/.idea \ No newline at end of file +/.idea + +/cmake-build*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f4fd5c..df54a8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,20 +1,34 @@ -if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_LIST_DIR}) - cmake_minimum_required(VERSION 3.16) - # cmake_policy(SET CMP0141 NEW) - set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") - project(Renderive) - set(Renderive_VERSION 1.0.0) - set(CMAKE_CXX_STANDARD 20) - set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - include(third_party/build_infra/start.cmake) - include(third_party/CPP_Core/main.cmake) - include(${CMAKE_SOURCE_DIR}/main.cmake) - include(third_party/build_infra/end.cmake) -else () +cmake_minimum_required(VERSION 3.20) +project(Renderive VERSION 1.0.0 LANGUAGES CXX) +set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +option(RENDERIVE_BUILD_2D "Build 2D renderer" ON) +option(RENDERIVE_BUILD_QT "Build Qt integration" ON) +option(RENDERIVE_BUILD_WEB "Build Web integration" ON) +option(RENDERIVE_BUILD_TESTS "Build tests" ON) +option(RENDERIVE_BUILD_3D "Build 3D renderer" OFF) +if (RENDERIVE_BUILD_QT AND NOT RENDERIVE_BUILD_2D) + message(FATAL_ERROR "RENDERIVE_BUILD_QT requires RENDERIVE_BUILD_2D") endif () - - - - - - +if (RENDERIVE_BUILD_WEB AND NOT RENDERIVE_BUILD_2D) + message(FATAL_ERROR "RENDERIVE_BUILD_WEB requires RENDERIVE_BUILD_2D") +endif () +if (RENDERIVE_BUILD_3D) + message(FATAL_ERROR "RENDERIVE_BUILD_3D is reserved; the 3D renderer is not implemented") +endif () +include("${CMAKE_CURRENT_LIST_DIR}/third_party/build_infra/start.cmake") +if (RENDERIVE_BUILD_TESTS) + enable_testing() +endif () +add_subdirectory(Kernel) +if (RENDERIVE_BUILD_2D) + add_subdirectory(render_2D) +endif () +if (RENDERIVE_BUILD_QT) + add_subdirectory(Qt) +endif () +if (RENDERIVE_BUILD_WEB) + add_subdirectory(web_server) +endif () +include("${CMAKE_CURRENT_LIST_DIR}/cmake/RenderivePackage.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/third_party/build_infra/end.cmake") diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index 2d9ae9d..ed08db4 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -1,86 +1,55 @@ -cmake_minimum_required(VERSION 3.20) -project(Renderive_Scene_Taskflow LANGUAGES CXX) -set(RENDERIVE_TASKFLOW_ROOT "" CACHE PATH "Taskflow 4.1.0 root") -set(_renderive_builtin_taskflow_dir "${CMAKE_CURRENT_LIST_DIR}/src/renderive/compat") -find_path(RENDERIVE_TASKFLOW_INCLUDE_DIR - NAMES taskflow/taskflow.hpp - PATHS - "${RENDERIVE_TASKFLOW_ROOT}" - "${RENDERIVE_TASKFLOW_ROOT}/include" - "${CMAKE_CURRENT_LIST_DIR}/third_party/taskflow" - "${CMAKE_CURRENT_LIST_DIR}/third_party/taskflow/include" -) -if (NOT RENDERIVE_TASKFLOW_INCLUDE_DIR) - set(RENDERIVE_TASKFLOW_INCLUDE_DIR "${_renderive_builtin_taskflow_dir}") -endif () -get_filename_component(_renderive_taskflow_include_dir_real "${RENDERIVE_TASKFLOW_INCLUDE_DIR}" REALPATH) -get_filename_component(_renderive_builtin_taskflow_dir_real "${_renderive_builtin_taskflow_dir}" REALPATH) -set(RENDERIVE_TASKFLOW_MOCK OFF) -if (_renderive_taskflow_include_dir_real STREQUAL _renderive_builtin_taskflow_dir_real) - set(RENDERIVE_TASKFLOW_MOCK ON) - message(STATUS "Using the built-in dependency-graph executor") -endif () -add_library(renderive_scene STATIC - "${CMAKE_CURRENT_LIST_DIR}/src/renderive/renderable/Renderable_Task_Graph.cpp" - "${CMAKE_CURRENT_LIST_DIR}/src/renderive/renderable/base/Renderable_Base.cpp" - "${CMAKE_CURRENT_LIST_DIR}/src/renderive/scene/base/Scene_Base.cpp" -) -target_include_directories(renderive_scene - PUBLIC - "$" - PRIVATE "${RENDERIVE_TASKFLOW_INCLUDE_DIR}" -) -target_compile_features(renderive_scene PUBLIC cxx_std_20) -if (RENDERIVE_TASKFLOW_MOCK) - target_compile_definitions(renderive_scene PRIVATE RENDERIVE_TASKFLOW_MOCK=1) -endif () +find_package(Taskflow CONFIG REQUIRED) find_package(Threads REQUIRED) -target_link_libraries(renderive_scene PRIVATE Threads::Threads) -include(CTest) -if (BUILD_TESTING) - set(renderive_test_sources - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/Concepts_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/node/Multiway_Node_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/observer/Observer_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/observer/time_source/Steady_Time_Source_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Concepts_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Property_Builder_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/base/property/Validated_Value_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/concept/Concepts_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/real_time_data/base/Real_Time_Data_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/real_time_data/Real_Time_Data_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/base/Renderable_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/color/Color_Cache_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/concept/Renderable_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/base/Scene_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene_Memory_Resource_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/concept/Scene_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene2D_Context_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene2D_Render_Order_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene3D_Context_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/scene/Scene_State_Observer_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/base/State_Strategy_Base_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Concepts_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Double_State_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/state/Triple_State_Strategy_Test.cpp" - "${CMAKE_CURRENT_LIST_DIR}/tests/renderive/threading/Threading_Contract_Test.cpp" +add_library(Renderive_Kernel STATIC) +target_sources(Renderive_Kernel PRIVATE + src/renderive/renderable/Renderable_Task_Graph.cpp + src/renderive/renderable/base/Renderable_Base.cpp + src/renderive/scene/base/Scene_Base.cpp +) +target_include_directories(Renderive_Kernel PUBLIC + "$" +) +target_compile_features(Renderive_Kernel PUBLIC cxx_std_20) +target_link_libraries(Renderive_Kernel PRIVATE Taskflow::Taskflow Threads::Threads) +if(MSVC) + target_compile_options(Renderive_Kernel PRIVATE /utf-8) +endif() +if(RENDERIVE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) + add_executable(Renderive_Kernel_Tests) + target_sources(Renderive_Kernel_Tests PRIVATE + tests/renderive/base/Concepts_Test.cpp + tests/renderive/base/node/Multiway_Node_Test.cpp + tests/renderive/base/observer/Observer_Test.cpp + tests/renderive/base/observer/time_source/Steady_Time_Source_Test.cpp + tests/renderive/base/property/Concepts_Test.cpp + tests/renderive/base/property/Property_Builder_Test.cpp + tests/renderive/base/property/Validated_Value_Test.cpp + tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp + tests/renderive/frame_control/concept/Concepts_Test.cpp + tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp + tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp + tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp + tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp + tests/renderive/real_time_data/base/Real_Time_Data_Base_Test.cpp + tests/renderive/real_time_data/Real_Time_Data_Test.cpp + tests/renderive/renderable/base/Renderable_Base_Test.cpp + tests/renderive/renderable/color/Color_Cache_Test.cpp + tests/renderive/renderable/concept/Renderable_Test.cpp + tests/renderive/renderable/Renderable_Task_Graph_Test.cpp + tests/renderive/scene/base/Scene_Base_Test.cpp + tests/renderive/scene/concept/Scene_Test.cpp + tests/renderive/scene/Scene_Memory_Resource_Test.cpp + tests/renderive/scene/Scene_State_Observer_Test.cpp + tests/renderive/scene/Scene2D_Context_Test.cpp + tests/renderive/scene/Scene2D_Render_Order_Test.cpp + tests/renderive/scene/Scene3D_Context_Test.cpp + tests/renderive/state/base/State_Strategy_Base_Test.cpp + tests/renderive/state/Concepts_Test.cpp + tests/renderive/state/Double_State_Strategy_Test.cpp + tests/renderive/state/Triple_State_Strategy_Test.cpp + tests/renderive/threading/Threading_Contract_Test.cpp ) - find_package(GTest CONFIG QUIET) - if (NOT GTest_FOUND) - add_library(renderive_gtest_compat INTERFACE) - target_include_directories(renderive_gtest_compat INTERFACE "${CMAKE_CURRENT_LIST_DIR}/tests/compat") - add_library(GTest::gtest ALIAS renderive_gtest_compat) - message(STATUS "GTest not found; using the test-only compatibility runner") - endif () - add_executable(renderive_scene_tests "${CMAKE_CURRENT_LIST_DIR}/tests/main.cpp" ${renderive_test_sources}) - if (RENDERIVE_TASKFLOW_MOCK) - target_compile_definitions(renderive_scene_tests PRIVATE RENDERIVE_TASKFLOW_MOCK=1) - endif () - target_link_libraries(renderive_scene_tests PRIVATE renderive_scene GTest::gtest) - add_test(NAME renderive_scene_tests COMMAND renderive_scene_tests) -endif () + target_link_libraries(Renderive_Kernel_Tests PRIVATE Renderive_Kernel GTest::gtest_main) + add_test(NAME Renderive_Kernel_Tests COMMAND Renderive_Kernel_Tests) +endif() diff --git a/Kernel/readme.md b/Kernel/readme.md index 9b9a82d..13eec04 100644 --- a/Kernel/readme.md +++ b/Kernel/readme.md @@ -78,7 +78,7 @@ void build_task_graph(Renderable_Task_Graph& graph) override { 4. 将所有 module task 连接到最终颜色缓存生成任务。 5. 交给进程内共享的长期 `tf::Executor` 执行并等待完成。 -Taskflow 只在 `Scene_Base.cpp` 中包含。`Scene_Base` 通过内部执行上下文访问进程内共享的 `tf::Executor`,Executor 跨 Scene、跨帧复用。`renderive_scene` 对 Taskflow include 路径使用 `PRIVATE`,外部头文件不会受到 Taskflow 宏、类型或包含链污染。 +Taskflow 只在 `Scene_Base.cpp` 中包含。`Scene_Base` 通过内部执行上下文访问进程内共享的 `tf::Executor`,Executor 跨 Scene、跨帧复用。`Renderive_Kernel` 对 Taskflow 使用 `PRIVATE` 链接,外部头文件不会受到 Taskflow 宏、类型或包含链污染。 ## 显示树与依赖树 @@ -86,7 +86,7 @@ Taskflow 只在 `Scene_Base.cpp` 中包含。`Scene_Base` 通过内部执行上 ## 测试入口 -`BUILD_TESTING=ON` 时构建 `renderive_scene_tests` 并通过 CTest 运行;`BUILD_TESTING=OFF` 时只构建 `renderive_scene` 库,不再编译 `tests/main.cpp` 或任何测试源文件。 +`RENDERIVE_BUILD_TESTS=ON` 时构建 `Renderive_Kernel_Tests` 并通过 CTest 运行;`RENDERIVE_BUILD_TESTS=OFF` 时只构建 `Renderive_Kernel` 库,不编译任何测试源文件。 ## 线程模型 @@ -94,7 +94,7 @@ Scene、Frame Strategy、Real-Time-Data、State Strategy、Renderable task graph ## Taskflow 查找 -CMake 通过 `RENDERIVE_TASKFLOW_ROOT` 或系统 include 路径查找 `taskflow/taskflow.hpp`。找到 Taskflow 4.1.0 时使用真实执行器;找不到时使用 `src/renderive/compat/taskflow/taskflow.hpp` 的内置顺序依赖图执行器,使工程仍可完成 CMake 配置、编译并运行依赖图测试。兼容执行器只实现 Renderive 当前使用的 Taskflow 子集,用于验证内部 DAG、Renderable 外部依赖和 module 完成关系是否被正确转换,不模拟并行调度能力。真实 Taskflow 下额外运行并行执行测试。 +`Kernel/CMakeLists.txt` 通过 `find_package(Taskflow CONFIG REQUIRED)` 查找 Taskflow。Kernel 不提供替代执行器,配置时必须提供真实 Taskflow 包。 ## Flow 并发队列 diff --git a/Kernel/src/renderive/compat/taskflow/taskflow.hpp b/Kernel/src/renderive/compat/taskflow/taskflow.hpp deleted file mode 100644 index 8890ad8..0000000 --- a/Kernel/src/renderive/compat/taskflow/taskflow.hpp +++ /dev/null @@ -1,129 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#define TF_VERSION 400100 -namespace tf { -class Taskflow; -class Task { -public: - Task() = default; - Task& name(std::string name); - void precede(Task task) const; -private: - friend class Taskflow; - Task(Taskflow& owner, std::size_t index) noexcept : owner_(&owner), index_(index) {} - Taskflow* owner_{}; - std::size_t index_{}; -}; -class Taskflow { -public: - Taskflow() = default; - Taskflow(const Taskflow&) = delete; - Taskflow& operator=(const Taskflow&) = delete; - Taskflow(Taskflow&&) = default; - Taskflow& operator=(Taskflow&&) = default; - template - Task emplace(Work&& work) { - nodes_.push_back({std::function(std::forward(work)), nullptr, {}, {}}); - return Task(*this, nodes_.size() - 1); - } - Task composed_of(Taskflow& module) { - nodes_.push_back({{}, &module, {}, {}}); - return Task(*this, nodes_.size() - 1); - } -private: - friend class Task; - friend class Executor; - struct Node { - std::function work; - Taskflow* module{}; - std::string name; - std::vector successors; - }; - void precede(std::size_t from, std::size_t to) { - if (from >= nodes_.size() || to >= nodes_.size()) { - throw std::out_of_range("taskflow compatibility task index"); - } - nodes_[from].successors.push_back(to); - } - void set_name(std::size_t index, std::string name) { - nodes_.at(index).name = std::move(name); - } - void run() { - std::vector indegrees(nodes_.size()); - for (const Node& node : nodes_) { - for (std::size_t successor : node.successors) { - if (successor >= nodes_.size()) { - throw std::out_of_range("taskflow compatibility successor index"); - } - ++indegrees[successor]; - } - } - std::deque ready; - for (std::size_t index = 0; index < indegrees.size(); ++index) { - if (indegrees[index] == 0) { - ready.push_back(index); - } - } - std::size_t completed{}; - while (!ready.empty()) { - const std::size_t index = ready.front(); - ready.pop_front(); - Node& node = nodes_[index]; - if (node.module) { - node.module->run(); - } else if (node.work) { - node.work(); - } - ++completed; - for (std::size_t successor : node.successors) { - if (--indegrees[successor] == 0) { - ready.push_back(successor); - } - } - } - if (completed != nodes_.size()) { - throw std::runtime_error("taskflow compatibility graph contains a cycle"); - } - } - std::vector nodes_; -}; -inline Task& Task::name(std::string name) { - owner_->set_name(index_, std::move(name)); - return *this; -} -inline void Task::precede(Task task) const { - if (!owner_ || owner_ != task.owner_) { - throw std::invalid_argument("taskflow compatibility tasks belong to different graphs"); - } - owner_->precede(index_, task.index_); -} -class Future { -public: - explicit Future(std::exception_ptr exception) noexcept : exception_(std::move(exception)) {} - void get() { - if (exception_) { - std::rethrow_exception(exception_); - } - } -private: - std::exception_ptr exception_; -}; -class Executor { -public: - Future run(Taskflow& graph) { - try { - graph.run(); - return Future(nullptr); - } catch (...) { - return Future(std::current_exception()); - } - } -}; -} diff --git a/Kernel/tests/compat/gtest/gtest.h b/Kernel/tests/compat/gtest/gtest.h deleted file mode 100644 index 70e15fa..0000000 --- a/Kernel/tests/compat/gtest/gtest.h +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -namespace testing { -struct Test_Info { - std::string suite; - std::string name; - std::function function; -}; -inline std::vector& registry() { - static std::vector tests; - return tests; -} -inline int& current_failures() { - static thread_local int failures{}; - return failures; -} -class Fatal_Failure {}; -inline bool register_test(const char* suite, const char* name, std::function function) { - registry().push_back({suite, name, std::move(function)}); - return true; -} -inline void fail(const char* file, int line, const char* expression, bool fatal) { - ++current_failures(); - std::cerr << file << ':' << line << ": failure: " << expression << '\n'; - if (fatal) { - throw Fatal_Failure{}; - } -} -inline void InitGoogleTest(int*, char**) {} -inline int Run_All_Tests() { - int failed_tests{}; - for (const Test_Info& test : registry()) { - if (test.suite.starts_with("DISABLED_") || test.name.starts_with("DISABLED_")) { - std::cout << "[ DISABLED ] " << test.suite << '.' << test.name << '\n'; - continue; - } - current_failures() = 0; - try { - test.function(); - } catch (const Fatal_Failure&) { - } catch (const std::exception& exception) { - fail(__FILE__, __LINE__, exception.what(), false); - } catch (...) { - fail(__FILE__, __LINE__, "unknown exception", false); - } - if (current_failures() != 0) { - ++failed_tests; - std::cerr << "[ FAILED ] " << test.suite << '.' << test.name << '\n'; - } else { - std::cout << "[ OK ] " << test.suite << '.' << test.name << '\n'; - } - } - std::cout << registry().size() << " tests, " << failed_tests << " failed\n"; - return failed_tests == 0 ? 0 : 1; -} -} -#define TEST(suite, name) static void suite##_##name##_test(); static const bool suite##_##name##_registered = ::testing::register_test(#suite, #name, suite##_##name##_test); static void suite##_##name##_test() -#define EXPECT_TRUE(value) do { if (!(value)) ::testing::fail(__FILE__, __LINE__, "EXPECT_TRUE(" #value ")", false); } while (false) -#define EXPECT_FALSE(value) do { if ((value)) ::testing::fail(__FILE__, __LINE__, "EXPECT_FALSE(" #value ")", false); } while (false) -#define ASSERT_TRUE(value) do { if (!(value)) ::testing::fail(__FILE__, __LINE__, "ASSERT_TRUE(" #value ")", true); } while (false) -#define EXPECT_EQ(left, right) do { if (!((left) == (right))) ::testing::fail(__FILE__, __LINE__, "EXPECT_EQ(" #left ", " #right ")", false); } while (false) -#define EXPECT_NE(left, right) do { if (!((left) != (right))) ::testing::fail(__FILE__, __LINE__, "EXPECT_NE(" #left ", " #right ")", false); } while (false) -#define EXPECT_GT(left, right) do { if (!((left) > (right))) ::testing::fail(__FILE__, __LINE__, "EXPECT_GT(" #left ", " #right ")", false); } while (false) -#define EXPECT_GE(left, right) do { if (!((left) >= (right))) ::testing::fail(__FILE__, __LINE__, "EXPECT_GE(" #left ", " #right ")", false); } while (false) -#define EXPECT_DOUBLE_EQ(left, right) do { double gtest_left = static_cast(left); double gtest_right = static_cast(right); if (!(std::fabs(gtest_left - gtest_right) <= 1e-12 || (std::isnan(gtest_left) && std::isnan(gtest_right)))) ::testing::fail(__FILE__, __LINE__, "EXPECT_DOUBLE_EQ(" #left ", " #right ")", false); } while (false) -#define EXPECT_THROW(statement, exception_type) do { bool gtest_thrown = false; try { statement; } catch (const exception_type&) { gtest_thrown = true; } catch (...) {} if (!gtest_thrown) ::testing::fail(__FILE__, __LINE__, "EXPECT_THROW(" #statement ", " #exception_type ")", false); } while (false) -#define EXPECT_NO_THROW(statement) do { bool gtest_thrown = false; try { statement; } catch (...) { gtest_thrown = true; } if (gtest_thrown) ::testing::fail(__FILE__, __LINE__, "EXPECT_NO_THROW(" #statement ")", false); } while (false) -#define SUCCEED() do {} while (false) -#define RUN_ALL_TESTS() ::testing::Run_All_Tests() diff --git a/Kernel/tests/main.cpp b/Kernel/tests/main.cpp deleted file mode 100644 index 1edad91..0000000 --- a/Kernel/tests/main.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include -int main(int argc, char** argv) { - testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/Qt/CMakeLists.txt b/Qt/CMakeLists.txt new file mode 100644 index 0000000..bc68a0a --- /dev/null +++ b/Qt/CMakeLists.txt @@ -0,0 +1,37 @@ +find_package(Qt5 CONFIG REQUIRED COMPONENTS Core Gui Widgets) +add_library(Renderive_Qt STATIC) +target_sources(Renderive_Qt PRIVATE + bridge/Qt_Event_Adapter.cpp + bridge/Qt_Image_Adapter.cpp + bridge/Qt_Presentation_Sink.cpp + plot/Explicit_Plot.cpp + plot/Latency_Eager_Plot.cpp + plot/Plot.cpp +) +set_target_properties(Renderive_Qt PROPERTIES AUTOMOC ON) +target_include_directories(Renderive_Qt PUBLIC + "$" +) +target_compile_features(Renderive_Qt PUBLIC cxx_std_20) +target_link_libraries(Renderive_Qt PUBLIC + Renderive_render_2D + Qt5::Core + Qt5::Gui + Qt5::Widgets +) +if(MSVC) + target_compile_options(Renderive_Qt PRIVATE /utf-8) +endif() +if(RENDERIVE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) + add_executable(Renderive_Qt_Tests tests/Qt_Bridge_Tests.cpp) + target_link_libraries(Renderive_Qt_Tests PRIVATE Renderive_Qt GTest::gtest) + get_filename_component(renderive_qt_runtime_root "${Qt5_DIR}/../../.." ABSOLUTE) + add_test(NAME Renderive_Qt_Tests + COMMAND "${CMAKE_COMMAND}" -E env + "PATH=${renderive_qt_runtime_root}/bin\;$ENV{PATH}" + "QT_QPA_PLATFORM=offscreen" + "QT_QPA_PLATFORM_PLUGIN_PATH=${renderive_qt_runtime_root}/plugins/platforms" + "$" + ) +endif() diff --git a/render_2D/tests/Qt_Bridge_Tests.cpp b/Qt/tests/Qt_Bridge_Tests.cpp similarity index 97% rename from render_2D/tests/Qt_Bridge_Tests.cpp rename to Qt/tests/Qt_Bridge_Tests.cpp index 5e14527..94b9685 100644 --- a/render_2D/tests/Qt_Bridge_Tests.cpp +++ b/Qt/tests/Qt_Bridge_Tests.cpp @@ -1,19 +1,14 @@ #include "Qt/plot/Latency_Eager_Plot.h" - #include #include #include #include - #include - namespace renderive { namespace { - TEST(Renderive_Qt, WidgetLifecycleDrivesAndStopsKernelScene) { auto* application = qobject_cast(QCoreApplication::instance()); ASSERT_NE(application, nullptr); - Latency_Eager_Plot plot; plot.resize(240, 120); plot.init(); @@ -34,7 +29,6 @@ TEST(Renderive_Qt, WidgetLifecycleDrivesAndStopsKernelScene) { .set_frequency_point_size(8) .build(); spectrum->update_samples(std::vector{0.1, 0.3, 0.8, 0.5, 0.9, 0.4, 0.2, 0.7}); - plot.show(); QTimer::singleShot(150, application, &QCoreApplication::quit); EXPECT_EQ(application->exec(), 0); @@ -44,14 +38,11 @@ TEST(Renderive_Qt, WidgetLifecycleDrivesAndStopsKernelScene) { application->processEvents(); EXPECT_FALSE(plot.running()); } - -} // namespace -} // namespace renderive - +} +} int main(int argc, char** argv) { qputenv("QT_QPA_PLATFORM", "offscreen"); QApplication application(argc, argv); testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } - diff --git a/cmake/RenderivePackage.cmake b/cmake/RenderivePackage.cmake new file mode 100644 index 0000000..bed1390 --- /dev/null +++ b/cmake/RenderivePackage.cmake @@ -0,0 +1,27 @@ +get_filename_component(renderive_package_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) +set(renderive_package_includes "^.*$") +set(renderive_package_excludes + "^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 + "${renderive_package_root}/renderive_package.zip" + "${renderive_package_root}" + renderive_package_includes + renderive_package_excludes +) diff --git a/main.cmake b/main.cmake deleted file mode 100644 index d6095d4..0000000 --- a/main.cmake +++ /dev/null @@ -1,178 +0,0 @@ -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 - global::taskflow - global::mpmcqueue - global::drogon - global::libdatachannel - # renderive::datoviz - ${Psc_Core_dependencies} -) -#rcl_generate_registered_cmake_library() - -library_is_installed_with_rely(renderive_dependencies_installed ${renderive_dependencies}) -if (NOT renderive_dependencies_installed) - set(log "[FATAL_ERROR] Renderive 依赖未安装,请先构建 renderive_env 目标") - rcl_log_append("${log}") - rcl_add_dependency_action_targets(renderive_env ${renderive_dependencies}) - return() -endif () -rcl_load_dependency_environment(${renderive_dependencies}) -find_package(Drogon) -find_package(spdlog CONFIG REQUIRED) -find_package(LibDataChannel CONFIG REQUIRED) - - -find_package(Qt5 MODULE REQUIRED COMPONENTS - Core Gui Widgets Svg Xml PrintSupport QuickWidgets Multimedia -) -find_package(blend2d REQUIRED) -find_package(GTest REQUIRED) -find_package(Taskflow CONFIG REQUIRED) -find_package(MPMCQueue CONFIG REQUIRED) -find_package(Threads REQUIRED) -set(qt_libraries - Qt5::Widgets - Qt5::Core - Qt5::Svg - Qt5::Xml - Qt5::PrintSupport - Qt5::Multimedia - Qt5::QuickWidgets -) -set(root_dir "${CMAKE_CURRENT_LIST_DIR}") -# Kernel is the only rendering architecture used by render_2D. -add_library(Renderive_Kernel STATIC - "${root_dir}/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp" - "${root_dir}/Kernel/src/renderive/renderable/base/Renderable_Base.cpp" - "${root_dir}/Kernel/src/renderive/scene/base/Scene_Base.cpp" -) -target_include_directories(Renderive_Kernel PUBLIC "${root_dir}/Kernel/src") -target_compile_features(Renderive_Kernel PUBLIC cxx_std_20) -target_link_libraries(Renderive_Kernel PRIVATE Taskflow::Taskflow Threads::Threads) -file(GLOB_RECURSE renderive_core2_sources CONFIGURE_DEPENDS - "${root_dir}/render_2D/*.cpp" - "${root_dir}/render_2D/*.h" - "${root_dir}/render_2D/*.hpp" -) -list(FILTER renderive_core2_sources EXCLUDE REGEX "[/\\\\]tests[/\\\\]") -add_library(Renderive_render_2D STATIC ${renderive_core2_sources}) -target_include_directories(Renderive_render_2D PUBLIC "${root_dir}") -target_compile_features(Renderive_render_2D PUBLIC cxx_std_20) -target_link_libraries(Renderive_render_2D - PUBLIC Renderive_Kernel - PRIVATE blend2d::blend2d -) -set(renderive_qt_sources - "${root_dir}/Qt/bridge/Qt_Event_Adapter.cpp" - "${root_dir}/Qt/bridge/Qt_Image_Adapter.cpp" - "${root_dir}/Qt/bridge/Qt_Presentation_Sink.cpp" - "${root_dir}/Qt/plot/Plot.cpp" - "${root_dir}/Qt/plot/Plot.h" - "${root_dir}/Qt/plot/Latency_Eager_Plot.cpp" - "${root_dir}/Qt/plot/Latency_Eager_Plot.h" - "${root_dir}/Qt/plot/Explicit_Plot.cpp" - "${root_dir}/Qt/plot/Explicit_Plot.h" -) -add_library(Renderive_Qt STATIC ${renderive_qt_sources}) -set_property(TARGET Renderive_Qt PROPERTY AUTOMOC ON) -target_include_directories(Renderive_Qt PUBLIC "${root_dir}") -target_compile_features(Renderive_Qt PUBLIC cxx_std_20) -target_link_libraries(Renderive_Qt PUBLIC Renderive_render_2D ${qt_libraries}) -unset(Psc_Widget_src) -append_glob_source(Psc_Widget_src "${root_dir}/Widget") -add_library(Psc_Widget STATIC ${Psc_Widget_src}) -set_property(TARGET Psc_Widget PROPERTY AUTOMOC ON) -set_property(TARGET Psc_Widget PROPERTY AUTORCC ON) -target_link_libraries(Psc_Widget PUBLIC ${qt_libraries} Psc_Core_Static) -enable_testing() -file(GLOB_RECURSE renderive_kernel_test_sources CONFIGURE_DEPENDS - "${root_dir}/Kernel/tests/renderive/*.cpp" -) -add_executable(Renderive_Kernel_Tests EXCLUDE_FROM_ALL - "${root_dir}/Kernel/tests/main.cpp" - ${renderive_kernel_test_sources} -) -target_compile_definitions(Renderive_Kernel_Tests PRIVATE RENDERIVE_WITH_GTEST) -target_link_libraries(Renderive_Kernel_Tests PRIVATE Renderive_Kernel GTest::GTest) -add_test(NAME Renderive_Kernel_Tests COMMAND Renderive_Kernel_Tests) -add_executable(Renderive_render_2D_Tests EXCLUDE_FROM_ALL - "${root_dir}/Kernel/tests/main.cpp" - "${root_dir}/render_2D/tests/render_2D_Integration_Tests.cpp" - "${root_dir}/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp" -) -target_link_libraries(Renderive_render_2D_Tests PRIVATE - Renderive_render_2D - GTest::GTest - blend2d::blend2d -) -add_test(NAME Renderive_render_2D_Tests COMMAND Renderive_render_2D_Tests) -add_executable(Renderive_Qt_Tests EXCLUDE_FROM_ALL - "${root_dir}/render_2D/tests/Qt_Bridge_Tests.cpp" -) -target_link_libraries(Renderive_Qt_Tests PRIVATE Renderive_Qt GTest::GTest) -get_filename_component(renderive_qt_runtime_root "${Qt5_DIR}/../../.." ABSOLUTE) -add_test(NAME Renderive_Qt_Tests - COMMAND "${CMAKE_COMMAND}" -E env - "PATH=${renderive_qt_runtime_root}/bin\;$ENV{PATH}" - "QT_QPA_PLATFORM=offscreen" - "QT_QPA_PLATFORM_PLUGIN_PATH=${renderive_qt_runtime_root}/plugins/platforms" - "$" -) -if (MSVC) - target_compile_options(Renderive_Kernel PRIVATE /utf-8) - target_compile_options(Renderive_render_2D PRIVATE /utf-8) - target_compile_options(Renderive_Qt PRIVATE /utf-8) -endif () -if (NOT TARGET Adminive::Nlohmann) - set(renderive_saved_build_testing "${BUILD_TESTING}") - set(BUILD_TESTING OFF) - - - if (CMAKE_CURRENT_LIST_DIR STREQUAL CMAKE_SOURCE_DIR) - set(3rd_dir "${CMAKE_SOURCE_DIR}/third_party") - else () - set(3rd_dir "${root_dir}/..") - endif () - - add_subdirectory("${3rd_dir}/Adminive" - "${CMAKE_CURRENT_BINARY_DIR}/third_party/Adminive" - EXCLUDE_FROM_ALL) - set(BUILD_TESTING "${renderive_saved_build_testing}") - 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 -) \ No newline at end of file diff --git a/render_2D/CMakeLists.txt b/render_2D/CMakeLists.txt new file mode 100644 index 0000000..46e1855 --- /dev/null +++ b/render_2D/CMakeLists.txt @@ -0,0 +1,31 @@ +find_package(blend2d CONFIG REQUIRED) +add_library(Renderive_render_2D STATIC) +target_sources(Renderive_render_2D PRIVATE + axis/Axis.cpp + base/Types.cpp + plot/Plot_Core.cpp + plottable/Heatmaps.cpp + plottable/Overlays.cpp + plottable/Performance_Overlay.cpp + plottable/Spectrum.cpp + render/Blend2D_Cache.cpp + renderable/Renderable.cpp +) +target_include_directories(Renderive_render_2D PUBLIC + "$" +) +target_compile_features(Renderive_render_2D PUBLIC cxx_std_20) +target_link_libraries(Renderive_render_2D PUBLIC Renderive_Kernel blend2d::blend2d) +if(MSVC) + target_compile_options(Renderive_render_2D PRIVATE /utf-8) +endif() +if(RENDERIVE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) + add_executable(Renderive_render_2D_Tests) + target_sources(Renderive_render_2D_Tests PRIVATE + tests/render_2D_Frame_Pipeline_Tests.cpp + tests/render_2D_Integration_Tests.cpp + ) + target_link_libraries(Renderive_render_2D_Tests PRIVATE Renderive_render_2D GTest::gtest_main) + add_test(NAME Renderive_render_2D_Tests COMMAND Renderive_render_2D_Tests) +endif() diff --git a/test/Renderive_Core_Tests.cpp b/test/Renderive_Core_Tests.cpp deleted file mode 100644 index de138c5..0000000 --- a/test/Renderive_Core_Tests.cpp +++ /dev/null @@ -1,2675 +0,0 @@ -#include "Core/architecture/Frame_Scheduler.h" -#include "Core/architecture/Render_Lease.h" -#include "Core/Axis/Axis_p.h" -#include "Core/flow/low_latency/Low_Latency_Roles.h" -#include "Core/flow/low_latency/Triple_Buffer.h" -#include "Core/flow/explicit/Explicit_Frame_Flow.h" -#include "Core/flow/explicit/Explicit_Renderable_Runtime.h" -#include "Core/flow/low_latency/Low_Latency_Feedback_Controller.h" -#include "Core/flow/low_latency/Low_Latency_Frame_Flow.h" -#include "Core/flow/low_latency/Low_Latency_Frame_Pipeline.h" -#include "Core/flow/low_latency/Low_Latency_Renderable_Runtime.h" -#include "Core/flow/Present_Surface_Lease.h" -#include "Core/plot/Plot_Core.h" -#include "Core/execution/Render_Executor.h" -#include "Core/primitive/Curve_Utils_p.h" -#include "Core/plottable/Frequency_Trace_p.h" -#include "Core/plottable/Performance_Overlay_p.h" -#include "Core/plottable/export.h" -#include "Core/render/Image.h" -#include "Core/render/Render_Frame_Snapshot.h" -#include "Core/render/Render_Output.h" -#include "Core/render/Renderable_Frame_Node.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -namespace renderive { - -struct Low_Latency_Frame_Pipeline_Test_Probe { - static bool paint_request_pending(const Low_Latency_Frame_Pipeline& pipeline) { - return pipeline.paint_request_pending.load(std::memory_order_acquire); - } - - static void clear_paint_request(Low_Latency_Frame_Pipeline& pipeline) { - pipeline.paint_request_pending.store(false, std::memory_order_release); - } - - static Triple_Buffer_Mark_Record acquire_frame_role(Low_Latency_Frame_Pipeline& pipeline, std::uint8_t role) { - return pipeline.frame_control.try_acquire_role(role); - } - - static void release_frame_role(Low_Latency_Frame_Pipeline& pipeline, Triple_Buffer_Lease lease) { - pipeline.frame_control.unmark_use(lease); - } - - static bool has_published_frame(const Low_Latency_Frame_Pipeline& pipeline) { - return pipeline.has_published_frame.load(std::memory_order_acquire); - } -}; - -struct Low_Latency_Frame_Flow_Test_Probe { - static Low_Latency_Frame_Pipeline& pipeline(Low_Latency_Frame_Flow& flow) { - return flow.pipeline_for_test(); - } -}; - -namespace { -bool unique_roles(const Triple_Buffer_View& view) { - std::array seen{}; - for (std::uint8_t index : view.index) { - if (index >= seen.size() || seen[index]) - return false; - seen[index] = true; - } - return true; -} -Frame_Lifecycle_Record render_frame(std::uint64_t begin_ns, std::uint64_t end_ns) { - Frame_Lifecycle_Record frame; - frame.render_role_enter_ns = begin_ns; - frame.render_begin_ns = begin_ns; - frame.render_end_ns = end_ns; - return frame; -} -Timeline_Stream_Snapshot timeline_for_ticks(int first_tick, int last_tick, int visible_count = 0) { - Timeline_Stream_Snapshot timeline; - int count = std::max(0, last_tick - first_tick + 1); - timeline.visible_time_point_count = visible_count > 0 ? visible_count : std::max(1, count); - timeline.first_stream_tick = first_tick; - timeline.last_stream_tick = last_tick; - timeline.coordinate_begin = first_tick; - timeline.times.resize(static_cast(count)); - return timeline; -} -void prepare_frequency_trace(Frequency_Trace_Private& trace, std::initializer_list ticks, const Timeline_Stream_Snapshot& timeline) { - trace.resize_power_buffer(static_cast(ticks.size())); - for (int tick : ticks) - trace.push_power(Frequency_Sample{tick, static_cast(tick)}); - trace.snapshot_power_buffer(); - trace.rebuild_prepared_points(timeline); -} -Low_Latency_Frame_Pipeline::Frame_Publish_Result publish_pipeline_frame(Low_Latency_Frame_Pipeline& pipeline, std::uint64_t frame_id, std::uint64_t now_ns) { - auto lease = pipeline.try_acquire_render_frame(); - EXPECT_TRUE(static_cast(lease)); - if (!lease) - return {}; - EXPECT_TRUE(lease.frame->metadata); - lease.frame->metadata->frame_id = frame_id; - lease.frame->metadata->render_begin_ns = now_ns >= 10 ? now_ns - 10 : now_ns; - lease.frame->metadata->render_end_ns = now_ns >= 5 ? now_ns - 5 : now_ns; - lease.frame->image.resize(2, 2); - lease.frame->image.fill(Color::black()); - lease.frame->render_complete.store(true, std::memory_order_release); - lease.frame->version.store(frame_id, std::memory_order_release); - return pipeline.try_publish_rendered_frame(std::move(lease), now_ns); -} -Render_Surface_Publish_Result publish_low_latency_flow_frame(Low_Latency_Frame_Flow& flow, std::uint64_t frame_id, std::uint64_t now_ns, int width = 2, int height = 2, Color color = Color::black()) { - auto surface = flow.begin_render_frame(); - EXPECT_TRUE(static_cast(surface)); - if (!surface) - return {}; - EXPECT_TRUE(surface.output && surface.output->metadata); - surface.output->metadata->frame_id = frame_id; - surface.output->metadata->render_begin_ns = now_ns >= 10 ? now_ns - 10 : now_ns; - surface.output->metadata->render_end_ns = now_ns >= 5 ? now_ns - 5 : now_ns; - surface.output->image.resize(width, height); - surface.output->image.fill(color); - surface.output->render_complete.store(true, std::memory_order_release); - return flow.publish_rendered_frame(std::move(surface), now_ns); -} -Render_Surface_Publish_Result publish_explicit_frame(Explicit_Frame_Flow& flow, std::uint64_t frame_id, std::uint64_t now_ns, int width = 2, int height = 2, Color color = Color::green()) { - auto surface = flow.begin_render_frame(); - EXPECT_TRUE(static_cast(surface)); - if (!surface) - return {}; - EXPECT_TRUE(surface.output && surface.output->metadata); - surface.output->metadata->frame_id = frame_id; - surface.output->metadata->render_begin_ns = now_ns >= 10 ? now_ns - 10 : now_ns; - surface.output->metadata->render_end_ns = now_ns >= 5 ? now_ns - 5 : now_ns; - surface.output->image.resize(width, height); - surface.output->image.fill(color); - surface.output->render_complete.store(true, std::memory_order_release); - return flow.publish_rendered_frame(std::move(surface), now_ns); -} -Frame_Lifecycle_Record feedback_frame(std::uint64_t frame_id, std::uint64_t base_ns, std::uint64_t render_ns, std::uint64_t paint_ns, std::uint64_t ready_wait_ns = 0, std::uint64_t gui_wait_ns = 0) { - Frame_Lifecycle_Record frame; - frame.frame_id = frame_id; - frame.core_data_time_ns = base_ns; - frame.render_role_enter_ns = base_ns; - frame.render_begin_ns = base_ns; - frame.render_end_ns = base_ns + render_ns; - frame.ready_queue_wait_ns = ready_wait_ns; - frame.transition_12_ns = frame.render_end_ns + ready_wait_ns; - frame.update_request_time_ns = frame.transition_12_ns; - frame.update_posted_ns = frame.update_request_time_ns; - frame.paint_begin_ns = frame.transition_12_ns + gui_wait_ns; - frame.paint_end_ns = frame.paint_begin_ns + paint_ns; - return frame; -} -void feed_feedback_frame(Low_Latency_Feedback_Controller& controller, std::uint64_t frame_id, std::uint64_t base_ns, std::uint64_t render_ns, std::uint64_t paint_ns, std::uint64_t ready_wait_ns = 0, std::uint64_t gui_wait_ns = 0) { - auto frame = feedback_frame(frame_id, base_ns, render_ns, paint_ns, ready_wait_ns, gui_wait_ns); - controller.on_render_completed(frame); - controller.on_frame_presented(frame); -} -void feed_backlogged_feedback_frame(Low_Latency_Feedback_Controller& controller, std::uint64_t frame_id, std::uint64_t base_ns, std::uint64_t render_ns, std::uint64_t paint_ns, std::uint64_t ready_wait_ns = 0, std::uint64_t gui_wait_ns = 0) { - auto frame = feedback_frame(frame_id, base_ns, render_ns, paint_ns, ready_wait_ns, gui_wait_ns); - controller.note_middle_published(frame.transition_12_ns); - controller.on_render_completed(frame); - controller.on_frame_presented(frame); -} -bool wait_until(std::function predicate) { - auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); - while (std::chrono::steady_clock::now() < deadline) { - if (predicate()) - return true; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - return predicate(); -} -std::shared_ptr performance_frame(std::uint64_t frame_id, int width = 240, int height = 80) { - auto frame = std::make_shared(); - frame->frame_id = frame_id; - frame->input_version = frame_id + 1; - frame->render_begin_ns = 10; - frame->prepare_begin_ns = 11; - frame->prepare_end_ns = 12; - frame->draw_begin_ns = 13; - frame->draw_end_ns = 14; - frame->render_end_ns = 20; - frame->transition_12_ns = 25; - frame->update_request_time_ns = 26; - frame->update_posted_ns = 26; - frame->paint_begin_ns = 30; - frame->paint_end_ns = 40; - frame->presented_frame_id = frame_id; - frame->transition_32_ns = 45; - frame->pixel_width = static_cast(width); - frame->pixel_height = static_cast(height); - frame->viewport_version = 1; - frame->outcome = Frame_Outcome::Presented; - frame->worker.executor_at_finish.worker_count = 4; - frame->worker.executor_at_finish.active_workers = 1; - frame->worker.executor_at_finish.completed_task_nodes = frame_id; - return frame; -} -bool valid_utf8(const std::string& text) { - for (std::size_t i = 0; i < text.size();) { - unsigned char c = static_cast(text[i]); - std::size_t n = 0; - if ((c & 0x80u) == 0) - n = 1; - else if ((c & 0xE0u) == 0xC0u) - n = 2; - else if ((c & 0xF0u) == 0xE0u) - n = 3; - else if ((c & 0xF8u) == 0xF0u) - n = 4; - else - return false; - if (i + n > text.size()) - return false; - for (std::size_t j = 1; j < n; ++j) { - if ((static_cast(text[i + j]) & 0xC0u) != 0x80u) - return false; - } - i += n; - } - return true; -} -std::filesystem::path unique_test_directory(std::string_view name) { - auto stamp = std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count(); - std::filesystem::path path = std::filesystem::temp_directory_path() - / ("renderive_" + std::string(name) + "_" + std::to_string(stamp)); - std::filesystem::remove_all(path); - std::filesystem::create_directories(path); - return path; -} -std::vector read_lines(const std::filesystem::path& path) { - std::ifstream input(path); - std::vector lines; - std::string line; - while (std::getline(input, line)) - lines.push_back(line); - return lines; -} -using Parsed_Log_Record = std::map; -std::vector read_log_records(const std::filesystem::path& path, std::string_view kind) { - std::vector records; - Parsed_Log_Record current; - bool in_record = false; - std::string begin = "[" + std::string(kind) + "]"; - std::string end = "[/" + std::string(kind) + "]"; - for (const auto& line : read_lines(path)) { - if (line == begin) { - current.clear(); - in_record = true; - continue; - } - if (line == end) { - if (in_record) - records.push_back(std::move(current)); - current = {}; - in_record = false; - continue; - } - if (!in_record) - continue; - std::size_t separator = line.find('='); - if (separator != std::string::npos) - current.emplace(line.substr(0, separator), line.substr(separator + 1)); - } - return records; -} -Frame_Feedback_Event_Record feedback_event(std::string event, const Frame_Lifecycle_Record& frame, std::uint64_t timestamp_ns) { - Frame_Feedback_Event_Record record; - record.event = std::move(event); - record.frame = frame; - record.timestamp_ns = timestamp_ns; - record.mode_before = frame.refresh.production_mode; - record.mode_after = frame.refresh.production_mode; - record.effective_interval_before_ns = frame.refresh.effective_min_render_interval_ns; - record.effective_interval_after_ns = frame.refresh.effective_min_render_interval_ns; - record.dirty = frame.dirty_reason_mask != 0; - record.middle_has_new_frame = frame.refresh.middle_published_count > 0; - record.paint_update_posted = frame.update_posted_ns != 0; - record.backlog_epoch_active = frame.refresh.consumer_window_size > 0; - record.continuously_backlogged = frame.refresh.last_consumer_sample_result == Consumer_Sample_Result::Accepted; - record.producer_starved = frame.refresh.producer_starvation_active; - record.backlog_epoch_id = frame.refresh.producer_starvation_epoch_id; - record.flush = true; - return record; -} - -struct Lease_Test_State : Render_State { - int value{}; -}; -struct Lease_Test_Input : Input_Data {}; -struct Lease_Test_Owner : Renderable { - Render_Object_Owner create_render_state() override { - return make_render_object(); - } - Render_Object_Owner create_input_data() override { - return make_render_object(); - } -}; -struct Lease_Test_Data : Typed_Render_Data { - Lease_Test_Owner runtime_owner; - void initialize_low_latency() { - q_ptr = &runtime_owner; - initialize_runtime(make_low_latency_renderable_runtime(runtime_owner)); - } - void initialize_explicit() { - q_ptr = &runtime_owner; - initialize_runtime(make_explicit_renderable_runtime(runtime_owner)); - } -}; -struct No_Input_Test_Owner : Renderable {}; -struct No_Input_Test_Data : Typed_Render_Data {}; -struct Runtime_Count_Owner : Renderable { - int state_create_count{}; - int input_create_count{}; - Render_Object_Owner create_render_state() override { - ++state_create_count; - return make_render_object(); - } - Render_Object_Owner create_input_data() override { - ++input_create_count; - return make_render_object(); - } -}; -struct Runtime_Count_Data : Typed_Render_Data {}; -struct Frame_View_Blocking_Renderable; -struct Frame_View_Blocking_Data : Typed_Render_Data {}; -struct Frame_View_Blocking_Renderable : Renderable { - Frame_View_Blocking_Renderable(); - Frame_View_Blocking_Data* d(); - Render_Object_Owner create_render_data() override; - Render_Object_Owner create_render_state() override; - Render_Object_Owner create_input_data() override; -}; -RENDERIVE_DEFINE_RENDERABLE_BINDING(Frame_View_Blocking_Renderable, Frame_View_Blocking_Data, Lease_Test_State, Lease_Test_Input, "Frame_View_Blocking") -struct Counting_Test_Input : Input_Data { - int clear_count{}; - void clear() override { - ++clear_count; - } -}; -struct Counting_Input_Owner : Renderable { - Render_Object_Owner create_render_state() override { - return make_render_object(); - } - Render_Object_Owner create_input_data() override { - return make_render_object(); - } -}; -struct Counting_Input_Data : Typed_Render_Data {}; - -void init_lease_test_state(Lease_Test_Data& data) { - data.initialize_low_latency(); -} - -struct Flow_Test_Host : Frame_Flow_Host { - Frame_Flow_Runtime_Snapshot snapshot; - int render_attempts{}; - int paint_requests{}; - int deadline_requests{}; - int deadline_cancels{}; - bool render_result = true; - std::vector feedback_events; - - Frame_Flow_Runtime_Snapshot runtime_snapshot(std::uint64_t now_ns) const override { - auto result = snapshot; - result.now_ns = now_ns; - return result; - } - bool try_render_once() override { - ++render_attempts; - snapshot.render_job_active = true; - return render_result; - } - void request_paint_from_middle() override { - ++paint_requests; - snapshot.middle_has_new_frame = false; - } - void schedule_render_deadline(std::uint64_t) override { - ++deadline_requests; - } - void cancel_render_deadline() override { - ++deadline_cancels; - } - void record_feedback_event(std::shared_ptr event) override { - if (event) - feedback_events.push_back(event->event); - } -}; - -struct Low_Latency_Present_Test_Host : Frame_Flow_Host { - Low_Latency_Frame_Flow* flow{}; - std::uint64_t request_now_ns{}; - int paint_requests{}; - Render_Surface_Publish_Result last_present_request; - - Frame_Flow_Runtime_Snapshot runtime_snapshot(std::uint64_t now_ns) const override { - return flow ? flow->runtime_snapshot(now_ns, false) : Frame_Flow_Runtime_Snapshot{false, false, false, false, false, now_ns}; - } - bool try_render_once() override { - return false; - } - void request_paint_from_middle() override { - ++paint_requests; - if (flow) - last_present_request = flow->request_present(request_now_ns); - } - void schedule_render_deadline(std::uint64_t) override {} - void cancel_render_deadline() override {} -}; - -} // namespace -TEST(Renderive_Canvas, TextUsesPenAndFillUsesBrush) { - auto diagnostics = Canvas::default_font_diagnostics(); - ASSERT_TRUE(diagnostics.face_valid); - ASSERT_TRUE(diagnostics.font_valid); - ASSERT_GT(diagnostics.test_width, 0.0); - ASSERT_GT(diagnostics.test_height, 0.0); - Image image(400, 100); - image.fill(Color::transparent()); - { - Canvas canvas(image); - canvas.set_brush(Brush{Color::white(), Brush_Style::Solid}); - canvas.set_pen(Pen{Color::black()}); - canvas.fill_rect(RectF{0.0, 0.0, 400.0, 100.0}); - canvas.draw_text(PointF{10.0, 10.0}, "0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87"); - canvas.draw_text(RectF{10.0, 50.0, 300.0, 30.0}, Text_Align::Left | Text_Align::Top, "0123"); - } - EXPECT_EQ(premultiplied_to_color(image.row(2)[2]), Color::white()); - int dark_pixels = 0; - for (int y = 8; y < 82; ++y) { - const Pixel* row = image.row(y); - ASSERT_NE(row, nullptr); - for (int x = 8; x < 320; ++x) { - Color color = premultiplied_to_color(row[x]); - if (color.a && color.r < 120 && color.g < 120 && color.b < 120) - ++dark_pixels; - } - } - EXPECT_GT(dark_pixels, 20); -} -TEST(Renderive_Triple_Buffer, RoleSwapsAreTryOnlyAndDeterministic) { - Triple_Role_Buffer_Control control; - control.reset(); - EXPECT_TRUE(unique_roles(control.read_view())); - auto first = control.try_swap_role(Frame_Rendering, Frame_Middle, [](const Triple_Buffer_View&) { - return true; - }); - EXPECT_EQ(first.result, Triple_Buffer_Result::Success); - EXPECT_TRUE(unique_roles(control.read_view())); - auto painting = control.try_acquire_role(Frame_Painting); - ASSERT_EQ(painting.result, Triple_Buffer_Result::Success); - auto blocked = control.try_swap_role(Frame_Middle, Frame_Painting, [](const Triple_Buffer_View&) { - return true; - }); - EXPECT_EQ(blocked.result, Triple_Buffer_Result::Busy); - control.unmark_use(painting.lease); - for (int i = 0; i < 1000000; ++i) { - auto a = control.try_swap_role(Frame_Rendering, Frame_Middle, [](const Triple_Buffer_View&) { - return true; - }); - ASSERT_EQ(a.result, Triple_Buffer_Result::Success); - auto b = control.try_swap_role(Frame_Middle, Frame_Painting, [](const Triple_Buffer_View&) { - return true; - }); - ASSERT_EQ(b.result, Triple_Buffer_Result::Success); - ASSERT_TRUE(unique_roles(control.read_view())); - } -} - -TEST(Renderive_State, CloseWakesBlockedAcquire) { - Triple_Role_Buffer_Control control; - control.reset(); - auto held = control.acquire_role(State_Edit); - ASSERT_EQ(held.result, Triple_Buffer_Result::Success); - std::atomic_bool entered{false}; - Triple_Buffer_Mark_Record blocked_result; - std::thread blocked([&]() { - entered.store(true, std::memory_order_release); - blocked_result = control.acquire_role(State_Edit); - }); - ASSERT_TRUE(wait_until([&]() { - return entered.load(std::memory_order_acquire); - })); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - control.close(); - blocked.join(); - EXPECT_EQ(blocked_result.result, Triple_Buffer_Result::Cancelled); - control.unmark_use(held.lease); -} - -TEST(Renderive_State, CloseDoesNotReleaseActiveLease) { - Triple_Role_Buffer_Control control; - control.reset(); - auto held = control.acquire_role(State_Edit); - ASSERT_EQ(held.result, Triple_Buffer_Result::Success); - EXPECT_TRUE(control.role_busy(State_Edit)); - - control.close(); - EXPECT_TRUE(control.role_busy(State_Edit)); - auto blocked = control.try_acquire_role(State_Edit); - EXPECT_EQ(blocked.result, Triple_Buffer_Result::Cancelled); - - control.unmark_use(held.lease); - control.wait_until_idle(); - EXPECT_FALSE(control.role_busy(State_Edit)); -} - -TEST(Renderive_State, ReinitializeWaitsForActiveLease) { - Lease_Test_Data data; - init_lease_test_state(data); - auto held = data.acquire_edit_state_read(); - ASSERT_TRUE(static_cast(held)); - - std::atomic_bool reinitialize_returned{false}; - std::latch reinitialize_started(1); - Lease_Test_Owner explicit_owner; - std::thread reinitializer([&]() { - reinitialize_started.count_down(); - data.initialize_runtime(make_explicit_renderable_runtime(explicit_owner)); - reinitialize_returned.store(true, std::memory_order_release); - }); - - reinitialize_started.wait(); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(reinitialize_returned.load(std::memory_order_acquire)); - held = {}; - reinitializer.join(); - EXPECT_TRUE(reinitialize_returned.load(std::memory_order_acquire)); - EXPECT_NE(data.edit_state(), nullptr); -} - -TEST(Renderive_State, BusyEditSetterWaitsAndCommits) { - Lease_Test_Data data; - init_lease_test_state(data); - auto held = data.acquire_edit_state_read(); - ASSERT_TRUE(static_cast(held)); - - std::atomic_bool setter_returned{false}; - std::thread setter([&]() { - data.set_state_value(&Lease_Test_State::value, 42); - setter_returned.store(true, std::memory_order_release); - }); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(setter_returned.load(std::memory_order_acquire)); - held = {}; - setter.join(); - - EXPECT_TRUE(setter_returned.load(std::memory_order_acquire)); - EXPECT_EQ(data.edit_state_value(&Lease_Test_State::value), 42); -} -TEST(Renderive_State, GetterWaitsForWriter) { - Lease_Test_Data data; - init_lease_test_state(data); - auto held = data.acquire_edit_state(); - ASSERT_TRUE(static_cast(held)); - auto* state = static_cast(held.state); - ASSERT_NE(state, nullptr); - state->value = 77; - - std::atomic_bool getter_started{false}; - std::atomic_bool getter_returned{false}; - int observed = -1; - std::thread getter([&]() { - getter_started.store(true, std::memory_order_release); - observed = data.edit_state_value(&Lease_Test_State::value); - getter_returned.store(true, std::memory_order_release); - }); - ASSERT_TRUE(wait_until([&]() { - return getter_started.load(std::memory_order_acquire); - })); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(getter_returned.load(std::memory_order_acquire)); - held = {}; - getter.join(); - - EXPECT_TRUE(getter_returned.load(std::memory_order_acquire)); - EXPECT_EQ(observed, 77); -} -TEST(Renderive_Input_Runtime, ExactInputDataBindingSkipsAllocation) { - using No_Input_Binding = Renderable_Binding; - using Custom_Input_Binding = Renderable_Binding; - EXPECT_FALSE(static_cast(No_Input_Binding::create_input_data())); - EXPECT_TRUE(static_cast(Custom_Input_Binding::create_input_data())); - - No_Input_Test_Data data; - Render_Input_Lease lease(&data); - EXPECT_FALSE(static_cast(lease)); -} - -TEST(Renderive_Input_Runtime, ExplicitRuntimeCreatesOneStateAndInput) { - Runtime_Count_Owner owner; - Runtime_Count_Data explicit_data; - explicit_data.initialize_runtime(make_explicit_renderable_runtime(owner)); - EXPECT_EQ(owner.state_create_count, 1); - EXPECT_EQ(owner.input_create_count, 1); - EXPECT_NE(explicit_data.edit_state(), nullptr); - { - Render_Edit_Lease edit(&explicit_data); - ASSERT_TRUE(static_cast(edit)); - edit->value = 42; - } - EXPECT_EQ(explicit_data.edit_state_value(&Lease_Test_State::value), 42); -} - -TEST(Renderive_Input_Runtime, LowLatencyRuntimeCreatesThreeStatesAndInputs) { - Runtime_Count_Owner owner; - Runtime_Count_Data low_latency_data; - low_latency_data.initialize_runtime(make_low_latency_renderable_runtime(owner)); - EXPECT_EQ(owner.state_create_count, 3); - EXPECT_EQ(owner.input_create_count, 3); - EXPECT_NE(low_latency_data.edit_state(), nullptr); - EXPECT_NE(low_latency_data.ready_state(), nullptr); - EXPECT_NE(low_latency_data.render_state(), nullptr); -} - -TEST(Renderive_Input_Runtime, ExplicitRuntimeContainsNoTripleBuffer) { - Runtime_Count_Owner owner; - Runtime_Count_Data data; - data.initialize_runtime(make_explicit_renderable_runtime(owner)); - auto frame_view = data.capture_frame_view(); - ASSERT_TRUE(static_cast(frame_view)); - EXPECT_EQ(frame_view.state, data.render_state()); - EXPECT_EQ(frame_view.input, data.render_input_data()); -} - -TEST(Renderive_Input_Runtime, ExplicitInputFrameDoesNotDeadlock) { - Runtime_Count_Owner owner; - Runtime_Count_Data data; - data.initialize_runtime(make_explicit_renderable_runtime(owner)); - auto completion = make_update_state(); - { - auto edit = data.acquire_edit_input(true); - ASSERT_TRUE(static_cast(edit)); - edit.set_completion(completion); - } - - auto frame_view = data.capture_frame_view(); - ASSERT_TRUE(static_cast(frame_view)); - frame_view.consume_input(); - EXPECT_TRUE(completion->stage_complete(Update_Stage::Committed)); - - std::atomic_bool setter_returned{false}; - std::thread setter([&]() { - auto edit = data.acquire_edit_input(true); - setter_returned.store(true, std::memory_order_release); - }); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(setter_returned.load(std::memory_order_acquire)); - frame_view = {}; - setter.join(); - EXPECT_TRUE(setter_returned.load(std::memory_order_acquire)); -} - -TEST(Renderive_Input_Runtime, ExplicitInputConsumedExactlyOnce) { - Counting_Input_Owner owner; - Counting_Input_Data data; - data.initialize_runtime(make_explicit_renderable_runtime(owner)); - auto completion = make_update_state(); - { - auto edit = data.acquire_edit_input(true); - ASSERT_TRUE(static_cast(edit)); - edit.set_completion(completion); - } - - auto frame_view = data.capture_frame_view(); - ASSERT_TRUE(static_cast(frame_view)); - auto* input = static_cast(frame_view.input); - ASSERT_NE(input, nullptr); - frame_view.consume_input(); - frame_view.consume_input(); - EXPECT_EQ(input->clear_count, 1); - EXPECT_TRUE(completion->stage_complete(Update_Stage::Committed)); - - std::pmr::vector> committed(memory_resource(Memory_Domain::Update_Completion)); - data.drain_committed_update_states(committed); - EXPECT_EQ(committed.size(), 1u); - committed.clear(); - data.drain_committed_update_states(committed); - EXPECT_TRUE(committed.empty()); -} - -TEST(Renderive_Input_Runtime, ExplicitPrepareAndDrawUseSameState) { - Runtime_Count_Owner owner; - Runtime_Count_Data data; - data.initialize_runtime(make_explicit_renderable_runtime(owner)); - auto frame_view = data.capture_frame_view(); - ASSERT_TRUE(static_cast(frame_view)); - auto* prepare_state = static_cast(frame_view.state); - auto* draw_state = static_cast(frame_view.state); - ASSERT_NE(prepare_state, nullptr); - EXPECT_EQ(prepare_state, draw_state); - - std::atomic_bool setter_returned{false}; - std::thread setter([&]() { - auto edit = data.acquire_edit_state(); - setter_returned.store(true, std::memory_order_release); - }); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(setter_returned.load(std::memory_order_acquire)); - frame_view = {}; - setter.join(); - EXPECT_TRUE(setter_returned.load(std::memory_order_acquire)); -} - -TEST(Renderive_Input_Runtime, ExplicitPrepareAndDrawUseSameInput) { - Runtime_Count_Owner owner; - Runtime_Count_Data data; - data.initialize_runtime(make_explicit_renderable_runtime(owner)); - auto frame_view = data.capture_frame_view(); - ASSERT_TRUE(static_cast(frame_view)); - auto* prepare_input = static_cast(frame_view.input); - auto* draw_input = static_cast(frame_view.input); - ASSERT_NE(prepare_input, nullptr); - EXPECT_EQ(prepare_input, draw_input); - - std::atomic_bool setter_returned{false}; - std::thread setter([&]() { - auto edit = data.acquire_edit_input(true); - setter_returned.store(true, std::memory_order_release); - }); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(setter_returned.load(std::memory_order_acquire)); - frame_view = {}; - setter.join(); - EXPECT_TRUE(setter_returned.load(std::memory_order_acquire)); -} - -TEST(Renderive_Input_Runtime, CrossRenderableReadUsesFrameSnapshot) { - auto axis = std::make_shared(); - Axis_Render_State frame_state; - frame_state.orientation = Orientation::Horizontal; - frame_state.x = 10; - frame_state.pixel_length = 50; - frame_state.coord_start = 100.0; - frame_state.coord_length = 25.0; - frame_state.unit_text = "Hz"; - - Renderable_Frame_Tree tree; - auto& node = tree.emplace_back(); - node.renderable = axis; - node.node_id = 77; - node.frame_view.state = &frame_state; - - Render_Frame_Snapshot snapshot; - snapshot.frame_tree = &tree; - - ASSERT_EQ(snapshot.find_node(77), &node); - Axis_Frame_Snapshot axis_snapshot = Axis_Render_Access::snapshot(axis.get(), snapshot); - EXPECT_EQ(axis_snapshot.coord_range.origin, 100.0); - EXPECT_EQ(axis_snapshot.coord_range.target, 125.0); - EXPECT_EQ(axis_snapshot.pixel_range.origin, 10.0); - EXPECT_EQ(axis_snapshot.pixel_range.target, 60.0); - EXPECT_EQ(Axis_Render_Access::unit_text(axis.get(), snapshot), "Hz"); -} - -TEST(Renderive_Input_Runtime, LowLatencyRenderLeaseHeldUntilFrameEnd) { - Runtime_Count_Owner owner; - Runtime_Count_Data data; - data.initialize_runtime(make_low_latency_renderable_runtime(owner)); - auto first = data.capture_frame_view(); - ASSERT_TRUE(static_cast(first)); - auto second = data.capture_frame_view(); - EXPECT_FALSE(static_cast(second)); - first = {}; - auto third = data.capture_frame_view(); - EXPECT_TRUE(static_cast(third)); -} - -TEST(Renderive_Input_Runtime, FlowCreatesRenderableRuntime) { - Runtime_Count_Owner low_owner; - Runtime_Count_Owner explicit_owner; - Low_Latency_Frame_Flow low_latency_flow; - Explicit_Frame_Flow explicit_flow; - auto low_runtime = low_latency_flow.create_renderable_runtime(low_owner); - auto explicit_runtime = explicit_flow.create_renderable_runtime(explicit_owner); - EXPECT_NE(dynamic_cast(low_runtime.get()), nullptr); - EXPECT_NE(dynamic_cast(explicit_runtime.get()), nullptr); -} - -TEST(Renderive_Frequency_Trace, FrequencyTraceContinuousTicksUseOneSegment) { - Frequency_Trace_Private trace; - auto timeline = timeline_for_ticks(0, 3); - prepare_frequency_trace(trace, {0, 1, 2, 3}, timeline); - EXPECT_EQ(trace.segment_count, 1); - EXPECT_EQ(trace.tick_gap_count, 0); - EXPECT_EQ(trace.non_monotonic_tick_count, 0); - ASSERT_EQ(trace.segment_offsets.size(), 1u); - EXPECT_EQ(trace.prepared_points.size(), 4u); -} - -TEST(Renderive_Frequency_Trace, FrequencyTraceTickGapSplitsSegment) { - Frequency_Trace_Private trace; - auto timeline = timeline_for_ticks(0, 4); - prepare_frequency_trace(trace, {0, 1, 3, 4}, timeline); - EXPECT_EQ(trace.segment_count, 2); - EXPECT_EQ(trace.tick_gap_count, 1); - EXPECT_EQ(trace.non_monotonic_tick_count, 0); - ASSERT_EQ(trace.segment_offsets.size(), 2u); - EXPECT_EQ(trace.segment_offsets[0], 0u); - EXPECT_EQ(trace.segment_offsets[1], 2u); -} - -TEST(Renderive_Frequency_Trace, FrequencyTraceNonMonotonicTickSplitsSegment) { - Frequency_Trace_Private trace; - auto timeline = timeline_for_ticks(0, 3); - prepare_frequency_trace(trace, {0, 1, 1, 2}, timeline); - EXPECT_EQ(trace.segment_count, 2); - EXPECT_EQ(trace.tick_gap_count, 0); - EXPECT_EQ(trace.non_monotonic_tick_count, 1); - ASSERT_EQ(trace.segment_offsets.size(), 2u); - EXPECT_EQ(trace.segment_offsets[1], 2u); -} - -TEST(Renderive_Frequency_Trace, FrequencyTraceMappingFailureSplitsSegment) { - Frequency_Trace_Private trace; - auto timeline = timeline_for_ticks(0, 2); - prepare_frequency_trace(trace, {0, 1, 5, 2}, timeline); - EXPECT_EQ(trace.segment_count, 2); - EXPECT_EQ(trace.tick_gap_count, 0); - EXPECT_EQ(trace.non_monotonic_tick_count, 0); - ASSERT_EQ(trace.segment_offsets.size(), 2u); - EXPECT_EQ(trace.segment_offsets[1], 2u); -} - -TEST(Renderive_Frequency_Trace, FrequencyTraceRingWrapKeepsLogicalOrder) { - Frequency_Trace_Private trace; - trace.resize_power_buffer(3); - trace.push_power(Frequency_Sample{0, 0.0}); - trace.push_power(Frequency_Sample{1, 1.0}); - trace.push_power(Frequency_Sample{2, 2.0}); - trace.push_power(Frequency_Sample{3, 3.0}); - trace.snapshot_power_buffer(); - ASSERT_EQ(trace.data_count, 3); - EXPECT_EQ(trace.data_snapshot[0].tick, 1); - EXPECT_EQ(trace.data_snapshot[1].tick, 2); - EXPECT_EQ(trace.data_snapshot[2].tick, 3); -} - -TEST(Renderive_Frame_Pipeline, PublishAndPaintUseMiddleVersionOnly) { - Low_Latency_Frame_Pipeline pipeline; - auto published = publish_pipeline_frame(pipeline, 1, 25); - EXPECT_TRUE(pipeline.middle_frame_ready()); - EXPECT_TRUE(published.request_present); - EXPECT_EQ(published.dropped_frame_record, nullptr); - auto painting = pipeline.acquire_paint_frame(30); - EXPECT_TRUE(painting.has_new_frame); - EXPECT_TRUE(painting.has_valid_frame); - EXPECT_TRUE(static_cast(painting.lease)); - EXPECT_FALSE(pipeline.middle_frame_ready()); -} - -TEST(Renderive_Frame_Pipeline, PresentBeforeFirstFrameReturnsEmpty) { - Low_Latency_Frame_Pipeline pipeline; - auto consumed = pipeline.acquire_paint_frame(10); - EXPECT_FALSE(static_cast(consumed.lease)); - EXPECT_FALSE(consumed.has_valid_frame); - EXPECT_TRUE(consumed.underrun); -} - -TEST(Renderive_Frame_Pipeline, PresentWithoutNewFrameRepeatsPainting) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - EXPECT_TRUE(first.has_new_frame); - first.lease.reset(); - auto repeated = pipeline.acquire_paint_frame(35); - EXPECT_TRUE(static_cast(repeated.lease)); - EXPECT_FALSE(repeated.has_new_frame); - EXPECT_TRUE(repeated.has_valid_frame); - EXPECT_FALSE(repeated.underrun); -} - -TEST(Renderive_Frame_Pipeline, PresentDuringMiddleBusyRepeatsPainting) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - first.lease.reset(); - ASSERT_TRUE(publish_pipeline_frame(pipeline, 2, 40).request_present); - auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); - ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); - auto repeated = pipeline.acquire_paint_frame(45); - EXPECT_TRUE(static_cast(repeated.lease)); - EXPECT_FALSE(repeated.has_new_frame); - EXPECT_TRUE(repeated.has_valid_frame); - EXPECT_FALSE(repeated.underrun); - EXPECT_TRUE(repeated.present_request_was_pending); - EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - repeated.lease.reset(); - Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); -} - -TEST(Renderive_Frame_Pipeline, MiddleBusyPaintAcknowledgesPostedUpdate) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - first.lease.reset(); - ASSERT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - - ASSERT_TRUE(publish_pipeline_frame(pipeline, 2, 40).request_present); - ASSERT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - - auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); - ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); - auto repeated = pipeline.acquire_paint_frame(45); - - EXPECT_TRUE(static_cast(repeated.lease)); - EXPECT_FALSE(repeated.has_new_frame); - EXPECT_TRUE(repeated.has_valid_frame); - EXPECT_TRUE(repeated.present_request_was_pending); - EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - - repeated.lease.reset(); - Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); -} - -TEST(Renderive_Frame_Pipeline, MiddleBusyPaintRepostsAfterPaintFinished) { - Low_Latency_Frame_Flow flow; - Low_Latency_Present_Test_Host host; - host.flow = &flow; - flow.attach(host); - flow.activate_view(20); - ASSERT_TRUE(publish_low_latency_flow_frame(flow, 1, 25).request_present); - std::shared_ptr returned; - auto first = flow.begin_present(30, returned); - ASSERT_TRUE(static_cast(first)); - EXPECT_TRUE(first.new_frame()); - (void)flow.end_present(std::move(first), 31, 32); - - ASSERT_TRUE(publish_low_latency_flow_frame(flow, 2, 40).request_present); - Low_Latency_Frame_Pipeline& pipeline = Low_Latency_Frame_Flow_Test_Probe::pipeline(flow); - auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); - ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); - auto repeated = flow.begin_present(45, returned); - ASSERT_TRUE(static_cast(repeated)); - EXPECT_FALSE(repeated.new_frame()); - EXPECT_TRUE(repeated.request_was_pending()); - (void)flow.end_present(std::move(repeated), 46, 47); - - host.request_now_ns = 50; - flow.notify_paint_finished(50); - EXPECT_EQ(host.paint_requests, 1); - EXPECT_TRUE(host.last_present_request.request_present); - EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); - - auto recovered = flow.begin_present(55, returned); - EXPECT_TRUE(static_cast(recovered)); - EXPECT_TRUE(recovered.new_frame()); - EXPECT_TRUE(recovered.request_was_pending()); - EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); -} - -TEST(Renderive_Frame_Pipeline, LostPresentWakeupDoesNotFreezeFlow) { - Low_Latency_Frame_Flow flow; - Low_Latency_Present_Test_Host host; - host.flow = &flow; - flow.attach(host); - flow.activate_view(20); - ASSERT_TRUE(publish_low_latency_flow_frame(flow, 1, 25).request_present); - std::shared_ptr returned; - auto first = flow.begin_present(30, returned); - ASSERT_TRUE(static_cast(first)); - (void)flow.end_present(std::move(first), 31, 32); - - ASSERT_TRUE(publish_low_latency_flow_frame(flow, 2, 40).request_present); - Low_Latency_Frame_Pipeline& pipeline = Low_Latency_Frame_Flow_Test_Probe::pipeline(flow); - auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); - ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); - auto repeated = flow.begin_present(45, returned); - ASSERT_TRUE(static_cast(repeated)); - EXPECT_FALSE(repeated.new_frame()); - EXPECT_TRUE(repeated.request_was_pending()); - EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - (void)flow.end_present(std::move(repeated), 46, 47); - - EXPECT_TRUE(pipeline.middle_frame_ready()); - host.request_now_ns = 50; - flow.notify_paint_finished(50); - ASSERT_TRUE(host.last_present_request.request_present); - Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); - - auto consumed = flow.begin_present(55, returned); - ASSERT_TRUE(static_cast(consumed)); - EXPECT_TRUE(consumed.new_frame()); - EXPECT_TRUE(consumed.request_was_pending()); - EXPECT_FALSE(pipeline.middle_frame_ready()); -} - -TEST(Renderive_Frame_Pipeline, HoverUpdatesDoNotRequireExternalRepaint) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - first.lease.reset(); - - ASSERT_TRUE(publish_pipeline_frame(pipeline, 2, 40).request_present); - auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); - ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); - auto repeated = pipeline.acquire_paint_frame(45); - ASSERT_TRUE(static_cast(repeated.lease)); - EXPECT_TRUE(repeated.present_request_was_pending); - EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - repeated.lease.reset(); - Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); - - auto hover_publish = publish_pipeline_frame(pipeline, 3, 60); - ASSERT_TRUE(hover_publish.request_present); - EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - - auto consumed = pipeline.acquire_paint_frame(65); - ASSERT_TRUE(static_cast(consumed.lease)); - EXPECT_TRUE(consumed.has_new_frame); - EXPECT_TRUE(consumed.has_valid_frame); - EXPECT_TRUE(consumed.present_request_was_pending); - EXPECT_EQ(consumed.lease.frame->version.load(std::memory_order_acquire), 3u); - EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); -} - -TEST(Renderive_Frame_Pipeline, PaintingLeaseBlocksUntilAvailable) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - first.lease.reset(); - - auto busy_painting = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Painting); - ASSERT_EQ(busy_painting.result, Triple_Buffer_Result::Success); - std::atomic_bool returned{false}; - std::atomic_bool lease_ok{false}; - std::atomic_bool underrun_seen{false}; - std::thread waiter([&]() { - auto consumed = pipeline.acquire_paint_frame(40); - lease_ok.store(static_cast(consumed.lease), std::memory_order_release); - underrun_seen.store(consumed.underrun, std::memory_order_release); - returned.store(true, std::memory_order_release); - }); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(returned.load(std::memory_order_acquire)); - Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_painting.lease); - waiter.join(); - EXPECT_TRUE(returned.load(std::memory_order_acquire)); - EXPECT_TRUE(lease_ok.load(std::memory_order_acquire)); - EXPECT_FALSE(underrun_seen.load(std::memory_order_acquire)); -} - -TEST(Renderive_Frame_Pipeline, TransientPaintBusyDoesNotCountUnderrun) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - first.lease.reset(); - auto repeated = pipeline.acquire_paint_frame(35); - EXPECT_TRUE(static_cast(repeated.lease)); - EXPECT_FALSE(repeated.underrun); - EXPECT_FALSE(repeated.paint_frame_unavailable); -} - -TEST(Renderive_Frame_Pipeline, RepeatedFrameDoesNotRequestBackgroundFallback) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - ASSERT_FALSE(first.lease.frame->image.empty()); - first.lease.reset(); - auto repeated = pipeline.acquire_paint_frame(35); - ASSERT_TRUE(static_cast(repeated.lease)); - EXPECT_FALSE(repeated.has_new_frame); - EXPECT_FALSE(repeated.lease.frame->image.empty()); - EXPECT_FALSE(repeated.underrun); -} - -TEST(Renderive_Frame_Pipeline, BeforeFirstPublishReturnsEmpty) { - Low_Latency_Frame_Pipeline pipeline; - auto consumed = pipeline.acquire_paint_frame(10); - EXPECT_FALSE(static_cast(consumed.lease)); - EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::has_published_frame(pipeline)); - EXPECT_TRUE(consumed.underrun); -} - -TEST(Renderive_Frame_Pipeline, FirstPublishCreatesPersistentPainting) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::has_published_frame(pipeline)); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - EXPECT_TRUE(first.has_new_frame); - EXPECT_TRUE(first.has_valid_frame); - EXPECT_EQ(first.lease.frame->version.load(std::memory_order_acquire), 1u); - EXPECT_FALSE(first.lease.frame->image.empty()); -} - -TEST(Renderive_Frame_Pipeline, NoNewMiddleReturnsSamePainting) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - Low_Latency_Frame* first_frame = first.lease.frame; - auto first_version = first_frame->version.load(std::memory_order_acquire); - first.lease.reset(); - - auto repeated = pipeline.acquire_paint_frame(35); - ASSERT_TRUE(static_cast(repeated.lease)); - EXPECT_EQ(repeated.lease.frame, first_frame); - EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); - EXPECT_FALSE(repeated.lease.frame->image.empty()); -} - -TEST(Renderive_Frame_Pipeline, RenderFailureKeepsPreviousPainting) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - Low_Latency_Frame* first_frame = first.lease.frame; - auto first_version = first_frame->version.load(std::memory_order_acquire); - first.lease.reset(); - - auto render_lease = pipeline.try_acquire_render_frame(); - ASSERT_TRUE(static_cast(render_lease)); - render_lease.reset(); - auto repeated = pipeline.acquire_paint_frame(40); - ASSERT_TRUE(static_cast(repeated.lease)); - EXPECT_EQ(repeated.lease.frame, first_frame); - EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); - EXPECT_FALSE(repeated.lease.frame->image.empty()); -} - -TEST(Renderive_Frame_Pipeline, PublishFailureKeepsPreviousPainting) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - Low_Latency_Frame* first_frame = first.lease.frame; - auto first_version = first_frame->version.load(std::memory_order_acquire); - first.lease.reset(); - - auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); - ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); - auto render_lease = pipeline.try_acquire_render_frame(); - ASSERT_TRUE(static_cast(render_lease)); - render_lease.frame->metadata->frame_id = 2; - render_lease.frame->metadata->render_begin_ns = 35; - render_lease.frame->metadata->render_end_ns = 38; - render_lease.frame->image.resize(2, 2); - render_lease.frame->image.fill(Color::red()); - render_lease.frame->version.store(2, std::memory_order_release); - auto failed_publish = pipeline.try_publish_rendered_frame(std::move(render_lease), 40); - EXPECT_FALSE(failed_publish.request_present); - EXPECT_NE(failed_publish.dropped_frame_record, nullptr); - Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); - - auto repeated = pipeline.acquire_paint_frame(45); - ASSERT_TRUE(static_cast(repeated.lease)); - EXPECT_EQ(repeated.lease.frame, first_frame); - EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); - EXPECT_FALSE(repeated.lease.frame->image.empty()); -} - -TEST(Renderive_Frame_Pipeline, IncompleteFrameDoesNotReplacePainting) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - Low_Latency_Frame* first_frame = first.lease.frame; - auto first_version = first_frame->version.load(std::memory_order_acquire); - first.lease.reset(); - - auto render_lease = pipeline.try_acquire_render_frame(); - ASSERT_TRUE(static_cast(render_lease)); - render_lease.frame->metadata->frame_id = 2; - render_lease.frame->metadata->render_begin_ns = 35; - render_lease.frame->metadata->render_end_ns = 38; - render_lease.frame->image.resize(2, 2); - render_lease.frame->image.fill(Color::red()); - render_lease.frame->render_complete.store(false, std::memory_order_release); - render_lease.frame->version.store(2, std::memory_order_release); - auto failed_publish = pipeline.try_publish_rendered_frame(std::move(render_lease), 40); - EXPECT_FALSE(failed_publish.request_present); - EXPECT_NE(failed_publish.dropped_frame_record, nullptr); - - auto repeated = pipeline.acquire_paint_frame(45); - ASSERT_TRUE(static_cast(repeated.lease)); - EXPECT_FALSE(repeated.has_new_frame); - EXPECT_EQ(repeated.lease.frame, first_frame); - EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); -} - -TEST(Renderive_Frame_Pipeline, RepeatedPresentNeverReturnsEmpty) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - auto first_version = first.lease.frame->version.load(std::memory_order_acquire); - first.lease.reset(); - - for (int i = 0; i < 1000; ++i) { - auto repeated = pipeline.acquire_paint_frame(31 + static_cast(i)); - ASSERT_TRUE(static_cast(repeated.lease)); - EXPECT_FALSE(repeated.lease.frame->image.empty()); - EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); - EXPECT_FALSE(repeated.underrun); - } -} - -TEST(Renderive_Frame_Pipeline, RequestPresentDoesNotLeaseBusyMiddle) { - Low_Latency_Frame_Pipeline pipeline; - auto published = publish_pipeline_frame(pipeline, 1, 25); - ASSERT_TRUE(published.request_present); - Low_Latency_Frame_Pipeline_Test_Probe::clear_paint_request(pipeline); - Low_Latency_Frame* middle = pipeline.frame_by_role(Frame_Middle); - ASSERT_TRUE(middle); - ASSERT_TRUE(middle->metadata); - middle->metadata->update_request_id = 0; - middle->metadata->update_request_time_ns = 0; - - auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); - ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); - auto posted = pipeline.request_present(40); - EXPECT_TRUE(posted.request_present); - EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); - EXPECT_EQ(middle->metadata->update_request_id, 0u); - EXPECT_EQ(middle->metadata->update_request_time_ns, 0u); - Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); -} -TEST(Renderive_Render_Model, NeutralSnapshotAndOutputTypesAreWired) { - static_assert(std::is_default_constructible_v); - static_assert(std::is_default_constructible_v); - static_assert(std::is_default_constructible_v); - static_assert(std::is_base_of_v); - static_assert(std::is_move_constructible_v); - static_assert(!std::is_copy_constructible_v); - static_assert(!std::is_same_v); - static_assert(sizeof(Present_Surface_Lease) <= 160); - - Low_Latency_Frame frame; - Render_Output& output = frame; - EXPECT_TRUE(output.metadata); - EXPECT_TRUE(output.presented_update_states.empty()); -} -TEST(Renderive_MPMCQueue, UsesTryOnlyBoundedCapacity) { - rigtorp::MPMCQueue queue(2); - EXPECT_TRUE(queue.try_push(1)); - EXPECT_TRUE(queue.try_push(2)); - EXPECT_FALSE(queue.try_push(3)); - int value = 0; - EXPECT_TRUE(queue.try_pop(value)); - EXPECT_EQ(value, 1); - EXPECT_TRUE(queue.try_pop(value)); - EXPECT_EQ(value, 2); - EXPECT_FALSE(queue.try_pop(value)); -} -TEST(Renderive_Curve_Sampling, AllSamplesKeepsSourcePointCount) { - Axis_Mapping_2D mapping{ - Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.0, 4.0}, Range{0.0, 100.0}}, - Axis_Frame_Snapshot{Orientation::Vertical, Range{0.0, 10.0}, Range{100.0, 0.0}} - }; - std::array samples{0.0, 1.0, 2.0, 3.0, 4.0}; - auto points = Curve_Utils::build_resampled_points(mapping, Range{0.0, 4.0}, 5, 5, false, Line_Interpolation_Mode::Linear_Value, [&samples](int i) { - return samples[static_cast(i)]; - }); - ASSERT_EQ(points.size(), samples.size()); - EXPECT_DOUBLE_EQ(points.front().x, 0.0); - EXPECT_DOUBLE_EQ(points.back().x, 100.0); -} -TEST(Renderive_Curve_Sampling, AllSamplesAddsVisibleBoundaryPoints) { - Axis_Mapping_2D mapping{ - Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.5, 3.5}, Range{0.0, 90.0}}, - Axis_Frame_Snapshot{Orientation::Vertical, Range{0.0, 40.0}, Range{0.0, 40.0}} - }; - std::array samples{0.0, 10.0, 20.0, 30.0, 40.0}; - auto points = Curve_Utils::build_resampled_points(mapping, Range{0.0, 4.0}, 5, 5, true, Line_Interpolation_Mode::Linear_Value, [&samples](int i) { - return samples[static_cast(i)]; - }); - ASSERT_EQ(points.size(), 5u); - EXPECT_DOUBLE_EQ(mapping.domain.pixel_to_coord(points.front().x), 0.5); - EXPECT_DOUBLE_EQ(points.front().y, 5.0); - EXPECT_DOUBLE_EQ(mapping.domain.pixel_to_coord(points.back().x), 3.5); - EXPECT_DOUBLE_EQ(points.back().y, 35.0); -} -TEST(Renderive_Curve_Sampling, AllSamplesUsesInterpolationModeForBoundaryPoints) { - Axis_Mapping_2D mapping{ - Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.5, 1.5}, Range{0.0, 100.0}}, - Axis_Frame_Snapshot{Orientation::Vertical, Range{0.0, 20.0}, Range{0.0, 20.0}} - }; - std::array samples{0.0, 10.0, 20.0}; - auto points = Curve_Utils::build_resampled_points(mapping, Range{0.0, 2.0}, 3, 3, true, Line_Interpolation_Mode::Step_Left, [&samples](int i) { - return samples[static_cast(i)]; - }); - ASSERT_EQ(points.size(), 4u); - EXPECT_DOUBLE_EQ(points.front().y, 0.0); - EXPECT_DOUBLE_EQ(points[1].y, 0.0); - EXPECT_DOUBLE_EQ(points[2].y, 10.0); - EXPECT_DOUBLE_EQ(points[1].x, points[2].x); - EXPECT_DOUBLE_EQ(points.back().y, 10.0); -} -TEST(Renderive_Curve_Sampling, PixelEnvelopeUsesPhysicalPixelBuckets) { - Axis_Mapping_2D mapping{ - Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.0, 5.0}, Range{10.1, 10.9}}, - Axis_Frame_Snapshot{Orientation::Vertical, Range{-100.0, 100.0}, Range{-100.0, 100.0}} - }; - std::array samples{0.0, 100.0, -100.0, 80.0, -80.0, 0.0}; - auto points = Curve_Utils::build_pixel_envelope_points(mapping, Range{0.0, 5.0}, 6, 6, false, [&samples](int i) { - return samples[static_cast(i)]; - }); - EXPECT_LE(points.size(), 4u); -} -TEST(Renderive_Curve_Sampling, PixelEnvelopeSkipsNonFiniteSamples) { - Axis_Mapping_2D mapping{ - Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.0, 4.0}, Range{0.0, 4.0}}, - Axis_Frame_Snapshot{Orientation::Vertical, Range{-1.0, 1.0}, Range{-1.0, 1.0}} - }; - std::array samples{0.0, std::numeric_limits::quiet_NaN(), 1.0, std::numeric_limits::infinity(), -1.0}; - auto points = Curve_Utils::build_pixel_envelope_points(mapping, Range{0.0, 4.0}, 5, 5, false, [&samples](int i) { - return samples[static_cast(i)]; - }); - ASSERT_FALSE(points.empty()); - for (const PointF& point : points) { - EXPECT_TRUE(std::isfinite(point.x)); - EXPECT_TRUE(std::isfinite(point.y)); - } -} -TEST(Renderive_Curve_Sampling, ReverseAxisEnvelopeKeepsSourceOrderInsideBucket) { - Axis_Mapping_2D mapping{ - Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.0, 4.0}, Range{1.0, 0.0}}, - Axis_Frame_Snapshot{Orientation::Vertical, Range{-20.0, 20.0}, Range{20.0, 0.0}} - }; - std::array samples{0.0, 10.0, -10.0, 20.0, -20.0}; - auto points = Curve_Utils::build_pixel_envelope_points(mapping, Range{0.0, 4.0}, 5, 5, false, [&samples](int i) { - return samples[static_cast(i)]; - }); - ASSERT_FALSE(points.empty()); - double previous = -1.0; - for (const PointF& point : points) { - double coord = mapping.domain.pixel_to_coord(point.x); - EXPECT_GE(coord, previous); - previous = coord; - } -} -TEST(Renderive_Frame_Feedback, AlternatingRenderPaintJitterDoesNotFlap) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(1000.0); - std::uint64_t base = 0; - for (std::uint64_t i = 0; i < 40; ++i, base += 20000000) { - std::uint64_t render_ns = i % 2 ? 10000000 : 11000000; - std::uint64_t paint_ns = i % 2 ? 11000000 : 10000000; - feed_feedback_frame(controller, i + 1, base, render_ns, paint_ns); - } - EXPECT_NE(controller.state().bottleneck, Low_Latency_Bottleneck_State::Consumer_Limited); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); -} - -TEST(Renderive_Frame_Feedback, TransientPaintSpikeDoesNotChangeStrategy) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(1000.0); - std::uint64_t base = 0; - for (std::uint64_t i = 0; i < 24; ++i, base += 20000000) - feed_feedback_frame(controller, i + 1, base, 10000000, 5000000); - ASSERT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); - auto spike = feedback_frame(100, base, 5000000, 60000000, 20000000, 20000000); - controller.on_render_completed(spike); - controller.on_frame_presented(spike); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); - EXPECT_NE(controller.state().bottleneck, Low_Latency_Bottleneck_State::Consumer_Limited); -} - -TEST(Renderive_Frame_Feedback, ConsumerLimitedRequiresHighWindowConfidence) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(1000.0); - std::uint64_t base = 0; - for (std::uint64_t i = 0; i < 16; ++i, base += 25000000) - feed_backlogged_feedback_frame(controller, i + 1, base, 5000000, 5000000, 5000000, 5000000); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); - EXPECT_EQ(controller.state().consumer_confidence, Consumer_Confidence::High); - - feed_backlogged_feedback_frame(controller, 17, base, 5000000, 5000000, 5000000, 5000000); - EXPECT_EQ(controller.state().bottleneck, Low_Latency_Bottleneck_State::Consumer_Limited); - EXPECT_EQ(controller.state().source, Refresh_Limit_Source::Paint); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); - EXPECT_EQ(controller.state().consumer_confidence, Consumer_Confidence::High); - EXPECT_EQ(controller.state().limit_reason, Low_Latency_Limit_Reason::Consumer_Capacity); -} - -TEST(Renderive_Frame_Feedback, ConsumerRecoveryUsesExitHysteresis) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - std::uint64_t base = 0; - for (std::uint64_t i = 0; i < 18; ++i, base += 50000000) - feed_backlogged_feedback_frame(controller, i + 1, base, 5000000, 5000000, 5000000, 5000000); - ASSERT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); - - for (std::uint64_t i = 0; i < 23; ++i, base += 12000000) - feed_feedback_frame(controller, 100 + i, base, 1000000, 1000000); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); - - feed_feedback_frame(controller, 200, base, 1000000, 1000000); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); - EXPECT_NE(controller.state().bottleneck, Low_Latency_Bottleneck_State::Consumer_Limited); -} - -TEST(Renderive_Frame_Feedback, QueueWaitDoesNotChangePaintService) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(1000.0); - auto frame = feedback_frame(1, 0, 5000000, 5000000); - frame.present_queue_wait_ns = 50000000; - controller.on_render_completed(frame); - controller.on_frame_presented(frame); - EXPECT_LT(controller.state().paint_period_ns, 10000000u); -} - -TEST(Renderive_Frame_Feedback, InputLimitedIsNotReportedAsRenderLimited) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(1000.0); - for (std::uint64_t i = 1; i <= 24; ++i) - controller.on_input_changed(i * 30000000, i, Dirty_Reason::Core_Data); - std::uint64_t base = 800000000; - for (std::uint64_t i = 0; i < 24; ++i, base += 30000000) - feed_feedback_frame(controller, i + 1, base, 5000000, 5000000); - EXPECT_EQ(controller.state().source, Refresh_Limit_Source::Input); - EXPECT_EQ(controller.state().bottleneck, Low_Latency_Bottleneck_State::Input_Limited); -} - -TEST(Renderive_Frame_Feedback, DeadlineRemainsMonotonicAcrossCandidateChanges) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - Frame_Lifecycle_Record frame; - frame.render_role_enter_ns = 100000000; - controller.on_render_started(frame, 100000000); - std::uint64_t first_deadline = controller.next_render_time_ns(); - ASSERT_GT(first_deadline, 100000000u); - - controller.set_max_render_fps(1000.0); - EXPECT_GE(controller.next_render_time_ns(), first_deadline); - std::uint64_t base = 120000000; - for (std::uint64_t i = 0; i < 8; ++i, base += 20000000) - feed_feedback_frame(controller, i + 1, base, i % 2 ? 30000000 : 5000000, i % 2 ? 5000000 : 30000000, 1000000, 1000000); - EXPECT_GE(controller.next_render_time_ns(), first_deadline); -} - -TEST(Renderive_Frame_Feedback, ConsumerSampleAcceptedOnlyWithContinuousBacklog) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); - feed_backlogged_feedback_frame(controller, 2, 50000000, 2000000, 1000000); - EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Accepted); - EXPECT_EQ(controller.state().consumer_sample_accepted_count, 1u); - - feed_feedback_frame(controller, 3, 100000000, 2000000, 1000000); - EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Rejected_No_Backlog); - EXPECT_EQ(controller.state().consumer_sample_rejected_no_backlog_count, 1u); -} - -TEST(Renderive_Frame_Feedback, AdmissionDeadlineWaitIsNotProducerStarvation) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); - feed_backlogged_feedback_frame(controller, 2, 50000000, 2000000, 1000000); - std::uint64_t accepted = controller.state().consumer_sample_accepted_count; - - auto frame = feedback_frame(3, 100000000, 2000000, 1000000); - controller.note_middle_published(frame.transition_12_ns); - controller.on_render_completed(frame); - controller.note_admission_wait(20000000, frame.transition_12_ns + 1); - controller.on_frame_presented(frame); - - EXPECT_EQ(controller.state().consumer_sample_accepted_count, accepted + 1); - EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Accepted); - EXPECT_EQ(controller.state().producer_starvation_count, 0u); - EXPECT_FALSE(controller.state().producer_starvation_active); -} - -TEST(Renderive_Frame_Feedback, ConsumerSampleRejectedWhenNoNewFrame) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - auto frame = feedback_frame(1, 100000000, 2000000, 1000000); - controller.note_middle_published(frame.transition_12_ns); - controller.on_render_completed(frame); - controller.on_frame_presented(frame); - controller.note_middle_published(frame.transition_12_ns + 1000); - frame.paint_begin_ns += 50000000; - frame.paint_end_ns = frame.paint_begin_ns + 1000000; - controller.on_frame_presented(frame); - EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Rejected_No_New_Frame); -} - -TEST(Renderive_Frame_Feedback, LowConfidenceFallsBackToUserPeriod) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); - feed_backlogged_feedback_frame(controller, 2, 100000000, 2000000, 1000000); - EXPECT_EQ(controller.state().consumer_confidence, Consumer_Confidence::Low); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); - EXPECT_EQ(controller.state().effective_min_render_interval_ns, controller.state().user_period_ns); -} - -TEST(Renderive_Frame_Feedback, EffectiveIntervalNeverExceedsFourUserPeriods) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - std::uint64_t base = 0; - for (std::uint64_t i = 0; i < 18; ++i, base += 200000000) - feed_backlogged_feedback_frame(controller, i + 1, base, 2000000, 1000000); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); - EXPECT_GE(controller.state().effective_min_render_interval_ns, controller.state().user_period_ns); - EXPECT_LE(controller.state().effective_min_render_interval_ns, controller.state().user_period_ns * 4); -} - -TEST(Renderive_Frame_Feedback, EffectiveIntervalRisesAtBoundedRate) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - std::uint64_t base = 0; - for (std::uint64_t i = 0; i < 17; ++i, base += 80000000) - feed_backlogged_feedback_frame(controller, i + 1, base, 2000000, 1000000); - EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); - EXPECT_LE(controller.state().effective_min_render_interval_ns, static_cast(static_cast(controller.state().user_period_ns) * 1.10) + 1); -} - -TEST(Renderive_Frame_Feedback, UpdateToPaintLatencyMeasuredSeparately) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 2000000, 0, 10000000); - EXPECT_GT(controller.state().update_to_paint_begin_ns, controller.state().paint_service_ns); - EXPECT_LT(controller.state().paint_service_ns, 5000000u); -} - -TEST(Renderive_Frame_Feedback, DiagnosticsCountAcceptedAndRejectedSamples) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); - feed_backlogged_feedback_frame(controller, 2, 50000000, 2000000, 1000000); - feed_feedback_frame(controller, 3, 100000000, 2000000, 1000000); - auto diagnostics = controller.performance_diagnostics(); - EXPECT_EQ(diagnostics.consumer_sample_accepted_count, 1u); - EXPECT_EQ(diagnostics.consumer_sample_rejected_no_backlog_count, 1u); -} - -TEST(Renderive_Frame_Feedback, RawDisplayIntervalUpdatesForRejectedSample) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - feed_feedback_frame(controller, 1, 0, 2000000, 1000000); - feed_feedback_frame(controller, 2, 50000000, 2000000, 1000000); - EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Rejected_No_Backlog); - EXPECT_GT(controller.state().raw_display_interval_current_ns, 0u); - EXPECT_EQ(controller.state().accepted_consumer_interval_current_ns, 0u); -} - -TEST(Renderive_Frame_Feedback, AcceptedConsumerIntervalUpdatesOnlyForAcceptedSample) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); - EXPECT_EQ(controller.state().accepted_consumer_interval_current_ns, 0u); - feed_backlogged_feedback_frame(controller, 2, 50000000, 2000000, 1000000); - EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Accepted); - EXPECT_GT(controller.state().raw_display_interval_current_ns, 0u); - EXPECT_GT(controller.state().accepted_consumer_interval_current_ns, 0u); -} - -TEST(Renderive_Frame_Feedback, OldRejectedSamplesDoNotLockConfidenceForever) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - std::uint64_t base = 0; - for (std::uint64_t i = 0; i < 96; ++i, base += 20000000) - feed_feedback_frame(controller, i + 1, base, 2000000, 1000000); - ASSERT_EQ(controller.state().consumer_confidence, Consumer_Confidence::Low); - for (std::uint64_t i = 0; i < 64; ++i, base += 50000000) - feed_backlogged_feedback_frame(controller, 200 + i, base, 2000000, 1000000, 1000000, 1000000); - EXPECT_EQ(controller.state().consumer_confidence, Consumer_Confidence::High); - EXPECT_EQ(controller.state().consumer_window_size, 64u); - EXPECT_GE(controller.state().consumer_window_accepted_ratio, 0.75); -} - -TEST(Renderive_Frame_Feedback, ProducerStarvationCountsStateTransitionOnce) { - Low_Latency_Feedback_Controller controller; - controller.set_max_render_fps(60.0); - auto frame = feedback_frame(1, 100000000, 2000000, 1000000); - controller.on_render_completed(frame); - controller.note_middle_published(frame.transition_12_ns); - std::uint64_t deadline = controller.next_render_time_ns(); - ASSERT_GT(deadline, 0u); - controller.observe_runtime(deadline + 100000000, false, true, false); - controller.observe_runtime(deadline + 200000000, false, true, false); - controller.observe_runtime(deadline + 300000000, false, true, false); - ASSERT_TRUE(controller.state().producer_starvation_active); - EXPECT_EQ(controller.state().producer_starvation_count, 1u); - controller.observe_runtime(deadline + 400000000, false, true, false); - controller.observe_runtime(deadline + 500000000, false, true, false); - EXPECT_EQ(controller.state().producer_starvation_count, 1u); -} -TEST(Renderive_Frame_Feedback, ProducerIntervalUsesActualRenderStart) { - Low_Latency_Feedback_Controller controller; - auto first = feedback_frame(1, 100000000, 2000000, 1000000); - auto second = feedback_frame(2, 116666667, 2000000, 1000000); - controller.on_render_completed(first); - controller.on_render_completed(second); - EXPECT_NEAR(static_cast(controller.state().producer_interval_current_ns), 16666667.0, 1.0); -} -TEST(Renderive_Frame_Feedback, AdmissionWaitRecordsActualElapsedTimeAndClears) { - Low_Latency_Feedback_Controller controller; - controller.note_admission_wait(5000000, 100000000); - EXPECT_EQ(controller.state().admission_wait_remaining_ns, 5000000u); - controller.note_render_admitted(104000000); - EXPECT_EQ(controller.state().admission_wait_elapsed_ns, 4000000u); - EXPECT_EQ(controller.state().admission_wait_remaining_ns, 0u); - controller.note_admission_wait(5000000, 200000000); - controller.cancel_admission_wait(201000000); - EXPECT_EQ(controller.state().admission_wait_elapsed_ns, 0u); - EXPECT_EQ(controller.state().admission_wait_remaining_ns, 0u); -} - -TEST(Renderive_Frame_Flow, LatencyEagerRendersWhileMiddleIsPending) { - Low_Latency_Frame_Flow flow; - Flow_Test_Host host; - host.snapshot.dirty = true; - host.snapshot.middle_has_new_frame = true; - flow.attach(host); - - flow.activate_view(20); - - EXPECT_EQ(host.paint_requests, 1); - EXPECT_EQ(host.render_attempts, 1); -} -TEST(Renderive_Frame_Flow, PendingPresentRequestSuppressesDuplicateRequest) { - Low_Latency_Frame_Flow flow; - auto published = publish_low_latency_flow_frame(flow, 1, 20); - ASSERT_TRUE(published.request_present); - Flow_Test_Host host; - host.snapshot.middle_has_new_frame = true; - flow.attach(host); - flow.activate_view(30); - EXPECT_EQ(host.paint_requests, 0); -} - -TEST(Renderive_Frame_Flow, LowLatencyEmitsFeedbackEventsThroughHost) { - Low_Latency_Frame_Flow flow; - Flow_Test_Host host; - host.snapshot.dirty = true; - flow.attach(host); - flow.notify_model_dirty(Dirty_Reason::Core_Data, 10); - flow.activate_view(20); - EXPECT_NE(std::find(host.feedback_events.begin(), host.feedback_events.end(), "InputChanged"), host.feedback_events.end()); - EXPECT_NE(std::find(host.feedback_events.begin(), host.feedback_events.end(), "RenderAdmitted"), host.feedback_events.end()); - EXPECT_EQ(std::find(host.feedback_events.begin(), host.feedback_events.end(), "RenderStarted"), host.feedback_events.end()); - Frame_Lifecycle_Record frame; - frame.render_begin_ns = 30; - frame.render_end_ns = 40; - flow.notify_render_finished(frame, 40); - EXPECT_NE(std::find(host.feedback_events.begin(), host.feedback_events.end(), "RenderStarted"), host.feedback_events.end()); - EXPECT_NE(std::find(host.feedback_events.begin(), host.feedback_events.end(), "RenderCompleted"), host.feedback_events.end()); -} - -TEST(Renderive_Frame_Pipeline, NewRenderSupersedesOldMiddle) { - Low_Latency_Frame_Pipeline pipeline; - ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); - auto first = pipeline.acquire_paint_frame(30); - ASSERT_TRUE(static_cast(first.lease)); - first.lease.reset(); - - auto second = publish_pipeline_frame(pipeline, 2, 40); - ASSERT_TRUE(second.middle_published); - auto third = publish_pipeline_frame(pipeline, 3, 50); - - EXPECT_TRUE(third.middle_published); - ASSERT_NE(third.superseded_frame_record, nullptr); - EXPECT_EQ(third.superseded_frame_record->frame_id, 2u); - EXPECT_TRUE(third.duplicate_present_request_suppressed); -} - -TEST(Renderive_Frame_Pipeline, OnlyOneUpdateRemainsPosted) { - Low_Latency_Frame_Pipeline pipeline; - auto first = publish_pipeline_frame(pipeline, 1, 25); - ASSERT_TRUE(first.request_present); - ASSERT_TRUE(first.update_posted); - - auto repeated = pipeline.request_present(30); - EXPECT_FALSE(repeated.request_present); - EXPECT_TRUE(repeated.duplicate_present_request_suppressed); - EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); -} -TEST(Renderive_Frame_Flow, LowLatencyOwnsActiveState) { - Low_Latency_Frame_Flow flow; - Flow_Test_Host host; - host.snapshot.dirty = true; - flow.attach(host); - - flow.notify_model_dirty(10); - EXPECT_FALSE(flow.view_active()); - EXPECT_EQ(host.render_attempts, 0); - - flow.activate_view(20); - EXPECT_TRUE(flow.view_active()); - EXPECT_EQ(host.render_attempts, 1); - - host.snapshot.render_job_active = false; - flow.deactivate_view(); - flow.notify_model_dirty(30); - EXPECT_FALSE(flow.view_active()); - EXPECT_EQ(host.render_attempts, 1); - EXPECT_GT(host.deadline_cancels, 0); - - Flow_Diagnostics diagnostics = flow.diagnostics(); - EXPECT_TRUE(std::holds_alternative(diagnostics.strategy)); - EXPECT_FALSE(diagnostics.common.active); -} -TEST(Renderive_Frame_Flow, ExplicitRequestDoesNotNeedActiveView) { - Explicit_Frame_Flow flow; - Flow_Test_Host host; - host.snapshot.dirty = true; - flow.attach(host); - - auto ticket = flow.request_render(); - flow.notify_model_dirty(10); - - EXPECT_TRUE(static_cast(ticket)); - EXPECT_FALSE(flow.view_active()); - EXPECT_EQ(host.render_attempts, 1); - - Flow_Diagnostics diagnostics = flow.diagnostics(); - EXPECT_TRUE(std::holds_alternative(diagnostics.strategy)); -} -TEST(Renderive_Frame_Flow, ExplicitTicketsDrainIntoAcceptedFrame) { - Explicit_Frame_Flow flow; - Flow_Test_Host host; - flow.attach(host); - - auto ticket = flow.request_render(); - std::pmr::vector> states(memory_resource(Memory_Domain::Update_Completion)); - flow.drain_frame_update_states(states); - - ASSERT_TRUE(static_cast(ticket)); - ASSERT_EQ(states.size(), 1u); - EXPECT_EQ(ticket.id(), states.front()->id()); - complete_update_states(states, Update_Stage::Presented); - EXPECT_TRUE(ticket.is_complete()); -} - -TEST(Renderive_Plot_Core, FrameViewCaptureFailureMarksTreeIncomplete) { - Plot_Core plot(std::make_unique()); - plot.init(); - plot.set_viewport_size(Size{16, 16}); - auto root = plot.root_renderable(); - ASSERT_TRUE(root); - auto blocker = std::make_shared(); - blocker->init(root); - auto held_view = blocker->d()->capture_frame_view(); - ASSERT_TRUE(static_cast(held_view)); - auto blocked_view = blocker->d()->capture_frame_view(); - ASSERT_FALSE(static_cast(blocked_view)); - - Renderable_Frame_Tree blocked_tree; - root->append_tree_snapshot(blocked_tree); - EXPECT_TRUE(std::any_of(blocked_tree.begin(), blocked_tree.end(), [](const Renderable_Frame_Node& node) { - return node.frame_view_required && !node.frame_view; - })); - - held_view = {}; - Renderable_Frame_Tree recovered_tree; - root->append_tree_snapshot(recovered_tree); - EXPECT_FALSE(std::any_of(recovered_tree.begin(), recovered_tree.end(), [](const Renderable_Frame_Node& node) { - return node.frame_view_required && !node.frame_view; - })); -} - -TEST(Renderive_Explicit_Present, BeforeFirstRenderReturnsEmpty) { - Explicit_Frame_Flow flow; - std::shared_ptr returned; - auto lease = flow.begin_present(10, returned); - EXPECT_FALSE(static_cast(lease)); - EXPECT_EQ(returned, nullptr); -} - -TEST(Renderive_Explicit_Present, PublishedOutputRemainsPresentable) { - Explicit_Frame_Flow flow; - ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present); - std::shared_ptr returned; - auto lease = flow.begin_present(30, returned); - ASSERT_TRUE(static_cast(lease)); - EXPECT_FALSE(lease.image().empty()); - (void)flow.end_present(std::move(lease), 31, 32); -} - -TEST(Renderive_Explicit_Present, RepeatedPresentReturnsSameOutput) { - Explicit_Frame_Flow flow; - ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present); - std::shared_ptr returned; - auto first = flow.begin_present(30, returned); - ASSERT_TRUE(static_cast(first)); - Render_Output* first_output = Present_Surface_Lease_Access::output(first); - (void)flow.end_present(std::move(first), 31, 32); - - auto repeated = flow.begin_present(40, returned); - ASSERT_TRUE(static_cast(repeated)); - EXPECT_EQ(Present_Surface_Lease_Access::output(repeated), first_output); - EXPECT_FALSE(repeated.image().empty()); - (void)flow.end_present(std::move(repeated), 41, 42); -} - -TEST(Renderive_Explicit_Present, ActivePresentLeaseDoesNotHideOutput) { - Explicit_Frame_Flow flow; - ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present); - std::shared_ptr returned; - auto first = flow.begin_present(30, returned); - ASSERT_TRUE(static_cast(first)); - auto second = flow.begin_present(31, returned); - ASSERT_TRUE(static_cast(second)); - EXPECT_EQ(Present_Surface_Lease_Access::output(first), Present_Surface_Lease_Access::output(second)); - EXPECT_FALSE(first.image().empty()); - EXPECT_FALSE(second.image().empty()); - (void)flow.end_present(std::move(second), 32, 33); - (void)flow.end_present(std::move(first), 34, 35); -} - -TEST(Renderive_Explicit_Present, FailedRenderKeepsPreviousOutput) { - Explicit_Frame_Flow flow; - ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present); - std::shared_ptr returned; - auto first = flow.begin_present(30, returned); - ASSERT_TRUE(static_cast(first)); - Render_Output* first_output = Present_Surface_Lease_Access::output(first); - (void)flow.end_present(std::move(first), 31, 32); - - auto failed_surface = flow.begin_render_frame(); - ASSERT_TRUE(static_cast(failed_surface)); - flow.discard_render_frame(std::move(failed_surface)); - - auto repeated = flow.begin_present(40, returned); - ASSERT_TRUE(static_cast(repeated)); - EXPECT_EQ(Present_Surface_Lease_Access::output(repeated), first_output); - EXPECT_FALSE(repeated.image().empty()); - (void)flow.end_present(std::move(repeated), 41, 42); -} - -TEST(Renderive_Explicit_Present, IncompleteFrameDoesNotReplaceOutput) { - Explicit_Frame_Flow flow; - ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present); - std::shared_ptr returned; - auto first = flow.begin_present(30, returned); - ASSERT_TRUE(static_cast(first)); - Render_Output* first_output = Present_Surface_Lease_Access::output(first); - (void)flow.end_present(std::move(first), 31, 32); - - auto incomplete = flow.begin_render_frame(); - ASSERT_TRUE(static_cast(incomplete)); - incomplete.output->metadata->frame_id = 2; - incomplete.output->metadata->render_begin_ns = 35; - incomplete.output->metadata->render_end_ns = 38; - incomplete.output->image.resize(3, 3); - incomplete.output->image.fill(Color::red()); - incomplete.output->render_complete.store(false, std::memory_order_release); - auto failed_publish = flow.publish_rendered_frame(std::move(incomplete), 40); - EXPECT_FALSE(failed_publish.request_present); - EXPECT_NE(failed_publish.dropped_frame_record, nullptr); - - auto repeated = flow.begin_present(45, returned); - ASSERT_TRUE(static_cast(repeated)); - EXPECT_EQ(Present_Surface_Lease_Access::output(repeated), first_output); - EXPECT_FALSE(repeated.new_frame()); - EXPECT_FALSE(repeated.image().empty()); - (void)flow.end_present(std::move(repeated), 46, 47); -} - -TEST(Renderive_Explicit_Present, NewOutputAtomicallyReplacesPreviousOutput) { - Explicit_Frame_Flow flow; - ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present); - std::shared_ptr returned; - auto old_lease = flow.begin_present(30, returned); - ASSERT_TRUE(static_cast(old_lease)); - Render_Output* old_output = Present_Surface_Lease_Access::output(old_lease); - - ASSERT_TRUE(publish_explicit_frame(flow, 2, 50, 3, 3, Color::red()).request_present); - auto new_lease = flow.begin_present(55, returned); - ASSERT_TRUE(static_cast(new_lease)); - Render_Output* new_output = Present_Surface_Lease_Access::output(new_lease); - EXPECT_NE(new_output, old_output); - EXPECT_FALSE(old_lease.image().empty()); - EXPECT_FALSE(new_lease.image().empty()); - EXPECT_EQ(new_lease.image().width, 3); - EXPECT_EQ(new_lease.image().height, 3); - (void)flow.end_present(std::move(new_lease), 56, 57); - (void)flow.end_present(std::move(old_lease), 58, 59); -} - -TEST(Renderive_Render_Executor, ExecutorDoesNotOwnPlotAdmission) { - Render_Executor executor(Render_Runtime_Config{1}); - std::atomic_bool entered{false}; - std::atomic_bool release{false}; - std::atomic_int completed{0}; - auto first_stat = std::make_shared(); - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 1, - 1, - Render_Task_Kind::Frame, - Task([&]() { - entered.store(true, std::memory_order_release); - while (!release.load(std::memory_order_acquire)) - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - }), - {}, - Task([&]() { - completed.fetch_add(1, std::memory_order_acq_rel); - }), - first_stat, - true - })); - ASSERT_TRUE(wait_until([&]() { - return entered.load(std::memory_order_acquire); - })); - auto second_stat = std::make_shared(); - EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ - 2, - 1, - Render_Task_Kind::Frame, - Task([]() {}), - {}, - Task([&]() { - completed.fetch_add(1, std::memory_order_acq_rel); - }), - second_stat, - true - })); - auto third_stat = std::make_shared(); - EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ - 3, - 1, - Render_Task_Kind::Frame, - Task([]() {}), - {}, - Task([&]() { - completed.fetch_add(1, std::memory_order_acq_rel); - }), - third_stat, - true - })); - auto duplicate_stat = std::make_shared(); - EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ - 2, - 2, - Render_Task_Kind::Frame, - Task([]() {}), - {}, - Task([&]() { - completed.fetch_add(1, std::memory_order_acq_rel); - }), - duplicate_stat, - true - })); - auto overflow_stat = std::make_shared(); - EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ - 4, - 1, - Render_Task_Kind::Frame, - Task([]() {}), - {}, - Task([&]() { - completed.fetch_add(1, std::memory_order_acq_rel); - }), - overflow_stat, - true - })); - release.store(true, std::memory_order_release); - ASSERT_TRUE(wait_until([&]() { - return completed.load(std::memory_order_acquire) == 5; - })); - executor.shutdown(); -} -TEST(Renderive_Render_Executor, TaskflowTopologyRunsInFrameOrder) { - Render_Executor executor(Render_Runtime_Config{2}); - std::atomic_int sequence{0}; - std::atomic_bool completed{false}; - auto stat = std::make_shared(); - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 1, - 1, - Render_Task_Kind::Frame, - Task{}, - [&sequence](Render_Task_Graph& graph, Render_Task_Graph_Node entry, Render_Task_Graph_Node exit) { - auto prepare = graph.emplace(Render_Task_Kind::Frame, Task([&sequence]() { - int expected = 0; - sequence.compare_exchange_strong(expected, 1, std::memory_order_acq_rel); - })); - auto draw = graph.emplace(Render_Task_Kind::Primitive, Task([&sequence]() { - int expected = 1; - sequence.compare_exchange_strong(expected, 2, std::memory_order_acq_rel); - })); - auto finalize = graph.emplace(Render_Task_Kind::Frame, Task([&sequence]() { - int expected = 2; - sequence.compare_exchange_strong(expected, 3, std::memory_order_acq_rel); - })); - graph.precede(entry, prepare); - graph.precede(prepare, draw); - graph.precede(draw, finalize); - graph.precede(finalize, exit); - }, - Task([&completed]() { - completed.store(true, std::memory_order_release); - }), - stat, - true - })); - ASSERT_TRUE(wait_until([&]() { - return completed.load(std::memory_order_acquire); - })); - EXPECT_EQ(sequence.load(std::memory_order_acquire), 3); - EXPECT_EQ(stat->task_count, 3u); - executor.shutdown(); -} -TEST(Renderive_Render_Executor, FrameDrawWaitsForPrepareSubflow) { - Render_Executor executor(Render_Runtime_Config{2}); - std::latch child_entered(1); - std::latch release_child(1); - std::latch completed(1); - std::atomic_bool child_finished{false}; - std::atomic_bool draw_started{false}; - auto stat = std::make_shared(); - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 1, - 1, - Render_Task_Kind::Frame, - Task{}, - [&](Render_Task_Graph& graph, Render_Task_Graph_Node entry, Render_Task_Graph_Node exit) { - auto prepare = graph.emplace_subflow(Render_Task_Kind::Frame, [&](Render_Task_Subflow& subflow) { - auto child = subflow.emplace(Render_Task_Kind::Curve, Task([&]() { - child_entered.count_down(); - release_child.wait(); - child_finished.store(true, std::memory_order_release); - })); - auto merge = subflow.emplace(Render_Task_Kind::Compose, Task([&]() { - EXPECT_TRUE(child_finished.load(std::memory_order_acquire)); - })); - subflow.precede(child, merge); - }); - auto draw = graph.emplace(Render_Task_Kind::Primitive, Task([&]() { - draw_started.store(true, std::memory_order_release); - EXPECT_TRUE(child_finished.load(std::memory_order_acquire)); - })); - graph.precede(entry, prepare); - graph.precede(prepare, draw); - graph.precede(draw, exit); - }, - Task([&]() { - completed.count_down(); - }), - stat, - true - })); - child_entered.wait(); - EXPECT_FALSE(draw_started.load(std::memory_order_acquire)); - release_child.count_down(); - completed.wait(); - EXPECT_TRUE(draw_started.load(std::memory_order_acquire)); - EXPECT_GE(stat->task_count, 3u); - EXPECT_GE(stat->peak_parallelism, 1u); - executor.shutdown(); -} -TEST(Renderive_Render_Executor, SubflowKindCountersReflectChildren) { - Render_Executor executor(Render_Runtime_Config{4}); - std::latch completed(1); - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 1, - 1, - Render_Task_Kind::Frame, - Task{}, - [](Render_Task_Graph& graph, Render_Task_Graph_Node entry, Render_Task_Graph_Node exit) { - auto prepare = graph.emplace_subflow(Render_Task_Kind::Frame, [](Render_Task_Subflow& subflow) { - auto curve0 = subflow.emplace(Render_Task_Kind::Curve, Task([]() {})); - auto curve1 = subflow.emplace(Render_Task_Kind::Curve, Task([]() {})); - auto waterfall0 = subflow.emplace(Render_Task_Kind::Waterfall, Task([]() {})); - auto waterfall1 = subflow.emplace(Render_Task_Kind::Waterfall, Task([]() {})); - auto compose = subflow.emplace(Render_Task_Kind::Compose, Task([]() {})); - subflow.precede(curve0, compose); - subflow.precede(curve1, compose); - subflow.precede(waterfall0, compose); - subflow.precede(waterfall1, compose); - }); - auto draw = graph.emplace(Render_Task_Kind::Primitive, Task([]() {})); - auto finalize = graph.emplace(Render_Task_Kind::Frame, Task([]() {})); - graph.precede(entry, prepare); - graph.precede(prepare, draw); - graph.precede(draw, finalize); - graph.precede(finalize, exit); - }, - Task([&]() { - completed.count_down(); - }), - std::make_shared(), - true - })); - completed.wait(); - executor.shutdown(); - Render_Executor_Snapshot snapshot = executor.snapshot(); - EXPECT_GE(snapshot.curve_tasks, 2u); - EXPECT_GE(snapshot.waterfall_tasks, 2u); - EXPECT_GE(snapshot.compose_tasks, 1u); - EXPECT_GE(snapshot.primitive_tasks, 3u); -} -TEST(Renderive_Render_Executor, SubmittedFramesAllCompleteOnSingleWorker) { - Render_Executor executor(Render_Runtime_Config{1}); - std::atomic_bool first_entered{false}; - std::atomic_bool release_first{false}; - std::atomic_int order_index{0}; - std::array order{}; - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 1, - 1, - Render_Task_Kind::Frame, - Task([&]() { - first_entered.store(true, std::memory_order_release); - while (!release_first.load(std::memory_order_acquire)) - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 1; - }), - {}, - Task([]() {}), - std::make_shared(), - true - })); - ASSERT_TRUE(wait_until([&]() { - return first_entered.load(std::memory_order_acquire); - })); - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 2, - 1, - Render_Task_Kind::Frame, - Task([&]() { - order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 2; - }), - {}, - Task([]() {}), - std::make_shared(), - true - })); - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 3, - 1, - Render_Task_Kind::Frame, - Task([&]() { - order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 3; - }), - {}, - Task([]() {}), - std::make_shared(), - true - })); - release_first.store(true, std::memory_order_release); - ASSERT_TRUE(wait_until([&]() { - return order_index.load(std::memory_order_acquire) == 3; - })); - EXPECT_EQ(order_index.load(std::memory_order_acquire), 3); - executor.shutdown(); -} -TEST(Renderive_Render_Executor, ShutdownWaitsForAcceptedQueuedFrame) { - Render_Executor executor(Render_Runtime_Config{1}); - std::atomic_bool first_entered{false}; - std::atomic_bool release_first{false}; - std::atomic_bool queued_work_ran{false}; - std::atomic_bool queued_completed{false}; - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 1, - 1, - Render_Task_Kind::Frame, - Task([&]() { - first_entered.store(true, std::memory_order_release); - while (!release_first.load(std::memory_order_acquire)) - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - }), - {}, - Task([]() {}), - std::make_shared(), - true - })); - ASSERT_TRUE(wait_until([&]() { - return first_entered.load(std::memory_order_acquire); - })); - ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ - 2, - 1, - Render_Task_Kind::Frame, - Task([&]() { - queued_work_ran.store(true, std::memory_order_release); - }), - {}, - Task([&]() { - queued_completed.store(true, std::memory_order_release); - }), - std::make_shared(), - true - })); - std::thread shutdown_thread([&]() { - executor.shutdown(); - }); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_FALSE(queued_completed.load(std::memory_order_acquire)); - release_first.store(true, std::memory_order_release); - shutdown_thread.join(); - EXPECT_TRUE(queued_work_ran.load(std::memory_order_acquire)); - EXPECT_TRUE(queued_completed.load(std::memory_order_acquire)); -} -TEST(Renderive_Performance_Shower, ToggleDoesNotReenterRenderableTreeUnsafely) { - Plot_Core plot(std::make_unique()); - plot.init(); - auto root = plot.root_renderable(); - ASSERT_TRUE(root); - EXPECT_TRUE(root->children_snapshot().empty()); - set_performance_overlay_enabled(plot, true); - EXPECT_TRUE(performance_overlay_enabled(plot)); - EXPECT_TRUE(root->children_snapshot().empty()); - set_performance_overlay_enabled(plot, false); - EXPECT_FALSE(performance_overlay_enabled(plot)); - EXPECT_TRUE(root->children_snapshot().empty()); -} -TEST(Renderive_Performance_Shower, TogglePreservesAttachedOptions) { - Plot_Core plot(std::make_unique()); - plot.init(); - Performance_Overlay_Options options; - options.font.size = 17.0; - options.background = Color{10, 20, 30, 255}; - options.foreground = Color{240, 240, 240, 255}; - auto original = attach_performance_overlay(plot, options); - ASSERT_TRUE(original); - set_performance_overlay_enabled(plot, true); - EXPECT_TRUE(performance_overlay_enabled(plot)); - set_performance_overlay_enabled(plot, false); - EXPECT_FALSE(performance_overlay_enabled(plot)); - set_performance_overlay_enabled(plot, true); - auto toggled = performance_overlay(plot); - ASSERT_EQ(toggled, original); - EXPECT_EQ(toggled->options().font.size, options.font.size); - EXPECT_EQ(toggled->options().background, options.background); - EXPECT_EQ(toggled->options().foreground, options.foreground); -} -TEST(Renderive_Hover_Tooltip, DefaultTextPenContrastsWithDefaultBackground) { - Hover_Tooltip_State state; - EXPECT_EQ(state.tooltip_background_brush.color, Color::white()); - EXPECT_EQ(state.tooltip_text_pen.color, Color::black()); -} -TEST(Renderive_Performance_Overlay, MetricUpdatesDoNotRequestIndependentRepaint) { - auto state = std::make_shared(); - state->enabled.store(true, std::memory_order_release); - std::atomic_int update_count{}; - state->update_callback = [&update_count]() { - update_count.fetch_add(1, std::memory_order_relaxed); - }; - Performance_Frame_Record frame = performance_frame(1); - ASSERT_TRUE(state->metric_queue.try_push(frame)); - schedule_performance_metrics_worker(state); - ASSERT_TRUE(wait_until([&]() { - return state->published_snapshot.load(std::memory_order_acquire) && !state->worker_active.load(std::memory_order_acquire); - })); - EXPECT_EQ(update_count.load(std::memory_order_acquire), 0); - state->snapshot_rebuild_requested.store(true, std::memory_order_release); - schedule_performance_metrics_worker(state, true); - ASSERT_TRUE(wait_until([&]() { - return update_count.load(std::memory_order_acquire) == 1 && !state->worker_active.load(std::memory_order_acquire); - })); - state->cancelled.store(true, std::memory_order_release); -} -TEST(Renderive_Performance_Overlay, WorkerConsumesSharedFrameRecord) { - auto state = std::make_shared(); - state->enabled.store(true, std::memory_order_release); - auto frame = std::make_shared(); - frame->frame_id = 7; - frame->input_version = 3; - frame->render_begin_ns = 10; - frame->render_end_ns = 20; - frame->transition_12_ns = 25; - frame->paint_begin_ns = 30; - frame->paint_end_ns = 40; - frame->transition_32_ns = 45; - frame->pixel_width = 320; - frame->pixel_height = 240; - frame->outcome = Frame_Outcome::Presented; - Performance_Frame_Record record = frame; - ASSERT_TRUE(state->metric_queue.try_push(record)); - schedule_performance_metrics_worker(state); - auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); - std::shared_ptr snapshot; - while (std::chrono::steady_clock::now() < deadline) { - snapshot = state->published_snapshot.load(std::memory_order_acquire); - if (snapshot && !state->worker_active.load(std::memory_order_acquire)) - break; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - state->cancelled.store(true, std::memory_order_release); - ASSERT_TRUE(snapshot); - EXPECT_FALSE(snapshot->lines.empty()); - EXPECT_TRUE(state->aggregate.has_frame); - EXPECT_EQ(state->aggregate.latest.frame_id, 7); -} -TEST(Renderive_Performance_Overlay, LowLatencyDiagnosticsExposePresentFeedbackFields) { - Performance_Overlay shower; - shower.set_enabled(true); - shower.set_viewport(Size{360, 180}, 1); - auto frame = performance_frame(10, 360, 180); - frame->refresh.production_mode = Low_Latency_Production_Mode::Consumer_Paced; - frame->refresh.limit_reason = Low_Latency_Limit_Reason::Consumer_Capacity; - frame->refresh.consumer_confidence = Consumer_Confidence::High; - frame->refresh.consumer_sample_accepted_count = 8; - frame->refresh.consumer_sample_total_count = 10; - frame->refresh.consumer_sample_accepted_ratio = 0.8; - frame->refresh.update_to_paint_begin_ns = 1200000; - frame->refresh.paint_service_ns = 800000; - frame->refresh.producer_starvation_count = 2; - frame->refresh.middle_superseded_count = 3; - frame->refresh.update_posted_count = 4; - frame->refresh.duplicate_present_request_suppressed_count = 1; - frame->refresh.update_ack_count = 3; - frame->refresh.update_repost_count = 1; - shower.collect_frame(frame); - - ASSERT_TRUE(wait_until([&]() { - auto snapshot = shower.display_snapshot(); - return snapshot && !snapshot->lines.empty(); - })); - auto snapshot = shower.display_snapshot(); - ASSERT_TRUE(snapshot); - std::string text; - for (const auto& line : snapshot->lines) - text += line + "\n"; - EXPECT_NE(text.find("limit_reason:consumer_capacity"), std::string::npos); - EXPECT_NE(text.find("consumer_confidence:high"), std::string::npos); - EXPECT_NE(text.find("update_to_paint"), std::string::npos); - EXPECT_NE(text.find("producer_starvation"), std::string::npos); - EXPECT_NE(text.find("middle_superseded"), std::string::npos); - EXPECT_NE(text.find("duplicate_suppressed:"), std::string::npos); -} - -TEST(Renderive_Performance_Overlay, PerformanceLogDisabledWritesNothing) { - performance_log_service().shutdown(); - auto directory = unique_test_directory("perf_log_disabled"); - Performance_Overlay shower; - Performance_Overlay_Options options; - options.log.enabled = false; - options.log.directory = directory.string(); - options.log.session_name = "disabled"; - shower.set_options(options); - shower.set_enabled(false); - shower.collect_frame(performance_frame(1)); - performance_log_service().shutdown(); - EXPECT_FALSE(std::filesystem::exists(directory / "disabled_frames.log")); - EXPECT_FALSE(std::filesystem::exists(directory / "disabled_feedback.log")); - EXPECT_FALSE(std::filesystem::exists(directory / "disabled_meta.json")); - std::filesystem::remove_all(directory); -} - -TEST(Renderive_Performance_Overlay, PerformanceLogWorksWhenOverlayHidden) { - performance_log_service().shutdown(); - auto directory = unique_test_directory("perf_log_hidden"); - Performance_Overlay shower; - Performance_Overlay_Options options; - options.log.enabled = true; - options.log.directory = directory.string(); - options.log.session_name = "hidden"; - options.log.flush_interval_ms = 1; - shower.set_log_identity(Performance_Log_Plot_Identity{42, "hidden,plot\"a"}); - shower.set_options(options); - shower.set_enabled(false); - - auto frame = performance_frame(7); - frame->dirty_reason_mask = dirty_reason_bit(Dirty_Reason::Core_Data); - frame->refresh.update_posted_count = 1; - frame->refresh.consumer_window_size = 1; - frame->refresh.consumer_window_accepted = 1; - frame->refresh.consumer_window_accepted_ratio = 1.0; - shower.collect_frame(frame); - auto event = std::make_shared(feedback_event("InputChanged", *frame, 1000)); - shower.collect_feedback_event(event); - performance_log_service().shutdown(); - - auto frame_records = read_log_records(directory / "hidden_frames.log", "frame"); - auto feedback_records = read_log_records(directory / "hidden_feedback.log", "feedback"); - ASSERT_EQ(frame_records.size(), 1u); - ASSERT_EQ(feedback_records.size(), 1u); - const auto& frame_record = frame_records.front(); - EXPECT_EQ(frame_record.at("schema_version"), "3"); - EXPECT_EQ(frame_record.at("plot_id"), "42"); - EXPECT_EQ(frame_record.at("plot_name"), "hidden,plot\"a"); - EXPECT_TRUE(frame_record.contains("render_slot_idle_wait_ns")); - EXPECT_TRUE(frame_record.contains("executor_queue_wait_current_ns")); - EXPECT_TRUE(frame_record.contains("executor_started_topologies")); - EXPECT_TRUE(frame_record.contains("executor_completed_topologies")); - EXPECT_TRUE(frame_record.contains("render_request_to_start_ns")); - EXPECT_TRUE(frame_record.contains("duplicate_present_request_suppressed")); - EXPECT_TRUE(std::filesystem::exists(directory / "hidden_meta.json")); - std::filesystem::remove_all(directory); -} - -TEST(Renderive_Performance_Overlay, PerformanceLogSeparatesMultiplePlots) { - performance_log_service().shutdown(); - auto directory = unique_test_directory("perf_log_multi_plot"); - Performance_Overlay_Options options; - options.log.enabled = true; - options.log.directory = directory.string(); - options.log.session_name = "multi"; - options.log.flush_interval_ms = 1; - - Performance_Overlay first; - Performance_Overlay second; - first.set_log_identity(Performance_Log_Plot_Identity{11, "spectrum"}); - second.set_log_identity(Performance_Log_Plot_Identity{12, "waterfall"}); - first.set_options(options); - second.set_options(options); - first.set_enabled(false); - second.set_enabled(false); - first.collect_frame(performance_frame(1)); - second.collect_frame(performance_frame(2)); - performance_log_service().shutdown(); - - auto records = read_log_records(directory / "multi_frames.log", "frame"); - ASSERT_EQ(records.size(), 2u); - bool saw_spectrum = false; - bool saw_waterfall = false; - for (const auto& record : records) { - saw_spectrum = saw_spectrum || (record.at("plot_id") == "11" && record.at("plot_name") == "spectrum"); - saw_waterfall = saw_waterfall || (record.at("plot_id") == "12" && record.at("plot_name") == "waterfall"); - } - EXPECT_TRUE(saw_spectrum); - EXPECT_TRUE(saw_waterfall); - std::filesystem::remove_all(directory); -} - -TEST(Renderive_Performance_Overlay, FeedbackEventLogUsesRuntimeEventRecord) { - performance_log_service().shutdown(); - auto directory = unique_test_directory("perf_feedback_event"); - Performance_Overlay shower; - Performance_Overlay_Options options; - options.log.enabled = true; - options.log.directory = directory.string(); - options.log.session_name = "feedback"; - options.log.flush_interval_ms = 1; - shower.set_log_identity(Performance_Log_Plot_Identity{91, "event_plot"}); - shower.set_options(options); - shower.set_enabled(false); - - auto frame = performance_frame(9); - frame->refresh.production_mode = Low_Latency_Production_Mode::Consumer_Paced; - frame->refresh.effective_min_render_interval_ns = 33333333; - auto event = std::make_shared(feedback_event("ProducerStarvationEnter", *frame, 2000)); - event->mode_before = Low_Latency_Production_Mode::Latency_Eager; - event->mode_after = Low_Latency_Production_Mode::Consumer_Paced; - event->effective_interval_before_ns = 16666667; - event->effective_interval_after_ns = 33333333; - event->dirty = true; - event->producer_starved = true; - shower.collect_feedback_event(event); - performance_log_service().shutdown(); - - auto records = read_log_records(directory / "feedback_feedback.log", "feedback"); - ASSERT_EQ(records.size(), 1u); - const auto& record = records.front(); - EXPECT_EQ(record.at("event"), "ProducerStarvationEnter"); - EXPECT_EQ(record.at("mode_before"), "latency_eager"); - EXPECT_EQ(record.at("mode_after"), "consumer_paced"); - EXPECT_EQ(record.at("dirty"), "1"); - EXPECT_EQ(record.at("producer_starved"), "1"); - EXPECT_EQ(record.at("effective_interval_before_ns"), "16666667"); - EXPECT_EQ(record.at("effective_interval_after_ns"), "33333333"); - std::filesystem::remove_all(directory); -} - -TEST(Renderive_Performance_Overlay, SummaryUsesSingleColumnAndTextRoles) { - Performance_Overlay shower; - Performance_Overlay_Options options; - options.section_color = Color{10, 20, 30, 255}; - options.label_color = Color{40, 50, 60, 255}; - options.value_color = Color{70, 80, 90, 255}; - options.warning_color = Color{200, 20, 20, 255}; - shower.set_options(options); - shower.set_enabled(true); - shower.set_viewport(Size{1280, 260}, 1); - auto frame = performance_frame(1, 1280, 260); - frame->refresh.producer_starvation_active = true; - frame->refresh.producer_starvation_count = 1; - frame->refresh.consumer_confidence = Consumer_Confidence::Low; - frame->refresh.production_mode = Low_Latency_Production_Mode::Consumer_Paced; - shower.collect_frame(frame); - - ASSERT_TRUE(wait_until([&]() { - auto snapshot = shower.display_snapshot(); - return snapshot && !snapshot->text_runs.empty(); - })); - auto snapshot = shower.display_snapshot(); - ASSERT_TRUE(snapshot); - bool has_section = false; - bool has_label = false; - bool has_value = false; - bool has_warning = false; - for (const auto& run : snapshot->text_runs) { - EXPECT_EQ(run.column_index, 0); - has_section = has_section || run.role == Performance_Text_Role::Section; - has_label = has_label || run.role == Performance_Text_Role::Label; - has_value = has_value || run.role == Performance_Text_Role::Value; - has_warning = has_warning || run.role == Performance_Text_Role::Warning; - } - EXPECT_TRUE(has_section); - EXPECT_TRUE(has_label); - EXPECT_TRUE(has_value); - EXPECT_TRUE(has_warning); -} - -TEST(Renderive_Performance_Overlay, LayoutFallsBackToOneColumn) { - Performance_Overlay shower; - shower.set_enabled(true); - shower.set_viewport(Size{260, 220}, 1); - shower.collect_frame(performance_frame(1, 260, 220)); - ASSERT_TRUE(wait_until([&]() { - auto snapshot = shower.display_snapshot(); - return snapshot && !snapshot->text_runs.empty(); - })); - auto snapshot = shower.display_snapshot(); - ASSERT_TRUE(snapshot); - for (const auto& run : snapshot->text_runs) - EXPECT_EQ(run.column_index, 0); -} -TEST(Renderive_Performance_Shower, OptionsAffectSnapshotRendering) { - Performance_Overlay shower; - Performance_Overlay_Options options; - options.font.size = 18.0; - options.background = Color{10, 20, 30, 255}; - options.foreground = Color{240, 240, 240, 255}; - shower.set_options(options); - shower.set_enabled(true); - shower.set_viewport(Size{260, 120}, 1); - shower.collect_frame(performance_frame(1, 260, 120)); - ASSERT_TRUE(wait_until([&]() { - auto snapshot = shower.display_snapshot(); - return snapshot && !snapshot->image.empty(); - })); - auto snapshot = shower.display_snapshot(); - ASSERT_TRUE(snapshot); - ASSERT_FALSE(snapshot->image.empty()); - EXPECT_EQ(shower.options().font.size, 18.0); - EXPECT_GT(snapshot->line_height, 12.0); - EXPECT_EQ(premultiplied_to_color(snapshot->image.row(1)[1]), options.background); -} -TEST(Renderive_Performance_Shower, OutcomeWidthIsSeededWithLongestEnumText) { - auto state = std::make_shared(); - state->enabled.store(true, std::memory_order_release); - auto outcome_width = [&]() { - return state->field_layout_states[static_cast(Performance_Metric_Id::Outcome)].max_value_width; - }; - auto presented = performance_frame(1, 260, 120); - presented->outcome = Frame_Outcome::Presented; - ASSERT_TRUE(state->metric_queue.try_push(Performance_Frame_Record{presented})); - schedule_performance_metrics_worker(state); - ASSERT_TRUE(wait_until([&]() { - return state->aggregate.latest.frame_id == 1 && !state->worker_active.load(std::memory_order_acquire); - })); - double seeded_width = outcome_width(); - EXPECT_GT(seeded_width, 0.0); - auto dropped = performance_frame(2, 260, 120); - dropped->outcome = Frame_Outcome::Worker_Budget_Dropped; - ASSERT_TRUE(state->metric_queue.try_push(Performance_Frame_Record{dropped})); - schedule_performance_metrics_worker(state); - ASSERT_TRUE(wait_until([&]() { - return state->aggregate.latest.frame_id == 2 && !state->worker_active.load(std::memory_order_acquire); - })); - EXPECT_EQ(outcome_width(), seeded_width); - state->cancelled.store(true, std::memory_order_release); -} -TEST(Renderive_Performance_Shower, NumericFieldWidthIsMonotonicAcrossResetAndResize) { - auto state = std::make_shared(); - state->enabled.store(true, std::memory_order_release); - auto frame_width = [&]() { - return state->field_layout_states[static_cast(Performance_Metric_Id::Frame_Id)].max_value_width; - }; - double previous = 0.0; - double wide = 0.0; - for (std::uint64_t frame_id : {1ull, 99ull, 123456ull, 2ull, 8ull}) { - Performance_Frame_Record record = performance_frame(frame_id, 320, 120); - ASSERT_TRUE(state->metric_queue.try_push(record)); - schedule_performance_metrics_worker(state); - ASSERT_TRUE(wait_until([&]() { - auto snapshot = state->published_snapshot.load(std::memory_order_acquire); - return snapshot && state->aggregate.latest.frame_id == frame_id && !state->worker_active.load(std::memory_order_acquire); - })); - double current = frame_width(); - EXPECT_GE(current, previous); - previous = current; - if (frame_id == 123456) - wide = current; - if (frame_id == 2 || frame_id == 8) - EXPECT_EQ(current, wide); - } - state->reset_requested.store(true, std::memory_order_release); - schedule_performance_metrics_worker(state); - ASSERT_TRUE(wait_until([&]() { - return !state->worker_active.load(std::memory_order_acquire); - })); - EXPECT_EQ(frame_width(), wide); - state->viewport_width.store(180, std::memory_order_release); - state->viewport_height.store(90, std::memory_order_release); - state->viewport_version.store(2, std::memory_order_release); - state->snapshot_rebuild_requested.store(true, std::memory_order_release); - schedule_performance_metrics_worker(state); - ASSERT_TRUE(wait_until([&]() { - auto snapshot = state->published_snapshot.load(std::memory_order_acquire); - return snapshot && snapshot->viewport_version == 2 && !state->worker_active.load(std::memory_order_acquire); - })); - EXPECT_EQ(frame_width(), wide); - state->cancelled.store(true, std::memory_order_release); -} -TEST(Renderive_Performance_Shower, ScrollCommandsUpdateOffsetAndClamp) { - Performance_Overlay shower; - shower.set_enabled(true); - shower.set_viewport(Size{220, 70}, 1); - auto frame = performance_frame(1, 220, 70); - shower.collect_frame(frame); - ASSERT_TRUE(wait_until([&]() { - auto snapshot = shower.display_snapshot(); - return snapshot && snapshot->max_scroll_offset > 0.0; - })); - auto snapshot = shower.display_snapshot(); - ASSERT_TRUE(snapshot); - EXPECT_TRUE(shower.handle_wheel(PointF{5.0, 5.0}, -120.0, 0.0)); - ASSERT_TRUE(wait_until([&]() { - auto next = shower.display_snapshot(); - return next && next->scroll_offset > snapshot->scroll_offset; - })); - snapshot = shower.display_snapshot(); - ASSERT_TRUE(snapshot); - PointF page_down{snapshot->scroll_track_rect.x + 1.0, snapshot->scroll_thumb_rect.bottom() + 1.0}; - EXPECT_TRUE(shower.handle_pointer_press(page_down)); - ASSERT_TRUE(wait_until([&]() { - auto next = shower.display_snapshot(); - return next && next->scroll_offset > snapshot->scroll_offset; - })); - snapshot = shower.display_snapshot(); - ASSERT_TRUE(snapshot); - PointF thumb_center{snapshot->scroll_thumb_rect.x + 1.0, snapshot->scroll_thumb_rect.y + snapshot->scroll_thumb_rect.height * 0.5}; - EXPECT_TRUE(shower.handle_pointer_press(thumb_center)); - EXPECT_TRUE(shower.handle_pointer_move(PointF{thumb_center.x, snapshot->scroll_track_rect.bottom()})); - EXPECT_TRUE(shower.handle_pointer_release(PointF{thumb_center.x, snapshot->scroll_track_rect.bottom()})); - ASSERT_TRUE(wait_until([&]() { - auto next = shower.display_snapshot(); - return next && next->max_scroll_offset > 0.0 && next->scroll_offset > next->max_scroll_offset * 0.8; - })); -} -TEST(Renderive_Performance_Shower, Utf8WrappingKeepsValidText) { - Performance_Overlay shower; - shower.set_enabled(true); - shower.set_viewport(Size{120, 90}, 1); - auto frame = performance_frame(1, 120, 90); - Renderable_Frame_Stat stat; - stat.object_name = "\xE4\xB8\xAD\xE6\x96\x87\xE6\xB5\x8B\xE8\xAF\x95\xE5\xAD\x97\xE6\xAE\xB5\xE5\xBE\x88\xE9\x95\xBF\xE5\xBE\x88\xE9\x95\xBF"; - stat.tree_depth = 0; - stat.render_count = 1; - stat.total_render_time_ns = 1000000; - frame->renderable_stats.push_back(stat); - shower.collect_frame(frame); - ASSERT_TRUE(wait_until([&]() { - auto snapshot = shower.display_snapshot(); - return snapshot && !snapshot->lines.empty(); - })); - auto snapshot = shower.display_snapshot(); - ASSERT_TRUE(snapshot); - for (const auto& line : snapshot->lines) - EXPECT_TRUE(valid_utf8(line)) << line; -} -} // namespace renderive -int main(int argc, char** argv) { - testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/third_party/datoviz.bash b/third_party/datoviz.bash new file mode 100644 index 0000000..46406a4 --- /dev/null +++ b/third_party/datoviz.bash @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +git clone --depth 1 --branch v0.4.0rc2 --no-recurse-submodules https://github.com/datoviz/datoviz.git +cd datoviz +git submodule update --init --recursive --depth 1 external/cglm external/mimalloc external/kvazaar external/glfw external/cimgui external/msdf-atlas-gen diff --git a/third_party/init.bash b/third_party/init.bash index 0e7dc8f..00df906 100644 --- a/third_party/init.bash +++ b/third_party/init.bash @@ -46,7 +46,5 @@ fetch_repo_ex() { fetch_repo "$repo" "third_party/${dir}" "$branch" } fetch_repo_ex "ae/build_infra" -fetch_repo_ex "ae/CPP_Core" fetch_repo_ex "psc/Structive" fetch_repo_ex "psc/Adminive" - diff --git a/web.cmake b/web.cmake deleted file mode 100644 index 70049b2..0000000 --- a/web.cmake +++ /dev/null @@ -1,53 +0,0 @@ -set(renderive_web_sources - "${root_dir}/web_server/Gallery_Plot_Session.cpp" - "${root_dir}/web_server/Gallery_Protocol.cpp" - "${root_dir}/web_server/Gallery_WebSocket_Controller.cpp" - "${root_dir}/web_server/Pixel_Frame.cpp" - "${root_dir}/web_server/Renderive_WebSocket_Controller.cpp" - "${root_dir}/web_server/Web_Event_Adapter.cpp" - "${root_dir}/web_server/Web_Performance_Log.cpp" - "${root_dir}/web_server/Web_Plot_Session.cpp" - "${root_dir}/web_server/Web_Server.cpp" -) - -add_library(Renderive_Web STATIC ${renderive_web_sources}) -target_include_directories(Renderive_Web PUBLIC "${root_dir}") -target_compile_features(Renderive_Web PUBLIC cxx_std_20) -target_compile_definitions(Renderive_Web PRIVATE NOMINMAX) -target_link_libraries(Renderive_Web PRIVATE - Renderive_render_2D - Renderive_Kernel - Adminive::Nlohmann - Drogon::Drogon - spdlog::spdlog -) - -add_executable(Renderive_Web_Server "${root_dir}/web_server/main.cpp") -target_compile_features(Renderive_Web_Server PRIVATE cxx_std_20) -target_link_libraries(Renderive_Web_Server PRIVATE Renderive_Web) -add_custom_target(Renderive_Web_Assets - # COMMAND "${CMAKE_COMMAND}" -E copy_directory - # "${root_dir}/webapp" - # "${CMAKE_CURRENT_BINARY_DIR}/webapp" - COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${root_dir}/webapp_gallery" - "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery" - COMMENT "Synchronizing Renderive Web assets" -) -add_dependencies(Renderive_Web_Server Renderive_Web_Assets) - -add_executable(Renderive_Web_Tests EXCLUDE_FROM_ALL - "${root_dir}/Kernel/tests/main.cpp" - "${root_dir}/web_server/tests/Web_Bridge_Tests.cpp" -) -target_link_libraries(Renderive_Web_Tests PRIVATE - Renderive_Web - Adminive::Nlohmann - GTest::GTest -) -add_test(NAME Renderive_Web_Tests COMMAND Renderive_Web_Tests) - -if (MSVC) - target_compile_options(Renderive_Web PRIVATE /utf-8) - target_compile_options(Renderive_Web_Server PRIVATE /utf-8) -endif () diff --git a/web_server/CMakeLists.txt b/web_server/CMakeLists.txt new file mode 100644 index 0000000..fffcb76 --- /dev/null +++ b/web_server/CMakeLists.txt @@ -0,0 +1,61 @@ +function(renderive_find_web_dependencies) + if(POLICY CMP0144) + cmake_policy(SET CMP0144 NEW) + endif() + rcl_load_dependency_environment(global::drogon) + find_package(Drogon CONFIG REQUIRED) + find_package(spdlog CONFIG REQUIRED) +endfunction() +renderive_find_web_dependencies() +if(NOT TARGET Adminive::Nlohmann) + set(renderive_saved_build_testing "${BUILD_TESTING}") + set(BUILD_TESTING OFF) + add_subdirectory( + "${CMAKE_CURRENT_LIST_DIR}/../third_party/Adminive" + "${CMAKE_CURRENT_BINARY_DIR}/third_party/Adminive" + EXCLUDE_FROM_ALL + ) + set(BUILD_TESTING "${renderive_saved_build_testing}") + unset(renderive_saved_build_testing) +endif() +add_library(Renderive_Web STATIC) +target_sources(Renderive_Web PRIVATE + Gallery_Plot_Session.cpp + Gallery_Protocol.cpp + Gallery_WebSocket_Controller.cpp + Pixel_Frame.cpp + Renderive_WebSocket_Controller.cpp + Web_Event_Adapter.cpp + Web_Performance_Log.cpp + Web_Plot_Session.cpp + Web_Server.cpp +) +target_include_directories(Renderive_Web PUBLIC + "$" +) +target_compile_features(Renderive_Web PUBLIC cxx_std_20) +target_compile_definitions(Renderive_Web PRIVATE NOMINMAX) +target_link_libraries(Renderive_Web + PUBLIC Renderive_render_2D Drogon::Drogon + PRIVATE Adminive::Nlohmann spdlog::spdlog +) +add_executable(Renderive_Web_Server main.cpp) +target_compile_features(Renderive_Web_Server PRIVATE cxx_std_20) +target_link_libraries(Renderive_Web_Server PRIVATE Renderive_Web) +add_custom_target(Renderive_Web_Assets + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${CMAKE_CURRENT_LIST_DIR}/../webapp_gallery" + "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery" + COMMENT "Synchronizing Renderive Web assets" +) +add_dependencies(Renderive_Web_Server Renderive_Web_Assets) +if(MSVC) + target_compile_options(Renderive_Web PRIVATE /utf-8) + target_compile_options(Renderive_Web_Server PRIVATE /utf-8) +endif() +if(RENDERIVE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) + add_executable(Renderive_Web_Tests tests/Web_Bridge_Tests.cpp) + target_link_libraries(Renderive_Web_Tests PRIVATE Renderive_Web Adminive::Nlohmann GTest::gtest_main) + add_test(NAME Renderive_Web_Tests COMMAND Renderive_Web_Tests) +endif() diff --git a/web_server/Gallery_Plot_Session.cpp b/web_server/Gallery_Plot_Session.cpp index 85cbad5..1b192b9 100644 --- a/web_server/Gallery_Plot_Session.cpp +++ b/web_server/Gallery_Plot_Session.cpp @@ -1756,8 +1756,7 @@ struct Gallery_Plot_Session::Impl { return false; } else if constexpr (std::is_same_v) { - return value.kind != Gallery_Request_Kind::Catalog && - value.kind != Gallery_Request_Kind::Observe; + return value.kind != Gallery_Request_Kind::Catalog; } else { return true; @@ -1804,15 +1803,14 @@ struct Gallery_Plot_Session::Impl { Web_Response_Type::Json, 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 (request.kind == Gallery_Request_Kind::Refresh) { + update_client_metrics(request.message); return Web_Response{ Web_Response_Type::Json, - Gallery_Protocol::observer_json( - scene->case_id(), scene->frame_mode(), scene->telemetry_json()) + Gallery_Protocol::case_json(scene->case_id(), scene->state(), + scene->telemetry_json(), + "后端状态已手动刷新", + scene->frame_mode()) }; } if (request.kind == Gallery_Request_Kind::Patch) { diff --git a/web_server/Gallery_Protocol.cpp b/web_server/Gallery_Protocol.cpp index 1a2a517..e2bc37c 100644 --- a/web_server/Gallery_Protocol.cpp +++ b/web_server/Gallery_Protocol.cpp @@ -522,6 +522,66 @@ Json value_json(const Gallery_Value& value) { return std::visit([](const auto& item) { return Json(item); }, value); } +Json control_amis_schema(const Control_Definition& definition) { + const auto& model = definition.model; + Json result{ + {"name", model.id}, + {"label", model.label}, + {"description", model.description.empty() ? model.api : model.description}, + {"remark", model.api}, + {"size", "md"} + }; + if (model.input == "boolean") { + result["type"] = "switch"; + return result; + } + if (model.input == "select") { + result["type"] = "select"; + result["options"] = Json::array(); + for (const auto& option : definition.options) + result["options"].push_back({{"label", option}, {"value", option}}); + return result; + } + if (model.input == "color") { + result["type"] = "input-color"; + result["validations"] = {{"matchRegexp", "^#[0-9A-Fa-f]{6}$"}, {"maxLength", 7}}; + result["validationErrors"] = {{"matchRegexp", "颜色必须使用 #RRGGBB"}, {"maxLength", "颜色必须使用 #RRGGBB"}}; + return result; + } + if (model.input == "number") { + result["type"] = "input-number"; + result["min"] = model.minimum; + result["max"] = model.maximum; + result["step"] = model.step; + result["validations"] = {{"minimum", model.minimum}, {"maximum", model.maximum}}; + result["validationErrors"] = { + {"minimum", "数值低于后端允许范围"}, + {"maximum", "数值超出后端允许范围"} + }; + if (model.integer) { + result["precision"] = 0; + result["validations"]["isInt"] = true; + result["validationErrors"]["isInt"] = "必须是整数"; + } + return result; + } + result["type"] = "input-text"; + result["validations"] = {{"maxLength", 160}}; + result["validationErrors"] = {{"maxLength", "文本长度不能超过 160 个字符"}}; + return result; +} + +Json action_argument_amis_schema(const Action_Definition& definition) { + const auto& model = definition.model; + if (model.argument_input.empty()) + return Json(); + return { + {"type", model.argument_input == "number" ? "input-number" : "input-text"}, + {"name", "action_" + model.id}, + {"label", model.argument_label.empty() ? "参数" : model.argument_label} + }; +} + std::optional parse_value(const Json& value, const Control_Definition& definition, std::string& error) { @@ -823,6 +883,7 @@ std::string Gallery_Protocol::case_json(std::string_view case_id, ? gallery_detail::value_json(definition.initial) : gallery_detail::value_json(found->second); item["options"] = definition.options; + item["amis"] = gallery_detail::control_amis_schema(definition); result["controls"]["data"].push_back(std::move(item)); } result["actions"] = { @@ -831,9 +892,12 @@ std::string Gallery_Protocol::case_json(std::string_view case_id, gallery_detail::action_view())}, {"data", Json::array()} }; - for (const auto& definition : gallery_detail::actions(case_id, frame_mode)) - result["actions"]["data"].push_back( - adminive::to_frontend_json(definition.model)); + for (const auto& definition : gallery_detail::actions(case_id, frame_mode)) { + Json item = adminive::to_frontend_json(definition.model); + if (!definition.model.argument_input.empty()) + item["argument_amis"] = gallery_detail::action_argument_amis_schema(definition); + result["actions"]["data"].push_back(std::move(item)); + } try { result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); } catch (const std::exception&) { @@ -863,21 +927,6 @@ std::string Gallery_Protocol::error_json(std::string_view message, return result.dump(); } -std::string Gallery_Protocol::observer_json(std::string_view case_id, - Gallery_Frame_Mode frame_mode, - std::string_view telemetry_json) { - Json result = protocol_base(); - result["type"] = "observer_state"; - result["case_id"] = case_id; - result["frame_mode"] = frame_mode_contract(frame_mode); - try { - result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); - } catch (const std::exception&) { - result["telemetry"] = Json::object(); - } - return result.dump(); -} - Gallery_Patch_Result Gallery_Protocol::apply_patch(std::string_view case_id, const Gallery_State& current, std::string_view message, diff --git a/web_server/Gallery_Protocol.h b/web_server/Gallery_Protocol.h index d28713d..1d225a6 100644 --- a/web_server/Gallery_Protocol.h +++ b/web_server/Gallery_Protocol.h @@ -57,10 +57,6 @@ public: [[nodiscard]] static std::string error_json( std::string_view message, std::string_view field = {}); - [[nodiscard]] static std::string observer_json( - std::string_view case_id, - Gallery_Frame_Mode frame_mode, - std::string_view telemetry_json); [[nodiscard]] static Gallery_Patch_Result apply_patch( std::string_view case_id, const Gallery_State& current, diff --git a/web_server/Web_Event.h b/web_server/Web_Event.h index 8c3dde8..95a9113 100644 --- a/web_server/Web_Event.h +++ b/web_server/Web_Event.h @@ -27,7 +27,7 @@ struct Set_Smoothing { bool enabled{}; }; struct Clear_Selection {}; -enum class Gallery_Request_Kind : std::uint8_t { Catalog, Open, Patch, Action, Observe }; +enum class Gallery_Request_Kind : std::uint8_t { Catalog, Open, Refresh, Patch, Action }; struct Gallery_Request { Gallery_Request_Kind kind = Gallery_Request_Kind::Catalog; std::string message; diff --git a/web_server/Web_Event_Adapter.cpp b/web_server/Web_Event_Adapter.cpp index 7de67eb..351ad58 100644 --- a/web_server/Web_Event_Adapter.cpp +++ b/web_server/Web_Event_Adapter.cpp @@ -211,12 +211,12 @@ std::optional Web_Event_Adapter::decode(std::string_view message) { return Gallery_Request{Gallery_Request_Kind::Catalog, std::string(message)}; if (type == "gallery_open") return Gallery_Request{Gallery_Request_Kind::Open, std::string(message)}; + if (type == "gallery_refresh") + return Gallery_Request{Gallery_Request_Kind::Refresh, std::string(message)}; if (type == "gallery_patch") return Gallery_Request{Gallery_Request_Kind::Patch, std::string(message)}; if (type == "gallery_action") return Gallery_Request{Gallery_Request_Kind::Action, std::string(message)}; - if (type == "gallery_observe") - return Gallery_Request{Gallery_Request_Kind::Observe, std::string(message)}; return std::nullopt; } diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 26fae37..ec8fa2b 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -52,10 +52,10 @@ nlohmann::json response_json(std::optional response) { return parse_json(response->payload); } -nlohmann::json observe_telemetry(Gallery_Plot_Session& session) { +nlohmann::json refresh_telemetry(Gallery_Plot_Session& session) { return response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})"))).value( + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh"})"))).value( "telemetry", nlohmann::json::object()); } @@ -88,6 +88,20 @@ bool wait_for_condition(Predicate&& predicate, return predicate(); } +std::optional wait_for_pixel_frame(Gallery_Plot_Session& session, + std::uint32_t width, + std::uint32_t height) { + std::optional frame; + wait_for_condition([&] { + frame = session.handle(Frame_Request{}); + return frame && frame->type == Web_Response_Type::Pixels && + frame->payload.size() >= pixel_frame_header_size && + read_u32_le(frame->payload, 4) == width && + read_u32_le(frame->payload, 8) == height; + }); + return frame; +} + void set_gallery_view_active(Gallery_Plot_Session& session, bool active) { const auto event = Web_Event_Adapter::decode(active ? R"({"category":"event","type":"show"})" : @@ -97,7 +111,7 @@ void set_gallery_view_active(Gallery_Plot_Session& session, bool active) { } std::uint64_t successful_render_count(Gallery_Plot_Session& session) { - return observe_telemetry(session).at("performance").at("successful_render_count") + return refresh_telemetry(session).at("performance").at("successful_render_count") .get(); } @@ -129,10 +143,10 @@ TEST(RenderiveWebBridge, DecodesOnlyEventMessages) { ASSERT_TRUE(open.has_value()); EXPECT_EQ(std::get(*open).kind, Gallery_Request_Kind::Open); - const auto observe = Web_Event_Adapter::decode( - R"({"category":"event","type":"gallery_observe"})"); - ASSERT_TRUE(observe.has_value()); - EXPECT_EQ(std::get(*observe).kind, Gallery_Request_Kind::Observe); + const auto refresh = Web_Event_Adapter::decode( + R"({"category":"event","type":"gallery_refresh"})"); + ASSERT_TRUE(refresh.has_value()); + EXPECT_EQ(std::get(*refresh).kind, Gallery_Request_Kind::Refresh); const auto bounded = Web_Event_Adapter::decode( R"({"category":"event","type":"resize","width":1e100,"height":-1e100})"); @@ -645,7 +659,7 @@ TEST(RenderiveWebGallery, SelectionOverlayPointerLifecycleCreatesAndClearsRegion EXPECT_FALSE(session.handle(*event).has_value()); } - EXPECT_EQ(observe_telemetry(session).at("selection_regions"), 1); + EXPECT_EQ(refresh_telemetry(session).at("selection_regions"), 1); EXPECT_EQ(invoke_action(session, "clear_selection").at("telemetry") .at("selection_regions"), 0); const auto rebound = invoke_action(session, "rebind_selection_axes").at("telemetry"); @@ -695,13 +709,13 @@ TEST(RenderiveWebGallery, EveryCanvasAcceptsTheCompleteWebEventSetAndStillRender R"({"category":"event","type":"show"})"); ASSERT_TRUE(show.has_value()); EXPECT_FALSE(session.handle(*show).has_value()); - const auto frame = session.handle(Frame_Request{}); + const auto frame = wait_for_pixel_frame(session, 420, 260); ASSERT_TRUE(frame.has_value()); ASSERT_EQ(frame->type, Web_Response_Type::Pixels); EXPECT_EQ(frame->payload.substr(0, 4), "RVP1"); EXPECT_EQ(read_u32_le(frame->payload, 4), 420U); EXPECT_EQ(read_u32_le(frame->payload, 8), 260U); - const auto telemetry = observe_telemetry(session); + const auto telemetry = refresh_telemetry(session); EXPECT_TRUE(telemetry.at("view_active").get()); EXPECT_EQ(telemetry.at("viewport").at("width"), 420); EXPECT_EQ(telemetry.at("viewport").at("height"), 260); @@ -747,18 +761,18 @@ TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) { EXPECT_EQ(state.at("controls").at("view").at("protocol"), "adminive.view") << case_id; EXPECT_FALSE(session.handle(Viewport_Resize{{480, 280}}).has_value()) << case_id; - const auto frame = session.handle(Frame_Request{}); + const auto frame = wait_for_pixel_frame(session, 480, 280); ASSERT_TRUE(frame.has_value()) << case_id; ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << case_id; EXPECT_EQ(frame->payload.substr(0, 4), "RVP1") << case_id; EXPECT_EQ(read_u32_le(frame->payload, 4), 480U) << case_id; EXPECT_EQ(read_u32_le(frame->payload, 8), 280U) << case_id; const auto observed = session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})")); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh"})")); ASSERT_TRUE(observed.has_value()) << case_id; const auto telemetry = parse_json(observed->payload); - EXPECT_EQ(telemetry.at("type"), "observer_state") << case_id; + EXPECT_EQ(telemetry.at("type"), "case_state") << case_id; EXPECT_EQ(telemetry.at("frame_mode").at("id"), std::string(mode)) << case_id; EXPECT_TRUE(telemetry.at("telemetry").contains("kernel_observer")) << case_id; EXPECT_TRUE(telemetry.at("telemetry").contains("performance")) << case_id; @@ -769,6 +783,50 @@ TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) { } } +TEST(RenderiveWebGallery, BackendPublishesAmisValidationSchemas) { + Gallery_Plot_Session low_latency; + const auto opened = low_latency.handle(gallery_request( + Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))); + ASSERT_TRUE(opened.has_value()); + const auto state = parse_json(opened->payload); + const auto& controls = state.at("controls").at("data"); + const auto max_fps = std::find_if(controls.begin(), controls.end(), [](const auto& item) { + return item.at("id") == "max_render_fps"; + }); + ASSERT_NE(max_fps, controls.end()); + const auto& max_fps_amis = max_fps->at("amis"); + EXPECT_EQ(max_fps_amis.at("type"), "input-number"); + EXPECT_DOUBLE_EQ(max_fps_amis.at("min").get(), 0.01); + EXPECT_DOUBLE_EQ(max_fps_amis.at("max").get(), 1'000'000'000.0); + EXPECT_DOUBLE_EQ(max_fps_amis.at("validations").at("minimum").get(), 0.01); + EXPECT_DOUBLE_EQ(max_fps_amis.at("validations").at("maximum").get(), 1'000'000'000.0); + const auto background = std::find_if(controls.begin(), controls.end(), [](const auto& item) { + return item.at("id") == "background_color"; + }); + ASSERT_NE(background, controls.end()); + EXPECT_EQ(background->at("amis").at("validations").at("matchRegexp"), + "^#[0-9A-Fa-f]{6}$"); + const auto patched = Gallery_Protocol::apply_patch( + "spectrum", Gallery_Protocol::default_state("spectrum", Gallery_Frame_Mode::Low_Latency), + R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":100}})", + Gallery_Frame_Mode::Low_Latency); + ASSERT_TRUE(patched.candidate.has_value()); + EXPECT_DOUBLE_EQ(std::get(patched.candidate->values.at("max_render_fps")), 100.0); + Gallery_Plot_Session playback; + const auto playback_opened = playback.handle(gallery_request( + Gallery_Request_Kind::Open, open_message("spectrum", "playback"))); + ASSERT_TRUE(playback_opened.has_value()); + const auto playback_state = parse_json(playback_opened->payload); + const auto& actions = playback_state.at("actions").at("data"); + const auto burst = std::find_if(actions.begin(), actions.end(), [](const auto& item) { + return item.at("id") == "mode_enqueue_burst"; + }); + ASSERT_NE(burst, actions.end()); + const auto& burst_amis = burst->at("argument_amis"); + EXPECT_EQ(burst_amis.at("type"), "input-number"); + EXPECT_EQ(burst_amis.at("name"), "action_mode_enqueue_burst"); +} + TEST(RenderiveWebGallery, ActionsMutateLiveCore2ObjectsAndReturnTelemetry) { Gallery_Plot_Session session; ASSERT_TRUE(session.handle(gallery_request( @@ -805,10 +863,10 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers request["argument"] = *argument; return session.handle(gallery_request(Gallery_Request_Kind::Action, request.dump())); }; - const auto observe = [](Gallery_Plot_Session& session) { + const auto refresh = [](Gallery_Plot_Session& session) { const auto response = session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})")); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh"})")); EXPECT_TRUE(response.has_value()); return parse_json(response->payload).at("telemetry"); }; @@ -817,12 +875,12 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers ASSERT_TRUE(manual.handle(gallery_request( Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); ASSERT_TRUE(action(manual, "mode_prepare")); - auto manual_observer = observe(manual).at("kernel_observer"); + auto manual_observer = refresh(manual).at("kernel_observer"); EXPECT_EQ(manual_observer.at("last_event"), "prepared"); EXPECT_EQ(manual_observer.at("pending_frame_count"), 1); ASSERT_TRUE(action(manual, "mode_refresh")); ASSERT_TRUE(action(manual, "mode_render")); - manual_observer = observe(manual).at("kernel_observer"); + manual_observer = refresh(manual).at("kernel_observer"); EXPECT_EQ(manual_observer.at("last_event"), "rendered"); EXPECT_GE(manual_observer.at("consumed_frame_count").get(), 2U); @@ -835,7 +893,7 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers ASSERT_TRUE(high_frequency.has_value()); ASSERT_EQ(parse_json(high_frequency->payload).at("type"), "case_state"); ASSERT_TRUE(low_latency.handle(Frame_Request{})); - const auto low_telemetry = observe(low_latency); + const auto low_telemetry = refresh(low_latency); EXPECT_EQ(low_telemetry.at("frame_mode"), "low_latency"); EXPECT_GT(low_telemetry.at("kernel_observer").at("observation_count").get(), 0U); EXPECT_TRUE(low_telemetry.contains("render_fps")); @@ -868,11 +926,11 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers ASSERT_TRUE(playback.handle(gallery_request( Gallery_Request_Kind::Open, open_message("spectrum", "playback")))); ASSERT_TRUE(action(playback, "mode_enqueue_burst", 5)); - auto playback_observer = observe(playback).at("kernel_observer"); + auto playback_observer = refresh(playback).at("kernel_observer"); EXPECT_EQ(playback_observer.at("last_event"), "enqueued"); EXPECT_EQ(playback_observer.at("pending_frame_count"), 5); ASSERT_TRUE(action(playback, "mode_dequeue")); - playback_observer = observe(playback).at("kernel_observer"); + playback_observer = refresh(playback).at("kernel_observer"); EXPECT_EQ(playback_observer.at("last_event"), "rendered"); EXPECT_EQ(playback_observer.at("pending_frame_count"), 4); EXPECT_GT(playback_observer.at("queue_wait_ns").get(), 0U); @@ -888,7 +946,7 @@ TEST(RenderiveWebGallery, ManualStrategyRunsOnlyExplicitPrepareRefreshRenderCycl EXPECT_EQ(baseline.at("limit_state"), "not_applicable"); std::this_thread::sleep_for(std::chrono::milliseconds(80)); - auto observer = observe_telemetry(session).at("kernel_observer"); + auto observer = refresh_telemetry(session).at("kernel_observer"); EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence")); EXPECT_EQ(observer.at("produced_frame_count"), baseline.at("produced_frame_count")); EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count")); @@ -929,7 +987,7 @@ TEST(RenderiveWebGallery, ManualStrategyRunsOnlyExplicitPrepareRefreshRenderCycl const auto sequence_after_actions = observer.at("latest_sequence"); std::this_thread::sleep_for(std::chrono::milliseconds(80)); - EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"), + EXPECT_EQ(refresh_telemetry(session).at("kernel_observer").at("latest_sequence"), sequence_after_actions); } @@ -953,7 +1011,7 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres const auto frame = session.handle(Frame_Request{}); ASSERT_TRUE(frame.has_value()) << iteration; ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << iteration; - const auto telemetry = observe_telemetry(session); + const auto telemetry = refresh_telemetry(session); EXPECT_LT(std::chrono::steady_clock::now() - started, std::chrono::milliseconds(250)) << iteration; EXPECT_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 1000.0); @@ -964,7 +1022,7 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres } EXPECT_NE(first_pixels, last_pixels); - const auto telemetry = observe_telemetry(session); + const auto telemetry = refresh_telemetry(session); const auto& observer = telemetry.at("kernel_observer"); EXPECT_EQ(observer.at("target_interval_ns"), 1'000'000); EXPECT_GT(observer.at("latest_sequence").get(), 4U); @@ -1008,7 +1066,7 @@ TEST(RenderiveWebGallery, FrequencyLimitCannotBeBypassedByForegroundRescheduleEv const auto rendered = after - before; EXPECT_GT(rendered, 0U); EXPECT_LE(rendered, 16U); - const auto telemetry = observe_telemetry(session); + const auto telemetry = refresh_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(), 33'333'333U); @@ -1025,8 +1083,8 @@ TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundReschedule {"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}})"))); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh","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()); EXPECT_TRUE(observer.at("consumer_feedback_enabled").get()); @@ -1048,7 +1106,7 @@ TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundReschedule const auto rendered = after - before; EXPECT_GT(rendered, 0U); EXPECT_LE(rendered, 14U); - observed = observe_telemetry(session); + observed = refresh_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(), 40'000'000U); @@ -1064,8 +1122,8 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000}})"))).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":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":80,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":80,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); auto observer = observed.at("telemetry").at("kernel_observer"); auto limit = observed.at("telemetry").at("low_latency_limit"); EXPECT_TRUE(observer.at("frequency_limit_enabled").get()); @@ -1078,8 +1136,8 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback EXPECT_EQ(observer.at("limit_state"), "consumer_limited"); EXPECT_EQ(limit.at("consumer_feedback_source"), "presentation"); observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); observer = observed.at("telemetry").at("kernel_observer"); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 22'000'000); EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 21'000'000); @@ -1116,8 +1174,8 @@ TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndMa {"consumer_manual_feedback_enabled", false} }).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":60,"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":16,"display_interval_latest_ms":16,"display_interval_p95_ms":17,"display_jitter_ms":1,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":25,"presentation_fps":60,"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":16,"display_interval_latest_ms":16,"display_interval_p95_ms":17,"display_jitter_ms":1,"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_EQ(observer.at("consumer_feedback_source"), "pixel"); EXPECT_EQ(observer.at("consumer_pixel_interval_ns"), 40'000'000); @@ -1159,7 +1217,7 @@ TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndRe EXPECT_EQ(baseline.at("limit_state"), "not_applicable"); std::this_thread::sleep_for(std::chrono::milliseconds(80)); - auto observer = observe_telemetry(session).at("kernel_observer"); + auto observer = refresh_telemetry(session).at("kernel_observer"); EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence")); EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count")); @@ -1168,7 +1226,7 @@ TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndRe EXPECT_EQ(observer.at("last_event"), "enqueued"); EXPECT_EQ(observer.at("pending_frame_count"), 5); std::this_thread::sleep_for(std::chrono::milliseconds(20)); - EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("pending_frame_count"), 5); + EXPECT_EQ(refresh_telemetry(session).at("kernel_observer").at("pending_frame_count"), 5); state = invoke_action(session, "mode_dequeue"); observer = state.at("telemetry").at("kernel_observer"); @@ -1195,7 +1253,7 @@ TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndRe const auto sequence_after_actions = observer.at("latest_sequence"); std::this_thread::sleep_for(std::chrono::milliseconds(80)); - EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"), + EXPECT_EQ(refresh_telemetry(session).at("kernel_observer").at("latest_sequence"), sequence_after_actions); } @@ -1233,8 +1291,8 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) { return successful_render_count(session) >= 4; })); const auto observed = session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"display_interval_ms":16.7,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_jitter_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})")); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"display_interval_ms":16.7,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_jitter_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})")); ASSERT_TRUE(observed.has_value()); const auto telemetry = parse_json(observed->payload).at("telemetry"); const auto& performance = telemetry.at("performance"); @@ -1348,8 +1406,8 @@ void expect_low_latency_canvas_to_change(std::string_view case_id) { EXPECT_NE(first->payload, second->payload); const auto observed = session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})")); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh"})")); ASSERT_TRUE(observed.has_value()); const auto telemetry = parse_json(observed->payload).at("telemetry"); EXPECT_GT(telemetry.at("frame_index").get(), 2U); @@ -1424,8 +1482,8 @@ TEST(RenderiveWebGallery, BuilderOnlyControlsRebuildAndAllImageModesRender) { ASSERT_TRUE(palette.has_value()); ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value()); const auto waterfall_observer = waterfall.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe"})")); + Gallery_Request_Kind::Refresh, + R"({"category":"event","type":"gallery_refresh"})")); ASSERT_TRUE(waterfall_observer.has_value()); const auto waterfall_telemetry = parse_json(waterfall_observer->payload).at("telemetry"); EXPECT_GT(waterfall_telemetry.at("waterfall").at("row_count").get(), 0U); diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index 6c75c16..587c305 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -30,7 +30,6 @@ let displayIntervalMs = 0; let displayIntervalLatestMs = 0; let displayIntervalP95Ms = 0; let displayJitterMs = 0; -let lastUiUpdateAt = 0; const displayIntervalSamples = []; const message = (type, payload = {}) => JSON.stringify({category: "event", type, ...payload}); const quote = value => JSON.stringify(String(value)); @@ -59,9 +58,9 @@ function formatFps(value, digits = 1) { } function normalizeEventValue(payload, name) { if (payload !== null && typeof payload === "object") { - if (Object.prototype.hasOwnProperty.call(payload, "value")) return payload.value; if (Object.prototype.hasOwnProperty.call(payload, name)) return payload[name]; if (payload.data !== null && typeof payload.data === "object") return normalizeEventValue(payload.data, name); + if (Object.prototype.hasOwnProperty.call(payload, "value")) return payload.value; } return payload; } @@ -188,7 +187,6 @@ class GallerySession { this.frameRequestStartedAt = 0; this.frameRoundTripMs = 0; this.overwrittenPixelFrames = 0; - this.lastObserveRequest = 0; this.shell = null; this.canvas = null; this.context = null; @@ -279,6 +277,7 @@ class GallerySession { this.socket = socket; this.socketState = "connecting"; this.socketText = "连接中"; + updateMainData(); socket.binaryType = "arraybuffer"; socket.addEventListener("open", () => { if (this.socket !== socket) return; @@ -303,12 +302,14 @@ class GallerySession { this.socketState = "error"; this.socketText = this.displayVisible() ? "连接关闭 · 自动重连" : "连接关闭 · 等待可见"; this.updateMotionStatus(); + updateMainData(); if (!this.disposed && this.displayVisible()) this.reconnectTimer = setTimeout(() => this.connect(), 1000); }); socket.addEventListener("error", () => { if (this.socket !== socket) return; this.socketState = "error"; this.socketText = "连接错误 · 自动重连"; + updateMainData(); }); } send(type, payload = {}) { @@ -332,10 +333,6 @@ class GallerySession { } return; } - if (data.type === "observer_state") { - this.telemetry = data.telemetry || {}; - return; - } if (data.type !== "case_state") return; this.controls = data.controls?.data || []; this.actions = data.actions?.data || []; @@ -350,6 +347,7 @@ class GallerySession { this.socketText = `${data.frame_mode?.strategy || "Core2"} 在线`; this.syncActivity(); if (data.notice && !data.notice.includes("已创建")) notify(`${this.definition.title}:${data.notice}`); + updateMainData(); if (menuSession === this) { menuStatus = data.notice || "后端状态已回读"; updateMenuData(); @@ -464,15 +462,15 @@ class GallerySession { if (!this.streamActive() || this.socket?.readyState !== WebSocket.OPEN) return; this.socketState = "error"; this.socketText = "像素响应超时 · 正在恢复"; + updateMainData(); this.syncActivity(true); }, 1500); } - observe(time) { - if (!this.ready || !this.displayVisible() || this.socket?.readyState !== WebSocket.OPEN || time - this.lastObserveRequest < 650) return; - this.lastObserveRequest = time; + refreshBackendState() { + const time = performance.now(); this.transportFps = this.currentRate(this.transportTimes, time); this.presentationFps = this.currentRate(this.presentationTimes, time); - this.send("gallery_observe", {client_metrics: this.clientMetrics(time)}); + this.send("gallery_refresh", {client_metrics: this.clientMetrics(time)}); } clientMetrics(time = performance.now()) { return { @@ -524,20 +522,11 @@ class GallerySession { commitControl(id, payload) { const item = this.controls.find(control => control.id === id); if (!item) return; - let value = normalizeEventValue(payload, id); - if (item.input === "number") value = Number(value); - else if (item.input === "boolean") value = Boolean(value); - else value = String(value ?? ""); - if (item.input === "number" && (!Number.isFinite(value) || value < Number(item.minimum) || value > Number(item.maximum))) { - menuStatus = `${item.label} 超出后端允许范围`; - renderMenu(this, true); - return; - } + const value = normalizeEventValue(payload, id); if (Object.is(this.lastSubmittedControls.get(id), value) || Object.is(this.controlValues.get(id), value)) return; this.lastSubmittedControls.set(id, value); this.send("gallery_patch", {patch: {[id]: value}}); menuStatus = `提交 ${item.api} · 等待后端回读`; - updateMenuData(); } runAction(id, payload) { const item = this.actions.find(action => action.id === id); @@ -545,13 +534,10 @@ class GallerySession { const request = {action: item.id}; if (item.argument_input) { const field = `action_${item.id}`; - let value = normalizeEventValue(payload, field); - if (item.argument_input === "number") value = Number(value); - request.argument = value; + request.argument = normalizeEventValue(payload, field); } this.send("gallery_action", request); menuStatus = `执行 ${item.api}`; - updateMenuData(); if (["mode_render", "mode_dequeue", "mode_cycle"].includes(item.id)) setTimeout(() => this.requestFrame(performance.now(), true), 40); } controlData() { @@ -682,6 +668,7 @@ function buildCardSchema(definition, mode) { body, actions: [ {type: "button", label: frameLabel, level: "primary", onEvent: {click: {actions: [{actionType: "custom", script: `window.RenderiveGallery.requestFrame(${quote(key)});`}]}}}, + {type: "button", label: "刷新后端状态", onEvent: {click: {actions: [{actionType: "custom", script: `window.RenderiveGallery.refreshBackendState(${quote(key)});`}]}}}, {type: "button", label: "打开测试菜单", onEvent: {click: {actions: [{actionType: "custom", script: `window.RenderiveGallery.openMenu(${quote(key)});`}]}}} ] }; @@ -723,27 +710,10 @@ function controlEventAction(session, item, source) { return {actionType: "custom", script: `window.RenderiveGallery.commitControl(${quote(session.key)},${quote(item.id)},${source});`}; } function controlSchema(session, item) { - const base = {name: item.id, label: item.label, description: item.description || item.api, remark: item.api, size: "md"}; - if (item.input === "boolean") { - return {type: "form", wrapWithPanel: false, className: "rv-control-form", body: [{...base, type: "switch", onEvent: {change: {actions: [controlEventAction(session, item, "event.data")]}}}]}; - } - if (item.input === "select") { - return {type: "form", wrapWithPanel: false, className: "rv-control-form", body: [{...base, type: "select", options: (item.options || []).map(value => ({label: value, value})), onEvent: {change: {actions: [controlEventAction(session, item, "event.data")]}}}]}; - } - if (item.input === "color") { - return {type: "form", wrapWithPanel: false, className: "rv-control-form", body: [{...base, type: "input-color", onEvent: {change: {actions: [controlEventAction(session, item, "event.data")]}}}]}; - } - if (item.input === "number") { - return { - type: "form", - wrapWithPanel: false, - className: "rv-control-form", - body: [{...base, type: "input-number", min: item.minimum, max: item.maximum, step: item.step || 1, precision: item.integer ? 0 : undefined, onEvent: {blur: {actions: [controlEventAction(session, item, "event.data")]}}}], - actions: [], - onEvent: {submit: {preventDefault: true, actions: [controlEventAction(session, item, "context.data")]}} - }; - } - return {type: "form", wrapWithPanel: false, className: "rv-control-form", body: [{...base, type: "input-text", onEvent: {blur: {actions: [controlEventAction(session, item, "event.data")]}}}]}; + const control = {...item.amis}; + const eventName = ["switch", "select", "input-color"].includes(control.type) ? "change" : "blur"; + control.onEvent = {[eventName]: {actions: [controlEventAction(session, item, "context.data")]}}; + return {type: "form", wrapWithPanel: false, className: "rv-control-form", body: [control], actions: []}; } function buildControlTab(session) { return [...groupItems(session.controls)].map(([group, items]) => ({type: "panel", className: "rv-control-panel", title: group, body: items.map(item => controlSchema(session, item))})); @@ -754,7 +724,7 @@ function actionSchema(session, item) { const api = item.api || item.id; const description = item.description || ""; const body = [{type: "tpl", tpl: `
${escapeHtml(label)}${escapeHtml(api)}
${description ? `${escapeHtml(description)}` : ""}
`}]; - if (item.argument_input) body.push({type: item.argument_input === "number" ? "input-number" : "input-text", name: field, label: item.argument_label || "参数"}); + if (item.argument_input) body.push({...item.argument_amis, name: field}); body.push({type: "button", label: `执行 · ${label}`, level: "primary", onEvent: {click: {actions: [{actionType: "custom", script: `window.RenderiveGallery.runAction(${quote(session.key)},${quote(item.id)},context.data);`}]}}}); return {type: "form", wrapWithPanel: false, className: "rv-action-form", body, actions: []}; } @@ -775,7 +745,10 @@ function buildMenuSchema(session) { body: [ {type: "flex", className: "rv-menu-header", justify: "space-between", alignItems: "flex-start", items: [ {type: "tpl", tpl: `

${session.mode.strategy} / ${session.definition.component}

${session.definition.title}

${session.definition.description}

`}, - {type: "button", label: "关闭", onEvent: {click: {actions: [{actionType: "custom", script: "window.RenderiveGallery.closeMenu();"}]}}} + {type: "flex", gap: "sm", items: [ + {type: "button", label: "刷新后端状态", level: "primary", onEvent: {click: {actions: [{actionType: "custom", script: `window.RenderiveGallery.refreshBackendState(${quote(session.key)});`}]}}}, + {type: "button", label: "关闭", onEvent: {click: {actions: [{actionType: "custom", script: "window.RenderiveGallery.closeMenu();"}]}}} + ]} ]}, {type: "alert", level: "info", body: "${menuStatus}", showIcon: false}, {type: "tabs", className: "rv-menu-tabs", mountOnEnter: true, tabs: [ @@ -879,12 +852,6 @@ function loop(time) { session.presentLatest(time); session.updateMotionStatus(time); if (session.mode.id !== "low_latency") session.requestFrame(time); - session.observe(time); - } - if (time - lastUiUpdateAt >= 200) { - lastUiUpdateAt = time; - updateMainData(); - updateMenuData(); } requestAnimationFrame(loop); } @@ -903,6 +870,9 @@ window.RenderiveGallery = { requestFrame(key) { sessions.get(key)?.requestFrame(performance.now(), true); }, + refreshBackendState(key) { + sessions.get(key)?.refreshBackendState(); + }, openMenu(key) { const session = sessions.get(key); if (session) renderMenu(session, menuSession !== session);