From ba820afbcbbd419bb1367b52d8f8c97c0c0ff434 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 14 Aug 2026 06:22:14 +0800 Subject: [PATCH] =?UTF-8?q?datoviz=E6=9E=B6=E6=9E=84=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../low_latency/Low_Latency_Strategy.hpp | 7 + .../low_latency/Low_Latency_Strategy.inl | 20 +- .../manual/Manual_Refresh_Strategy.hpp | 11 +- .../manual/Manual_Refresh_Strategy.inl | 15 +- .../renderable/base/Renderable_Base.cpp | 10 +- .../renderable/base/Renderable_Base_p.hpp | 1 + .../low_latency/Low_Latency_Strategy_Test.cpp | 64 ++ .../manual/Manual_Refresh_Strategy_Test.cpp | 36 + render_3D/CMakeLists.txt | 72 +- render_3D/cdb_test_commands.txt | 3 + render_3D/datoviz/cmake/DatovizTests.cmake | 8 +- render_3D/env.ps1 | 44 +- render_3D/plan.md | 936 ++++++++++++++++++ .../render_3D/backend/datoviz/Datoviz_Frame.h | 25 + .../backend/datoviz/Datoviz_Renderer.cpp | 647 ++++++++++++ .../backend/datoviz/Datoviz_Renderer.h | 33 + .../backend/datoviz/Datoviz_Resources.h | 25 + render_3D/render_3D/base/Object_Access_p.h | 93 ++ render_3D/render_3D/base/Object_Id.cpp | 12 + render_3D/render_3D/base/Object_Id.h | 41 + render_3D/render_3D/base/Types.h | 57 ++ render_3D/render_3D/camera/Camera.cpp | 106 ++ render_3D/render_3D/camera/Camera.h | 59 ++ render_3D/render_3D/camera/Camera_p.h | 36 + render_3D/render_3D/main.cpp | 0 render_3D/render_3D/material/Material.cpp | 108 ++ render_3D/render_3D/material/Material.h | 61 ++ render_3D/render_3D/material/Material_p.h | 36 + render_3D/render_3D/mesh/Mesh.cpp | 104 ++ render_3D/render_3D/mesh/Mesh.h | 58 ++ render_3D/render_3D/mesh/Mesh_p.h | 36 + render_3D/render_3D/scene/Basic_Scene3D.h | 97 ++ render_3D/render_3D/scene/Scene.cpp | 244 +++++ render_3D/render_3D/scene/Scene.h | 85 ++ render_3D/render_3D/scene/Scene_Frame.h | 83 ++ render_3D/render_3D/scene/Scene_p.h | 49 + render_3D/render_3D/texture/Texture.cpp | 51 + render_3D/render_3D/texture/Texture.h | 49 + render_3D/render_3D/texture/Texture_p.h | 37 + render_3D/tests/Datoviz_Renderer_Tests.cpp | 109 ++ render_3D/tests/Object_State_Tests.cpp | 49 + render_3D/tests/Scene_Frame_Control_Tests.cpp | 46 + render_3D/tests/Scene_Frame_Tests.cpp | 107 ++ render_3D/tests/Test_Fixtures.h | 51 + render_3D/调试.md | 5 +- 45 files changed, 3800 insertions(+), 26 deletions(-) create mode 100644 render_3D/cdb_test_commands.txt create mode 100644 render_3D/plan.md create mode 100644 render_3D/render_3D/backend/datoviz/Datoviz_Frame.h create mode 100644 render_3D/render_3D/backend/datoviz/Datoviz_Renderer.cpp create mode 100644 render_3D/render_3D/backend/datoviz/Datoviz_Renderer.h create mode 100644 render_3D/render_3D/backend/datoviz/Datoviz_Resources.h create mode 100644 render_3D/render_3D/base/Object_Access_p.h create mode 100644 render_3D/render_3D/base/Object_Id.cpp create mode 100644 render_3D/render_3D/base/Object_Id.h create mode 100644 render_3D/render_3D/base/Types.h create mode 100644 render_3D/render_3D/camera/Camera.cpp create mode 100644 render_3D/render_3D/camera/Camera.h create mode 100644 render_3D/render_3D/camera/Camera_p.h delete mode 100644 render_3D/render_3D/main.cpp create mode 100644 render_3D/render_3D/material/Material.cpp create mode 100644 render_3D/render_3D/material/Material.h create mode 100644 render_3D/render_3D/material/Material_p.h create mode 100644 render_3D/render_3D/mesh/Mesh.cpp create mode 100644 render_3D/render_3D/mesh/Mesh.h create mode 100644 render_3D/render_3D/mesh/Mesh_p.h create mode 100644 render_3D/render_3D/scene/Basic_Scene3D.h create mode 100644 render_3D/render_3D/scene/Scene.cpp create mode 100644 render_3D/render_3D/scene/Scene.h create mode 100644 render_3D/render_3D/scene/Scene_Frame.h create mode 100644 render_3D/render_3D/scene/Scene_p.h create mode 100644 render_3D/render_3D/texture/Texture.cpp create mode 100644 render_3D/render_3D/texture/Texture.h create mode 100644 render_3D/render_3D/texture/Texture_p.h create mode 100644 render_3D/tests/Datoviz_Renderer_Tests.cpp create mode 100644 render_3D/tests/Object_State_Tests.cpp create mode 100644 render_3D/tests/Scene_Frame_Control_Tests.cpp create mode 100644 render_3D/tests/Scene_Frame_Tests.cpp create mode 100644 render_3D/tests/Test_Fixtures.h diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp index f3129d4..e795134 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -29,6 +30,10 @@ public: }; struct Configuration { double frequency_hz{60.0}; + // true keeps the existing latest-frame-wins behavior. false keeps one pending frame and + // applies producer backpressure until the renderer consumes it. Explicit discard APIs + // retain their existing semantics in either mode. + bool replace_pending_frame{true}; }; struct Timing_Statistics { std::uint64_t paint_request_time_ns{}; @@ -183,9 +188,11 @@ private: std::uint64_t real_time_data_update_sequence_{}; Real_Time_Data_Update_State last_real_time_data_update_; Counter_Statistics counters_; + bool replace_pending_frame_{true}; bool cache_ready_{}; bool render_ready_{}; mutable Mutex state_mutex_; + std::condition_variable_any pending_frame_condition_; Mutex painter_mutex_; Mutex render_mutex_; }; diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl index c9c82bf..a4db95f 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl @@ -8,7 +8,12 @@ Low_Latency_Strategy::Painter_Lease::Painter_Lease const std::uint64_t lease_acquired_time_ns = strategy.now_ns(); const std::uint64_t state_request_time_ns = strategy.now_ns(); { - std::lock_guard lock(strategy.state_mutex_); + std::unique_lock lock(strategy.state_mutex_); + if (!strategy.replace_pending_frame_) { + strategy.pending_frame_condition_.wait(lock, [&strategy] { + return !strategy.cache_ready_; + }); + } const std::uint64_t state_acquired_time_ns = strategy.now_ns(); frame_ = strategy.paint_; frame_->statistics = {}; @@ -94,7 +99,6 @@ Low_Latency_Strategy::Render_Lease::Render_Lease(L const std::uint64_t state_request_time_ns = strategy.now_ns(); { std::lock_guard lock(strategy.state_mutex_); - const std::uint64_t state_acquired_time_ns = strategy.now_ns(); if (strategy.cache_ready_) { std::swap(strategy.cache_, strategy.render_); strategy.cache_ready_ = false; @@ -105,6 +109,13 @@ Low_Latency_Strategy::Render_Lease::Render_Lease(L swap_failed_ = true; } frame_ = strategy.render_ready_ ? strategy.render_ : nullptr; + } + if (acquired_new_frame_) { + strategy.pending_frame_condition_.notify_one(); + } + { + std::lock_guard lock(strategy.state_mutex_); + const std::uint64_t state_acquired_time_ns = strategy.now_ns(); if (frame_) { ++frame_->statistics.frame_render_count; frame_->statistics.timing.render_request_time_ns = request_time_ns_; @@ -205,7 +216,7 @@ auto Low_Latency_Strategy::Render_Lease::get() con } template Low_Latency_Strategy::Low_Latency_Strategy(Observer observer, Configuration configuration) - : Frame_Control_Strategy_Base(checked_frequency_hz(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]) { + : Frame_Control_Strategy_Base(checked_frequency_hz(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]), replace_pending_frame_(configuration.replace_pending_frame) { state_.frequency_hz = configuration.frequency_hz; state_.target_interval_ns = frequency_interval_ns(configuration.frequency_hz); update_refresh_control(); @@ -302,6 +313,7 @@ bool Low_Latency_Strategy::discard_pending_frame() discarded = discard_pending_frame_locked(observation); } if (discarded) { + pending_frame_condition_.notify_one(); observe(observation); } return discarded; @@ -317,6 +329,7 @@ bool Low_Latency_Strategy::discard_pending_frame_b } } if (discarded) { + pending_frame_condition_.notify_one(); observe(observation); } return discarded; @@ -332,6 +345,7 @@ bool Low_Latency_Strategy::discard_stale_latest_da } } if (discarded) { + pending_frame_condition_.notify_one(); observe(observation); } return discarded; diff --git a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp index a4eea44..4193cf6 100644 --- a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -9,6 +10,12 @@ template > class Manual_Refresh_Strategy : public Frame_Control_Strategy_Base { public: + struct Configuration { + // true keeps prepared-frame replacement. false keeps one pending frame and applies + // producer backpressure until refresh/discard releases it. Explicit discard semantics + // are unchanged. + bool replace_pending_frame{true}; + }; enum class Observation_Event { prepared, prepared_replaced, @@ -92,7 +99,7 @@ public: Frame* frame_{}; std::unique_lock lease_lock_; }; - explicit Manual_Refresh_Strategy(Observer observer = {}); + explicit Manual_Refresh_Strategy(Observer observer = {}, Configuration configuration = {}); Painter_Lease acquire_painter(); Render_Lease acquire_renderer(); void swap() override; @@ -115,7 +122,9 @@ private: std::uint64_t real_time_data_update_sequence_{}; Real_Time_Data_Update_State last_real_time_data_update_; State state_; + bool replace_pending_frame_{true}; mutable Mutex state_mutex_; + std::condition_variable_any pending_frame_condition_; Mutex painter_mutex_; Mutex render_mutex_; }; diff --git a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl index cf117b2..57fa6f5 100644 --- a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl @@ -2,7 +2,12 @@ template Manual_Refresh_Strategy::Painter_Lease::Painter_Lease(Manual_Refresh_Strategy& strategy) : strategy_(&strategy), lease_lock_(strategy.painter_mutex_) { - std::lock_guard lock(strategy.state_mutex_); + std::unique_lock lock(strategy.state_mutex_); + if (!strategy.replace_pending_frame_) { + strategy.pending_frame_condition_.wait(lock, [&strategy] { + return !strategy.state_.pending_frame; + }); + } frame_ = strategy.paint_; frame_->statistics = {}; frame_->statistics.sequence = ++strategy.next_sequence_; @@ -127,8 +132,8 @@ auto Manual_Refresh_Strategy::Render_Lease::get() return frame_; } template -Manual_Refresh_Strategy::Manual_Refresh_Strategy(Observer observer) - : Frame_Control_Strategy_Base(), observer_(std::move(observer)), paint_(&frames_[0]), pending_(&frames_[1]), render_(&frames_[2]) {} +Manual_Refresh_Strategy::Manual_Refresh_Strategy(Observer observer, Configuration configuration) + : Frame_Control_Strategy_Base(), observer_(std::move(observer)), paint_(&frames_[0]), pending_(&frames_[1]), render_(&frames_[2]), replace_pending_frame_(configuration.replace_pending_frame) {} template auto Manual_Refresh_Strategy::acquire_painter() -> Painter_Lease { return Painter_Lease(*this); @@ -175,6 +180,9 @@ bool Manual_Refresh_Strategy::refresh() { observation = {Observation_Event::refresh_failed, {}, state_, last_real_time_data_update_}; } } + if (refreshed) { + pending_frame_condition_.notify_one(); + } render_lock.unlock(); observe(observation); return refreshed; @@ -192,6 +200,7 @@ bool Manual_Refresh_Strategy::discard_pending_fram update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::manually_discarded, pending_->statistics, state_, last_real_time_data_update_}; } + pending_frame_condition_.notify_one(); observe(observation); return true; } diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp index 1d308de..2e53d30 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp @@ -74,12 +74,16 @@ void Renderable_Base::Impl::rebuild_render_graph() { } void Renderable_Base::Impl::reset_render_graph() noexcept { - render_graph.reset(); + { + std::lock_guard lock(render_graph_mutex); + render_graph.reset(); + } invalidate_prepare(); } std::shared_ptr Renderable_Base::Impl::render_graph_snapshot() { + std::lock_guard lock(render_graph_mutex); if (!render_graph) { std::unordered_map next_identities; Renderable_Graph_Builder builder( @@ -99,7 +103,9 @@ Renderable_Base::Impl::render_graph_snapshot() { }); build_prepare_graph(builder); build_paint_graph(builder); - render_graph = std::make_shared( + render_graph = std::allocate_shared( + std::pmr::polymorphic_allocator{ + memory_resource_pointer}, std::move(builder).finish()); node_identities = std::move(next_identities); } diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base_p.hpp b/Kernel/src/renderive/renderable/base/Renderable_Base_p.hpp index c01f432..954590d 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base_p.hpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base_p.hpp @@ -76,6 +76,7 @@ public: const Render_Node_Id composite_node_id; std::atomic configuration; std::atomic visible{true}; + std::mutex render_graph_mutex; std::shared_ptr render_graph; std::unordered_map node_identities; std::atomic prepare_revision{1}; diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp index 1b05f10..4f8f86b 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -1,4 +1,7 @@ #include +#include +#include +#include #include #include #include "Low_Latency_Strategy_Test_Types.hpp" @@ -36,6 +39,67 @@ TEST(low_latency_strategy_test, abandons_unconsumed_cached_frame) { } EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 1); } +TEST(low_latency_strategy_test, replacement_disabled_preserves_pending_frame_and_wakes_after_renderer_acquire) { + using namespace std::chrono_literals; + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + Low_Latency_Test_Strategy strategy{ + Low_Latency_Test_Observer_State(observer, time_source), {60.0, false}}; + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + + std::promise producer_started; + auto producer_started_future = producer_started.get_future(); + auto producer = std::async(std::launch::async, [&] { + producer_started.set_value(); + auto frame = strategy.acquire_painter(); + frame->value = 2; + }); + producer_started_future.wait(); + EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout); + + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 1); + } + ASSERT_EQ(producer.wait_for(1s), std::future_status::ready); + producer.get(); + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 2); + } + EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 0u); +} +TEST(low_latency_strategy_test, replacement_disabled_wakes_blocked_producer_after_explicit_discard) { + using namespace std::chrono_literals; + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + Low_Latency_Test_Strategy strategy{ + Low_Latency_Test_Observer_State(observer, time_source), {60.0, false}}; + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + + std::promise producer_started; + auto producer_started_future = producer_started.get_future(); + auto producer = std::async(std::launch::async, [&] { + producer_started.set_value(); + auto frame = strategy.acquire_painter(); + frame->value = 2; + }); + producer_started_future.wait(); + EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout); + EXPECT_TRUE(strategy.discard_pending_frame()); + ASSERT_EQ(producer.wait_for(1s), std::future_status::ready); + producer.get(); + EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 0u); + EXPECT_EQ(strategy.counter_statistics().manually_discarded_frame_count, 1u); +} TEST(low_latency_strategy_test, manually_discards_stale_latest_data_frame) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; diff --git a/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp index 88a4cc3..67980ad 100644 --- a/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp @@ -1,6 +1,8 @@ #include #include +#include #include +#include #include #include #include @@ -44,6 +46,40 @@ TEST(manual_refresh_strategy_test, keeps_latest_prepared_frame) { ASSERT_TRUE(frame); EXPECT_EQ(frame->value, 2); } +TEST(manual_refresh_strategy_test, replacement_disabled_waits_for_refresh_and_preserves_order) { + using namespace std::chrono_literals; + Manual_Refresh_Test_Strategy strategy{{}, {false}}; + { + auto frame = strategy.acquire_painter(); + frame->value = 1; + } + + std::promise producer_started; + auto producer_started_future = producer_started.get_future(); + auto producer = std::async(std::launch::async, [&] { + producer_started.set_value(); + auto frame = strategy.acquire_painter(); + frame->value = 2; + }); + producer_started_future.wait(); + EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout); + ASSERT_TRUE(strategy.refresh()); + ASSERT_EQ(producer.wait_for(1s), std::future_status::ready); + producer.get(); + + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 1); + } + EXPECT_EQ(strategy.state().replaced_prepared_frame_count, 0u); + ASSERT_TRUE(strategy.refresh()); + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + EXPECT_EQ(frame->value, 2); + } +} TEST(manual_refresh_strategy_test, reports_failed_refresh_without_pending_frame) { Manual_Refresh_Test_Strategy strategy; EXPECT_FALSE(strategy.refresh()); diff --git a/render_3D/CMakeLists.txt b/render_3D/CMakeLists.txt index d71d456..fddc1d2 100644 --- a/render_3D/CMakeLists.txt +++ b/render_3D/CMakeLists.txt @@ -42,7 +42,30 @@ find_package(ZLIB REQUIRED) message(STATUS "${B}Vulkan_VERSION=${Vulkan_VERSION}${E}") if (RENDERIVE_BUILD_TESTS) find_package(GTest CONFIG REQUIRED) + + find_program(Renderive_render_3D_powershell + NAMES pwsh powershell REQUIRED) + if (MSVC) + if (CMAKE_VERSION VERSION_LESS 3.29) + message(FATAL_ERROR "Render3D tests require CMake 3.29 or newer for the CDB test launcher") + endif () + set(Renderive_render_3D_cdb + "D:/ae/ewdk/EWDK_22621_230929-1800/Program Files/Windows Kits/10/Debuggers/x64/cdb.exe" + CACHE FILEPATH "CDB used to run Render3D and Datoviz tests") + if (NOT EXISTS "${Renderive_render_3D_cdb}") + message(FATAL_ERROR "Render3D CDB executable does not exist: ${Renderive_render_3D_cdb}") + endif () + endif () endif () + +function(Renderive_render_3D_enable_cdb_for_test target) + if (MSVC) + set_property(TARGET "${target}" PROPERTY TEST_LAUNCHER + "${Renderive_render_3D_cdb}" + -cf + "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cdb_test_commands.txt") + endif () +endfunction() if (NOT TARGET Renderive_Kernel) rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_3D requires Renderive_Kernel") return() @@ -51,17 +74,43 @@ add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/datoviz" "${CMAKE_CURRENT_BINARY_DIR set(Renderive_render_3D_source_dir "${CMAKE_CURRENT_LIST_DIR}") append_glob_source(Renderive_render_3D_sources "${Renderive_render_3D_source_dir}") list(FILTER Renderive_render_3D_sources EXCLUDE REGEX "/tests/") -list(FILTER Renderive_render_3D_sources EXCLUDE REGEX "/datoviz/") +# Exclude only the vendored Datoviz source tree. The Render3D adapter intentionally lives in +# render_3D/backend/datoviz and is part of this library. +list(FILTER Renderive_render_3D_sources EXCLUDE REGEX "^${Renderive_render_3D_source_dir}/datoviz/") add_library(Renderive_render_3D STATIC ${Renderive_render_3D_sources}) target_include_directories(Renderive_render_3D PUBLIC "$" ) target_compile_features(Renderive_render_3D PUBLIC cxx_std_20) -target_link_libraries(Renderive_render_3D PRIVATE ${Renderive_render_3D_datoviz_targets} tinyobjloader::tinyobjloader) +if (MSVC) + # Datoviz's Windows headers select dllimport unless DVZ_SHARED is present. The migrated + # object modules are compiled with DVZ_SHARED and are folded into this static archive, so + # the adapter must use the same direct-symbol declaration mode. + target_compile_definitions(Renderive_render_3D PRIVATE DVZ_SHARED) +endif () +target_link_libraries(Renderive_render_3D + PUBLIC + Renderive_Kernel + PRIVATE + ${Renderive_render_3D_datoviz_targets} + tinyobjloader::tinyobjloader) if (MSVC) target_compile_options(Renderive_render_3D PRIVATE /utf-8) endif () if (RENDERIVE_BUILD_TESTS) + function(Renderive_render_3D_add_test target source) + add_executable("${target}" "${source}") + target_link_libraries("${target}" PRIVATE Renderive_render_3D GTest::gtest_main) + if (MSVC) + target_compile_options("${target}" PRIVATE /utf-8) + target_link_options("${target}" PRIVATE /STACK:8388608) + endif () + Renderive_render_3D_enable_cdb_for_test("${target}") + add_test(NAME "${target}" COMMAND "${target}") + set_tests_properties("${target}" PROPERTIES LABELS renderive_render_3D) + set_property(GLOBAL APPEND PROPERTY Renderive_render_3D_test_targets "${target}") + endfunction() + set(Renderive_render_3D_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Renderive_render_3D_test_sources "${Renderive_render_3D_test_dir}") foreach (Renderive_render_3D_test_source IN LISTS Renderive_render_3D_test_sources) @@ -74,8 +123,21 @@ if (RENDERIVE_BUILD_TESTS) get_filename_component(Renderive_render_3D_test_name "${Renderive_render_3D_test_source}" NAME_WE) string(MAKE_C_IDENTIFIER "${Renderive_render_3D_test_name}" Renderive_render_3D_test_name) set(Renderive_render_3D_test_target "Renderive_render_3D_${Renderive_render_3D_test_name}_${Renderive_render_3D_test_hash}") - add_executable("${Renderive_render_3D_test_target}" "${Renderive_render_3D_test_source}") - target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE Renderive_render_3D GTest::gtest_main) - add_test(NAME "${Renderive_render_3D_test_target}" COMMAND "${Renderive_render_3D_test_target}") + Renderive_render_3D_add_test("${Renderive_render_3D_test_target}" "${Renderive_render_3D_test_source}") endforeach () + + get_property(Renderive_render_3D_test_targets GLOBAL PROPERTY Renderive_render_3D_test_targets) + add_custom_target(Renderive_render_3D_tests DEPENDS ${Renderive_render_3D_test_targets}) + set_target_properties(Renderive_render_3D_tests PROPERTIES FOLDER Renderive_render_3D/tests) + add_custom_target(Renderive_render_3D_run_tests + COMMAND "${Renderive_render_3D_powershell}" + -NoProfile -ExecutionPolicy Bypass + -File "${CMAKE_CURRENT_LIST_DIR}/env.ps1" + -TestBuildDirectory "${CMAKE_BINARY_DIR}" + -TestLabel renderive_render_3D + -CTestExecutable "${CMAKE_CTEST_COMMAND}" + DEPENDS Renderive_render_3D_tests + USES_TERMINAL + VERBATIM) + set_target_properties(Renderive_render_3D_run_tests PROPERTIES FOLDER Renderive_render_3D/tests) endif () diff --git a/render_3D/cdb_test_commands.txt b/render_3D/cdb_test_commands.txt new file mode 100644 index 0000000..8f9ffe5 --- /dev/null +++ b/render_3D/cdb_test_commands.txt @@ -0,0 +1,3 @@ +sxe av +g +q diff --git a/render_3D/datoviz/cmake/DatovizTests.cmake b/render_3D/datoviz/cmake/DatovizTests.cmake index 515f527..025b63b 100644 --- a/render_3D/datoviz/cmake/DatovizTests.cmake +++ b/render_3D/datoviz/cmake/DatovizTests.cmake @@ -62,6 +62,7 @@ function(Renderive_render_3D_datoviz_add_test_runner target source_var component target_link_options(${target} PRIVATE /STACK:8388608) endif() Renderive_render_3D_datoviz_configure_target(${target}) + Renderive_render_3D_enable_cdb_for_test(${target}) set_target_properties(${target} PROPERTIES LINKER_LANGUAGE CXX FOLDER Renderive_render_3D/datoviz/tests) @@ -269,13 +270,12 @@ function(Renderive_render_3D_datoviz_add_tests) set_target_properties(Renderive_render_3D_datoviz_tests PROPERTIES FOLDER Renderive_render_3D/datoviz/tests) - find_program(Renderive_render_3D_datoviz_powershell - NAMES pwsh powershell REQUIRED) add_custom_target(Renderive_render_3D_datoviz_run_tests - COMMAND "${Renderive_render_3D_datoviz_powershell}" + COMMAND "${Renderive_render_3D_powershell}" -NoProfile -ExecutionPolicy Bypass -File "${Renderive_render_3D_datoviz_root}/../env.ps1" - -DatovizBuildDirectory "${CMAKE_BINARY_DIR}" + -TestBuildDirectory "${CMAKE_BINARY_DIR}" + -TestLabel datoviz -CTestExecutable "${CMAKE_CTEST_COMMAND}" DEPENDS Renderive_render_3D_datoviz_tests USES_TERMINAL diff --git a/render_3D/env.ps1 b/render_3D/env.ps1 index 376eaf7..c18b74b 100644 --- a/render_3D/env.ps1 +++ b/render_3D/env.ps1 @@ -1,5 +1,6 @@ param( - [string]$DatovizBuildDirectory, + [string]$TestBuildDirectory, + [string]$TestLabel = 'datoviz', [string]$CTestExecutable = 'ctest' ) @@ -9,6 +10,9 @@ $render3DRoot = $PSScriptRoot $workspaceRoot = Split-Path -Parent $render3DRoot $aeRoot = Split-Path -Parent (Split-Path -Parent $workspaceRoot) +$buildToolDirectories = @( + 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja' +) $runtimeDirectories = @( (Join-Path $aeRoot 'cached_external_library\vs2022_Debug_render_3D\install\PThreads4W\x86_64\Debug\bin') 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64' @@ -29,11 +33,23 @@ $linkDirectories = @( 'C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\ucrt\x64' 'C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\um\x64' ) +$includeDirectories = @( + 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include' + 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\ucrt' + 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\shared' + 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um' + 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\winrt' + 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\cppwinrt' +) $missingDirectories = @($runtimeDirectories | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Container) }) if ($missingDirectories.Count -ne 0) { throw "Renderive 3D runtime directories are missing: $($missingDirectories -join ', ')" } +$missingBuildToolDirectories = @($buildToolDirectories | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Container) }) +if ($missingBuildToolDirectories.Count -ne 0) { + throw "Renderive 3D build tool directories are missing: $($missingBuildToolDirectories -join ', ')" +} $missingLibraries = @($runtimeLibraries | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) }) if ($missingLibraries.Count -ne 0) { throw "Renderive 3D runtime libraries are missing: $($missingLibraries -join ', ')" @@ -42,11 +58,15 @@ $missingLinkDirectories = @($linkDirectories | Where-Object { -not (Test-Path -L if ($missingLinkDirectories.Count -ne 0) { throw "Renderive 3D link directories are missing: $($missingLinkDirectories -join ', ')" } +$missingIncludeDirectories = @($includeDirectories | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Container) }) +if ($missingIncludeDirectories.Count -ne 0) { + throw "Renderive 3D include directories are missing: $($missingIncludeDirectories -join ', ')" +} $pathEntries = @($env:PATH -split ';' | Where-Object { $_ }) -foreach ($runtimeDirectory in $runtimeDirectories) { - if ($pathEntries -notcontains $runtimeDirectory) { - $pathEntries = @($runtimeDirectory) + $pathEntries +foreach ($pathDirectory in @($buildToolDirectories + $runtimeDirectories)) { + if ($pathEntries -notcontains $pathDirectory) { + $pathEntries = @($pathDirectory) + $pathEntries } } $env:PATH = $pathEntries -join ';' @@ -59,6 +79,14 @@ foreach ($linkDirectory in $linkDirectories) { } $env:LIB = $libEntries -join ';' +$includeEntries = @($env:INCLUDE -split ';' | Where-Object { $_ }) +foreach ($includeDirectory in $includeDirectories) { + if ($includeEntries -notcontains $includeDirectory) { + $includeEntries = @($includeDirectory) + $includeEntries + } +} +$env:INCLUDE = $includeEntries -join ';' + $env:VULKAN_SDK = $vulkanSdkRoot $env:DVZ_SHADERC_RUNTIME_LIBRARY = $runtimeLibraries[1] $env:DVZ_WHEEL_RUNTIME_DIRS = Join-Path $vulkanSdkRoot 'Bin' @@ -72,16 +100,18 @@ $env:TEMP = $windowsTempDirectory $env:TMP = $windowsTempDirectory Write-Host 'Renderive render_3D runtime environment is ready.' +Write-Host " Ninja: $($buildToolDirectories[0])" Write-Host " PThreads4W: $($runtimeDirectories[0])" Write-Host " Windows SDK: $($runtimeDirectories[1])" Write-Host " Link roots: $($linkDirectories -join '; ')" +Write-Host " Includes: $($includeDirectories -join '; ')" Write-Host " Vulkan SDK: $vulkanSdkRoot" Write-Host " shaderc: $env:DVZ_SHADERC_RUNTIME_LIBRARY" Write-Host " TEMP: $windowsTempDirectory" -if ($DatovizBuildDirectory) { - $resolvedBuildDirectory = (Resolve-Path -LiteralPath $DatovizBuildDirectory).Path - & $CTestExecutable --test-dir $resolvedBuildDirectory -L datoviz --output-on-failure -j 1 +if ($TestBuildDirectory) { + $resolvedBuildDirectory = (Resolve-Path -LiteralPath $TestBuildDirectory).Path + & $CTestExecutable --test-dir $resolvedBuildDirectory -L $TestLabel --output-on-failure -j 1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/render_3D/plan.md b/render_3D/plan.md new file mode 100644 index 0000000..faf0c43 --- /dev/null +++ b/render_3D/plan.md @@ -0,0 +1,936 @@ +可以,下面这份就按 **Codex 执行计划** 来做。基于你当前代码,重点不是再动 Datoviz,而是把 +`Kernel → Scene3D → Datoviz backend` 这条链建立起来。 + +## 1. 先扩展 Kernel 帧策略 + +修改: + +```text +Kernel/src/renderive/frame_control/strategy/low_latency/ +Kernel/src/renderive/frame_control/strategy/manual/ +Kernel/tests/renderive/frame_control/strategy/ +``` + +### Low_Latency_Strategy + +给: + +```cpp +struct Configuration +``` + +增加: + +```cpp +bool replace_pending_frame{true}; +``` + +语义: + +```text +true += 保持现在行为 += pending frame 可以被新帧替换 += abandoned_frame_count 正常增长 + +false += 已发布 pending frame 禁止自动替换 += Painter 在 pending frame 被 renderer 取得前产生背压 +``` + +默认必须是 `true`,不能破坏 Render2D 当前语义。 + +禁止 busy wait。使用等待/通知机制;等待仅发生在 pending slot 争用阶段。 + +Renderer 完成: + +```text +cache → render +``` + +交换后立即释放状态锁并唤醒 Painter。 + +### Manual_Refresh_Strategy + +这个也必须一起处理。 + +当前 Manual 同样存在: + +```text +prepared A +↓ +prepare B +↓ +A 被 prepared_replaced +``` + +所以增加同语义配置: + +```cpp +bool replace_pending_frame{true}; +``` + +Scene3D 固定 `false`。 + +`Flow_Refresh_Strategy` 不改,它本身就是队列,不覆盖。 + +### 注释必须明确 + +Kernel 注释说明: + +```text +replace_pending_frame=true +保持 latest-frame-wins / prepared-frame-replacement 旧语义。 + +replace_pending_frame=false +保持单 pending frame,但通过 producer backpressure 保证已发布帧不会被后续帧自动替换。 +``` + +并明确: + +```text +这个选项只控制自动 replacement, +不改变显式 discard API 的语义。 +``` + +### Kernel 测试 + +必须新增: + +```text +low latency 默认仍覆盖 +low latency false 不覆盖 +low latency false producer 会等待 +renderer 获取 pending 后 producer 被唤醒 +显式 discard 后 producer 被唤醒 +abandoned_frame_count 在 false 模式不增加 + +manual 默认仍 replacement +manual false 不 replacement +refresh 后 painter 才继续 +``` + +全量 Kernel 测试通过后再进入 Render3D。 + +--- + +# 2. Render3D 正式链接 Kernel + +当前: + +```cmake +Renderive_render_3D +``` + +只检查: + +```cmake +TARGET +Renderive_Kernel +``` + +但没有像 Render2D 那样真正链接。 + +改成: + +```cmake +target_link_libraries(Renderive_render_3D + PUBLIC + Renderive_Kernel + ... +) +``` + +除此之外不要调整现有 Datoviz CMake 布局。 + +--- + +# 3. 不使用现有 Kernel Scene3D_Context + +当前: + +```text +Kernel/Scene3D_Context +↓ +Triple_State_Strategy +↓ +Scene_Base +↓ +Renderable DAG +↓ +Frame_Render_Snapshot +``` + +Render3D 不走这条链。 + +原因: + +```text +Datoviz Scene/Visual +不是 Kernel Renderable + +3D 不需要再套 Renderable DAG + +当前 Scene3D_Context 使用 Triple_State, +也不符合这次要求的 Double_State 模型 +``` + +只复用 Kernel 的基础机制: + +```text +Double_State_Strategy +Low_Latency_Strategy +Manual_Refresh_Strategy +Flow_Refresh_Strategy +Property_Builder +Validated_Value / Validator +renderive_Owner +Atomic_Spin_Mutex +Observer +``` + +不要改 `Scene3D_Context` 做兼容层。 + +--- + +# 4. 建 Render3D 自己的对象层 + +目录: + +```text +render_3D/render_3D/ +├── base/ +│ ├── Object_Id.h +│ └── Object_Id.cpp +├── camera/ +│ ├── Camera.h +│ ├── Camera.cpp +│ └── Camera_p.h +├── material/ +│ ├── Material.h +│ ├── Material.cpp +│ └── Material_p.h +├── texture/ +│ ├── Texture.h +│ ├── Texture.cpp +│ └── Texture_p.h +├── mesh/ +│ ├── Mesh.h +│ ├── Mesh.cpp +│ └── Mesh_p.h +└── scene/ + ├── Scene.h + ├── Scene.cpp + ├── Scene_p.h + └── Scene_Frame.h +``` + +第一批只做: + +```text +Scene +Camera +Mesh +Material +Texture +``` + +不要一次把 Datoviz 所有 Visual 包出去。 + +--- + +# 5. PImpl 规则 + +风格参考 Render2D。 + +例如: + +```text +Mesh + └── unique_ptr + +Material + └── unique_ptr +``` + +严格规定: + +```text +public Mesh → Mesh::Impl 允许 + +Scene::Impl → Mesh::Impl 允许 +Mesh::Impl → Material::Impl 允许 +Material::Impl → Texture::Impl 允许 + +Scene::Impl → Mesh public API 禁止 +Mesh::Impl → Material public API 禁止 +backend → 任意 public API 禁止 +``` + +内部需要访问别的对象时通过: + +```text +friend ++ +d_func() +``` + +完成。 + +不要为了访问内部状态绕回公开 getter。 + +--- + +# 6. 每个对象使用 Double_State_Strategy + +例如逻辑: + +```text +Mesh::Impl + ↓ +Double_State_Strategy +``` + +状态分成: + +```text +cache +render +scratch +``` + +setter: + +```text +短锁 +↓ +修改 cache +``` + +prepare: + +```text +publish +↓ +cache → scratch +↓ +交换 render +``` + +交换后不再持锁。 + +Camera、Material、Texture 同样处理。 + +--- + +# 7. 大数据禁止跟着 Double State 整体复制 + +例如: + +```text +Mesh_State +├── transform +├── visible +├── material_id +└── shared_ptr +``` + +`Geometry_Data`: + +```text +vertices +indices +normals +uv +``` + +Texture 同样: + +```text +Texture_State +└── shared_ptr +``` + +所以 Double State publish 只复制: + +```text +matrix +enum +id +shared_ptr +``` + +禁止每帧复制整个 vertex/image 数据。 + +--- + +# 8. Scene 结构不要做 Snapshot + +本阶段 **不引入 Snapshot**。 + +Scene 内部维护: + +```text +attached meshes +active camera +pending structure changes +``` + +结构修改统一通过一个内部 mutation/edit 对象完成,不增加大量: + +```text +apply_attach_xxx +apply_detach_xxx +apply_set_xxx +``` + +这种函数。 + +例如语义: + +```cpp +scene.update([](Scene_Edit& edit) { + edit.attach(mesh); + edit.detach(old_mesh); + edit.set_camera(camera); +}); +``` + +`Scene_Edit` 直接操作 `Scene::Impl` 的结构状态。 + +--- + +# 9. Scene 所有权 + +使用: + +```text +renderive_Owner +``` + +规则: + +```text +外部 owner ++ +Scene attach 后持有自己的 owner +``` + +所以: + +```text +external mesh.reset() +``` + +但 Mesh 仍 attach: + +```text +Mesh 继续存活 +``` + +detach 后: + +```text +Scene 释放自己的 owner +``` + +Datoviz backend **绝不持有 public Mesh/Material/Texture 指针**。 + +--- + +# 10. 定义 Scene3D_Frame_Data + +放: + +```text +render_3D/render_3D/scene/Scene_Frame.h +``` + +它是 Kernel frame strategy 传递的数据。 + +内容只包括: + +```text +scene state +object updates +structure updates +immutable resource handles +``` + +例如逻辑: + +```text +Scene3D_Frame_Data +├── camera +├── mesh_updates +├── material_updates +├── texture_updates +├── attach_updates +└── detach_updates +``` + +不要命名成 Snapshot。 + +它是: + +> Kernel 帧槽中的稳定交接数据。 + +--- + +# 11. 帧生成边界 + +`prepare_frame()` 固定流程: + +```text +acquire_painter() +↓ +publish Scene Double State +↓ +publish Camera/Mesh/Material/Texture Double State +↓ +比较 revision +↓ +把发生变化的稳定状态写入 Scene3D_Frame_Data +↓ +写入 attach/detach 等结构变化 +↓ +Painter_Lease 结束 +↓ +frame publish +``` + +一旦 Painter Lease 结束: + +```text +Scene3D_Frame_Data +``` + +必须完全独立于: + +```text +public object +Double_State 内部地址 +Scene::Impl 可变结构 +``` + +--- + +# 12. Scene3D 固定关闭帧覆盖 + +定义类似 Render2D: + +```text +Manual_Frame_Control +Low_Latency_Frame_Control +Playback_Frame_Control +``` + +但是创建 3D LowLatency 时: + +```cpp +configuration.replace_pending_frame = false; +``` + +Manual 同样: + +```cpp +configuration.replace_pending_frame = false; +``` + +代码旁必须写注释: + +```text +Scene3D 的 published frame 可能包含 Datoviz retained backend +的有序资源/状态转换,因此发布后的帧禁止被后续帧自动替换。 +``` + +这个值不提供 public setter。 + +Flow 不需要特殊配置。 + +--- + +# 13. Scene3D 不暴露 discard + +Kernel generic strategy 保留: + +```text +discard_pending_frame() +``` + +但 Scene3D public API 第一版不要暴露它。 + +否则: + +```text +attach/update frame +↓ +显式 discard +↓ +Datoviz backend 没收到 +``` + +仍然会破坏 retained state 顺序。 + +--- + +# 14. 建 Datoviz adapter/backend + +新目录: + +```text +render_3D/render_3D/backend/datoviz/ +├── Datoviz_Renderer.h +├── Datoviz_Renderer.cpp +├── Datoviz_Resources.h +└── Datoviz_Frame.h +``` + +这里才写 Render3D ↔ Datoviz 的 C++ 适配。 + +不要继续修改: + +```text +render_3D/datoviz/ +``` + +除非发现真正的 Datoviz 移植 bug。 + +--- + +# 15. Datoviz backend 所有权 + +统一: + +```text +Datoviz_Renderer +├── DvzScene +├── DvzFigure +├── DvzPanel +├── DvzDrp2Runtime +├── GPU/VMA context +├── object binding map +└── GPU frame slots +``` + +例如: + +```text +Mesh Object_Id + ↓ +Backend_Mesh + ↓ +DvzVisual* +``` + +这个映射属于: + +```text +Datoviz_Renderer +``` + +不属于: + +```text +Mesh::Impl +``` + +因此: + +```text +Mesh::~Mesh() +``` + +禁止直接调用 Datoviz/Vulkan destroy。 + +--- + +# 16. Renderer 热路径完全脱离对象层 + +固定: + +```text +acquire_renderer() +↓ +拿到 Scene3D_Frame_Data +↓ +从这里开始禁止访问 public object +↓ +也禁止访问 Mesh::Impl / Material::Impl +↓ +Datoviz_Renderer::render(frame) +``` + +也就是: + +```text +双缓冲 publish +↓ +Frame_Data 固化 +==================== +下面开始无模型锁区域 +==================== +Datoviz state update +DRP2 emit +command recording +submit +``` + +这条边界必须写测试。 + +--- + +# 17. Datoviz retained state 只在 Renderer 线程修改 + +禁止: + +```text +应用线程 → dvz_visual_xxx +Painter → dvz_scene_xxx +Mesh::Impl → dvz_buffer_xxx +``` + +只有: + +```text +Datoviz_Renderer thread +``` + +允许修改: + +```text +DvzScene +DvzVisual +DvzFramePlanEmitter +DvzDrp2Runtime +Vulkan resource +``` + +这样 Datoviz 内部不用额外补大量锁。 + +--- + +# 18. GPU 帧槽独立于 Kernel 帧槽 + +不要做: + +```text +Kernel frame[0] == GPU frame[0] +``` + +单独建立: + +```text +GPU_Frame_Slot +├── command buffer +├── fence +├── render image +└── staging/readback resources +``` + +Renderer: + +```text +选择下一 GPU slot +↓ +仅等待该 slot fence +↓ +reset +↓ +record +↓ +submit +``` + +禁止主渲染路径每帧: + +```text +vkDeviceWaitIdle() +dvz_device_wait() +queue-wide wait +``` + +--- + +# 19. 第一条真实功能只打通 Mesh + +不要同时做所有 Datoviz Visual。 + +第一条完整链: + +```text +Scene +↓ +Camera +↓ +Mesh +↓ +Material +↓ +Geometry_Data +↓ +Scene3D_Frame_Data +↓ +Datoviz_Renderer +↓ +Dvz mesh visual +↓ +DRP2 +↓ +Vulkan offscreen image +``` + +这条跑通以后再加: + +```text +Texture +Text +Point +Sphere +Primitive +Volume +Transparency +Picking +``` + +--- + +# 20. Builder + +对象 Builder 复用 Kernel: + +```text +Property_Builder +Validator +Validated_Value +``` + +例如: + +```text +Mesh_Properties +Mesh_Builder + +Material_Properties +Material_Builder +``` + +Builder 只负责: + +```text +验证 properties +↓ +构造 public object + Impl +``` + +禁止 Builder: + +```text +创建 DvzVisual +创建 VkBuffer +访问 Datoviz_Renderer +attach GPU resource +``` + +GPU 创建只能等 frame 到 renderer。 + +--- + +# 21. 必须补的 Render3D 测试 + +CPU 测试: + +```text +Mesh setter 只修改 cache + +publish 后 render state 稳定 + +publish 后应用线程继续修改 cache +旧 Frame_Data 不变化 + +Impl → Impl 读取正确 + +Scene attach 持有 owner + +external owner reset 后 attached object 存活 + +detach 后 Scene owner 释放 + +Frame_Data 不保存 public object 指针 + +Frame_Data 不保存 Double_State 内部引用 +``` + +帧测试: + +```text +LowLatency Scene3D 不出现 abandoned frame + +A published 未消费 +→ B 不能覆盖 A + +renderer acquire A +→ painter 才允许继续 B +``` + +并发测试: + +```text +Renderer 正在处理 Frame A +同时应用线程修改 Mesh cache +→ 不互相持有长期锁 +``` + +Backend 测试: + +```text +attach Mesh +→ 创建一次 DvzVisual + +修改 Mesh +→ 更新同一个 backend object + +detach Mesh +→ 销毁一次 + +public Mesh 销毁 +→ 不直接触发 Datoviz GPU 操作 +``` + +--- + +# 22. 完成标准 + +这一阶段完成时必须满足: + +```text +Datoviz 源码保持独立移植目录 +Kernel 不知道 Datoviz +public Render3D API 不出现 Dvz* +Scene3D 不使用 Renderable DAG +Scene3D 不使用 Frame_Render_Snapshot +对象状态使用 Double_State +LowLatency/Manual 3D 帧不自动覆盖 +Frame publish 后 renderer 不访问 public/Impl +Datoviz 全部由 Renderer 线程拥有 +CPU 修改下一帧与 GPU 渲染上一帧可以并行 +主路径不存在 device-wide wait +``` + +执行顺序严格按: + +```text +1 Kernel replacement 配置 + 测试 +2 Render3D 链接 Kernel +3 PImpl + Object State +4 Scene ownership / mutation +5 Scene3D_Frame_Data +6 Basic_Scene3D + 三种 Frame_Control +7 Datoviz_Renderer +8 Mesh 完整链路 +9 并发/所有权测试 +10 再扩展其他 Datoviz visual +``` + +**本轮不引入任何 Snapshot。** 当前稳定帧交接只需要轻量 `Scene3D_Frame_Data + immutable shared payload`,没有理由复制整个 +Scene 或 Datoviz backend 状态。 diff --git a/render_3D/render_3D/backend/datoviz/Datoviz_Frame.h b/render_3D/render_3D/backend/datoviz/Datoviz_Frame.h new file mode 100644 index 0000000..766e7a6 --- /dev/null +++ b/render_3D/render_3D/backend/datoviz/Datoviz_Frame.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace renderive::render_3D { + +enum class Datoviz_Frame_Status : std::uint8_t { + rendered, + gpu_unavailable, + invalid_frame, + backend_error, + invalid_usage, +}; + +struct Datoviz_Frame_Result { + Datoviz_Frame_Status status{Datoviz_Frame_Status::backend_error}; + std::uint32_t diagnostic_count{}; + std::uint32_t validation_error_count{}; + + [[nodiscard]] bool succeeded() const noexcept { + return status == Datoviz_Frame_Status::rendered; + } +}; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/backend/datoviz/Datoviz_Renderer.cpp b/render_3D/render_3D/backend/datoviz/Datoviz_Renderer.cpp new file mode 100644 index 0000000..5d0e397 --- /dev/null +++ b/render_3D/render_3D/backend/datoviz/Datoviz_Renderer.cpp @@ -0,0 +1,647 @@ +#include "Datoviz_Renderer.h" +#include "Datoviz_Resources.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::render_3D { +namespace { + +constexpr std::uint64_t external_color_target_id = 0xF000000000000001ULL; +constexpr std::uint64_t runtime_resource_scope_id = 0xF000000000000002ULL; +constexpr std::size_t gpu_frame_slot_count = 3; + +void copy_matrix(const Matrix4& source, mat4 destination) noexcept { + for (std::size_t column = 0; column < 4; ++column) { + for (std::size_t row = 0; row < 4; ++row) + destination[column][row] = source.elements[column * 4 + row]; + } +} + +struct GPU_Frame_Slot { + DvzImages* image{}; + DvzImageViews* view{}; + DvzCommands* commands{}; + DvzFence* fence{}; + DvzSubmit* submit{}; + bool image_created{}; + bool view_created{}; + bool commands_created{}; + bool fence_created{}; + bool submitted{}; + bool layout_initialized{}; + std::uint64_t generation{}; + + bool create( + DvzDevice* device, DvzVma* allocator, DvzQueue* queue, std::uint32_t width, + std::uint32_t height) { + image = dvz_images_create_wrapper(); + view = dvz_image_views_create_wrapper(); + commands = dvz_commands_create_wrapper(); + fence = dvz_fence_create_wrapper(); + submit = dvz_submit_create_wrapper(); + if (!image || !view || !commands || !fence || !submit) + return false; + + dvz_images(device, allocator, VK_IMAGE_TYPE_2D, 1, image); + dvz_images_format(image, VK_FORMAT_R8G8B8A8_UNORM); + dvz_images_size(image, width, height, 1); + dvz_images_usage( + image, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + if (dvz_images_create(image) != 0) + return false; + image_created = true; + + dvz_image_views(image, view); + if (dvz_image_views_create(view) != 0) + return false; + view_created = true; + + dvz_commands(device, queue, 1, commands); + if (dvz_commands_handle(commands) == VK_NULL_HANDLE) + return false; + commands_created = true; + + dvz_fence(device, true, fence); + if (dvz_fence_handle(fence) == VK_NULL_HANDLE) + return false; + fence_created = true; + return true; + } + + void destroy() noexcept { + if (submitted && fence_created) + (void)dvz_fence_wait(fence); + if (fence_created) + dvz_fence_destroy(fence); + if (fence) + dvz_fence_free(fence); + if (commands_created) + dvz_commands_destroy(commands); + if (commands) + dvz_commands_free(commands); + if (view_created) + dvz_image_views_destroy(view); + if (view) + dvz_image_views_free(view); + if (image_created) + dvz_images_destroy(image); + if (image) + dvz_images_free(image); + if (submit) + dvz_submit_free(submit); + *this = {}; + } +}; + +struct Backend_Mesh { + DvzVisual* visual{}; + Mesh_Frame_State state; +}; + +} // namespace + +class Datoviz_Renderer::Impl { +public: + ~Impl() { + destroy_gpu_resources(); + } + + Datoviz_Frame_Result render(const Scene3D_Frame_Data& frame) { + if (frame.scene.width == 0 || frame.scene.height == 0) + return {Datoviz_Frame_Status::invalid_frame}; + std::call_once(render_thread_once_, [this] { + render_thread_ = std::this_thread::get_id(); + }); + if (render_thread_ != std::this_thread::get_id()) + return {Datoviz_Frame_Status::invalid_usage}; + + retain_frame_values(frame); + const bool rebuild = !scene_ || !runtime_ || slots_.size() != gpu_frame_slot_count || + width_ != frame.scene.width || height_ != frame.scene.height; + if (rebuild) { + apply_logical_structure(frame.mesh_structure_changes); + if (!rebuild_backend(frame.scene.width, frame.scene.height)) + return {Datoviz_Frame_Status::gpu_unavailable}; + if (!realize_retained_scene()) + return fail_backend(); + } else { + if (!apply_structure_changes(frame.mesh_structure_changes)) + return fail_backend(); + if (!apply_frame_updates(frame)) + return fail_backend(); + } + + if (frame.camera_change && !apply_camera()) + return fail_backend(); + + discard_unreferenced_values(); + auto result = emit_and_submit(frame.scene); + if (!result.succeeded()) + destroy_render_resources(); + return result; + } + +private: + void retain_frame_values(const Scene3D_Frame_Data& frame) { + for (const auto& texture : frame.texture_updates) + textures_[texture.id] = texture; + for (const auto& material : frame.material_updates) + materials_[material.id] = material; + for (const auto& mesh : frame.mesh_updates) + mesh_states_[mesh.id] = mesh; + if (frame.camera_change) { + if (frame.camera_change->active) + camera_ = frame.camera_change->state; + else + camera_.reset(); + } + } + + void apply_logical_structure(const std::vector& changes) { + for (const auto& change : changes) { + if (change.operation == Mesh_Structure_Operation::attach) + attached_meshes_.insert(change.id); + else + attached_meshes_.erase(change.id); + } + } + + bool rebuild_backend(std::uint32_t width, std::uint32_t height) { + destroy_render_resources(); + const auto fail = [this] { + destroy_render_resources(); + return false; + }; + + if (!gpu_context_) { + DvzGpuCtxConfig gpu_configuration = dvz_gpu_ctx_config(); +#ifndef NDEBUG + dvz_gpu_ctx_config_validation(&gpu_configuration, true); +#endif + VkPhysicalDeviceFeatures features10{}; + features10.samplerAnisotropy = true; + features10.sampleRateShading = true; + dvz_gpu_ctx_config_features10(&gpu_configuration, &features10); + VkPhysicalDeviceVulkan12Features features12{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES}; + features12.timelineSemaphore = true; + dvz_gpu_ctx_config_features12(&gpu_configuration, &features12); + VkPhysicalDeviceVulkan13Features features13{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES}; + features13.dynamicRendering = true; + features13.synchronization2 = true; + dvz_gpu_ctx_config_features13(&gpu_configuration, &features13); + gpu_context_ = dvz_gpu_ctx(&gpu_configuration); + if (!gpu_context_) + return false; + device_ = dvz_gpu_ctx_device(gpu_context_); + allocator_ = dvz_gpu_ctx_alloc(gpu_context_); + queue_ = dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN); + if (!device_ || !allocator_ || !queue_) { + destroy_gpu_resources(); + return false; + } + } + + scene_ = dvz_scene(); + if (!scene_) + return false; + capabilities_ = dvz_capability_snapshot(); + if (dvz_scene_set_capabilities(scene_, &capabilities_) != DVZ_OK) + return fail(); + figure_ = dvz_figure(scene_, width, height, 0); + panel_ = figure_ ? dvz_panel_full(figure_) : nullptr; + if (!figure_ || !panel_) + return fail(); + + DvzDrp2RuntimeConfig runtime_configuration = + dvz_drp2_runtime_vklite_config(device_, allocator_); + runtime_ = dvz_drp2_runtime_vklite(&runtime_configuration); + if (!runtime_) + return fail(); + + slots_.resize(gpu_frame_slot_count); + for (auto& slot : slots_) { + if (!slot.create(device_, allocator_, queue_, width, height)) + return fail(); + } + width_ = width; + height_ = height; + next_slot_ = 0; + return true; + } + + bool realize_retained_scene() { + backend_meshes_.clear(); + for (const auto id : attached_meshes_) { + const auto state = mesh_states_.find(id); + if (state != mesh_states_.end() && !create_visual(state->second)) + return false; + } + return apply_camera(); + } + + bool apply_structure_changes(const std::vector& changes) { + for (const auto& change : changes) { + if (change.operation == Mesh_Structure_Operation::attach) { + if (!attached_meshes_.insert(change.id).second) + continue; + const auto state = mesh_states_.find(change.id); + if (state != mesh_states_.end() && !create_visual(state->second)) + return false; + } else { + attached_meshes_.erase(change.id); + const auto backend = backend_meshes_.find(change.id); + if (backend != backend_meshes_.end()) { + dvz_visual_destroy(backend->second.visual); + backend_meshes_.erase(backend); + std::lock_guard lock(statistics_mutex_); + ++statistics_.visual_destroy_count; + } + } + } + return true; + } + + bool apply_frame_updates(const Scene3D_Frame_Data& frame) { + std::unordered_set updated_meshes; + for (const auto& mesh : frame.mesh_updates) { + if (!attached_meshes_.contains(mesh.id)) + continue; + const auto backend = backend_meshes_.find(mesh.id); + if (backend == backend_meshes_.end()) { + if (!create_visual(mesh)) + return false; + } else { + if (!update_visual(backend->second, mesh)) + return false; + updated_meshes.insert(mesh.id); + } + } + + if (!frame.material_updates.empty()) { + std::unordered_set changed_materials; + for (const auto& material : frame.material_updates) + changed_materials.insert(material.id); + for (auto& [id, backend] : backend_meshes_) { + if (updated_meshes.contains(id) || !backend.state.material_id || + !changed_materials.contains(*backend.state.material_id)) + continue; + if (!apply_material(backend.visual, backend.state.material_id)) + return false; + std::lock_guard lock(statistics_mutex_); + ++statistics_.visual_update_count; + } + } + return true; + } + + void discard_unreferenced_values() { + std::erase_if(mesh_states_, [this](const auto& entry) { + return !attached_meshes_.contains(entry.first); + }); + + std::unordered_set referenced_materials; + for (const auto& [id, mesh] : mesh_states_) { + (void)id; + if (mesh.material_id) + referenced_materials.insert(*mesh.material_id); + } + std::erase_if(materials_, [&referenced_materials](const auto& entry) { + return !referenced_materials.contains(entry.first); + }); + + std::unordered_set referenced_textures; + for (const auto& [id, material] : materials_) { + (void)id; + if (material.texture_id) + referenced_textures.insert(*material.texture_id); + } + std::erase_if(textures_, [&referenced_textures](const auto& entry) { + return !referenced_textures.contains(entry.first); + }); + } + + bool create_visual(const Mesh_Frame_State& state) { + if (backend_meshes_.contains(state.id)) + return true; + DvzVisual* visual = dvz_mesh(scene_, 0); + if (!visual) + return false; + if (dvz_panel_add_visual(panel_, visual, nullptr) != DVZ_OK) { + dvz_visual_destroy(visual); + return false; + } + Backend_Mesh backend{visual, state}; + if (!apply_mesh_state(backend.visual, state)) { + dvz_visual_destroy(visual); + return false; + } + backend_meshes_.emplace(state.id, std::move(backend)); + std::lock_guard lock(statistics_mutex_); + ++statistics_.visual_create_count; + return true; + } + + bool update_visual(Backend_Mesh& backend, const Mesh_Frame_State& state) { + if (!apply_mesh_state(backend.visual, state)) + return false; + backend.state = state; + std::lock_guard lock(statistics_mutex_); + ++statistics_.visual_update_count; + return true; + } + + bool apply_mesh_state(DvzVisual* visual, const Mesh_Frame_State& state) { + if (!visual || !state.geometry || + state.geometry->vertices.size() > std::numeric_limits::max() || + state.geometry->indices.size() > std::numeric_limits::max()) + return false; + + DvzGeometry* geometry = dvz_geometry( + static_cast(state.geometry->vertices.size()), + static_cast(state.geometry->indices.size())); + if (!geometry) + return false; + for (std::size_t index = 0; index < state.geometry->vertices.size(); ++index) { + const auto position = state.geometry->vertices[index]; + geometry->positions[index][0] = position.x; + geometry->positions[index][1] = position.y; + geometry->positions[index][2] = position.z; + const auto normal = state.geometry->normals.empty() + ? Vector3{0.0F, 0.0F, 1.0F} + : state.geometry->normals[index]; + geometry->normals[index][0] = normal.x; + geometry->normals[index][1] = normal.y; + geometry->normals[index][2] = normal.z; + const auto uv = state.geometry->uv.empty() ? Vector2{} : state.geometry->uv[index]; + geometry->texcoords[index][0] = uv.x; + geometry->texcoords[index][1] = uv.y; + geometry->colors[index] = {255, 255, 255, 255}; + } + for (std::size_t index = 0; index < state.geometry->indices.size(); ++index) + geometry->indices[index] = state.geometry->indices[index]; + + const bool geometry_ok = dvz_mesh_set_geometry(visual, geometry) == DVZ_OK; + dvz_geometry_destroy(geometry); + if (!geometry_ok || dvz_visual_set_visible(visual, state.visible) != DVZ_OK) + return false; + mat4 transform{}; + copy_matrix(state.transform, transform); + if (dvz_visual_set_transform(visual, transform) != DVZ_OK) + return false; + return apply_material(visual, state.material_id); + } + + bool apply_material(DvzVisual* visual, std::optional material_id) { + if (!material_id) + return dvz_visual_set_material(visual, nullptr) == DVZ_OK; + const auto found = materials_.find(*material_id); + if (found == materials_.end()) + return false; + const auto& source = found->second; + DvzMaterialDesc material = dvz_standard_material_desc(); + material.base_color_factor[0] = source.base_color.red; + material.base_color_factor[1] = source.base_color.green; + material.base_color_factor[2] = source.base_color.blue; + material.base_color_factor[3] = source.base_color.alpha; + material.opacity = source.base_color.alpha; + material.standard.roughness = source.roughness; + material.standard.specular = source.specular; + material.standard.metallic = source.metallic; + material.standard.emissive[0] = source.emissive.x; + material.standard.emissive[1] = source.emissive.y; + material.standard.emissive[2] = source.emissive.z; + material.standard.rim_strength = source.rim_strength; + return dvz_visual_set_material(visual, &material) == DVZ_OK; + } + + bool apply_camera() { + if (!panel_) + return false; + if (!camera_) + return dvz_panel_set_view3d_desc(panel_, nullptr) == DVZ_OK; + DvzPanelView3DDesc descriptor = dvz_panel_view3d_desc(); + descriptor.view.eye[0] = camera_->eye.x; + descriptor.view.eye[1] = camera_->eye.y; + descriptor.view.eye[2] = camera_->eye.z; + descriptor.view.target[0] = camera_->target.x; + descriptor.view.target[1] = camera_->target.y; + descriptor.view.target[2] = camera_->target.z; + descriptor.view.up[0] = camera_->up.x; + descriptor.view.up[1] = camera_->up.y; + descriptor.view.up[2] = camera_->up.z; + descriptor.projection.type = + camera_->orthographic ? DVZ_CAMERA_ORTHOGRAPHIC : DVZ_CAMERA_PERSPECTIVE; + descriptor.projection.fov_y = camera_->fov_y_radians; + descriptor.projection.near_clip = camera_->near_clip; + descriptor.projection.far_clip = camera_->far_clip; + descriptor.projection.ortho_height = camera_->orthographic_height; + return dvz_panel_set_view3d_desc(panel_, &descriptor) == DVZ_OK; + } + + Datoviz_Frame_Result emit_and_submit(const Scene_Frame_State& scene_state) { + auto& slot = slots_[next_slot_]; + next_slot_ = (next_slot_ + 1U) % slots_.size(); + if (slot.submitted) { + if (!dvz_fence_wait(slot.fence)) + return backend_failure(); + } + + dvz_cmd_reset(slot.commands); + if (dvz_cmd_begin_result(slot.commands) != 0) + return backend_failure(); + if (!slot.layout_initialized) { + DvzBarriers barriers{}; + dvz_barriers(&barriers); + DvzBarrierImage* image_barrier = + dvz_barriers_image(&barriers, dvz_image_handle(slot.image, 0)); + if (!image_barrier) + return backend_failure(); + dvz_barrier_image_stage( + image_barrier, VK_PIPELINE_STAGE_2_NONE, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + dvz_barrier_image_access( + image_barrier, VK_ACCESS_2_NONE, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + dvz_barrier_image_layout( + image_barrier, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + dvz_cmd_barriers(slot.commands, &barriers); + } + + DvzStreamFrame stream_frame{}; + stream_frame.image = dvz_image_handle(slot.image, 0); + stream_frame.command_buffer = dvz_commands_handle(slot.commands); + stream_frame.image_view = dvz_image_views_handle(slot.view, 0); + stream_frame.extent = {scene_state.width, scene_state.height}; + stream_frame.color_format = VK_FORMAT_R8G8B8A8_UNORM; + stream_frame.image_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + stream_frame.usage = + DVZ_STREAM_FRAME_USAGE_RENDER_TARGET | DVZ_STREAM_FRAME_USAGE_COPY_SRC; + stream_frame.command_buffer_recording = true; + stream_frame.image_borrowed = true; + stream_frame.image_view_borrowed = true; + stream_frame.command_buffer_borrowed = true; + stream_frame.handles_dirty = true; + stream_frame.resource_generation = ++slot.generation; + stream_frame.image_valid = true; + stream_frame.memory_fd = -1; + stream_frame.wait_semaphore_fd = -1; + + if (!dvz_drp2_runtime_attach_frame_target( + runtime_, external_color_target_id, &stream_frame)) + return backend_failure(); + + DvzFramePlanEmitConfig emit_configuration = dvz_frame_plan_emit_config(); + emit_configuration.shader_format = DVZ_SCENE_SHADER_FORMAT_GLSL; + emit_configuration.external_color_target = true; + emit_configuration.color_target_id = external_color_target_id; + emit_configuration.color_target_format = DVZ_FORMAT_R8G8B8A8_UNORM; + emit_configuration.target_width = scene_state.width; + emit_configuration.target_height = scene_state.height; + emit_configuration.fullscreen_triangle = true; + emit_configuration.runtime_resource_scope_id = runtime_resource_scope_id; + emit_configuration.clear_color[0] = scene_state.clear_color.red; + emit_configuration.clear_color[1] = scene_state.clear_color.green; + emit_configuration.clear_color[2] = scene_state.clear_color.blue; + emit_configuration.clear_color[3] = scene_state.clear_color.alpha; + + DvzDiagnosticReport report{}; + DvzSceneFrameArtifact* artifact = dvz_figure_emit_frame( + figure_, &capabilities_, &report, &emit_configuration); + if (!artifact) + return backend_failure(report.count); + const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact); + const auto execution = dvz_drp2_runtime_execute(runtime_, stream); + dvz_scene_frame_artifact_destroy(artifact); + if (!execution.ok || dvz_cmd_end_result(slot.commands) != 0) + return backend_failure(report.count); + + dvz_submit(slot.submit); + dvz_submit_command(slot.submit, dvz_commands_handle(slot.commands)); + dvz_fence_reset(slot.fence); + if (dvz_submit_send( + slot.submit, dvz_queue_handle(queue_), dvz_fence_handle(slot.fence)) != + VK_SUCCESS) { + dvz_fence_destroy(slot.fence); + dvz_fence(device_, true, slot.fence); + slot.submitted = false; + return backend_failure(report.count); + } + slot.submitted = true; + slot.layout_initialized = true; + + Datoviz_Frame_Result result{ + Datoviz_Frame_Status::rendered, + report.count, + dvz_gpu_ctx_error_count(gpu_context_), + }; + { + std::lock_guard lock(statistics_mutex_); + ++statistics_.rendered_frame_count; + } + return result; + } + + Datoviz_Frame_Result backend_failure(std::uint32_t diagnostics = 0) const noexcept { + return { + Datoviz_Frame_Status::backend_error, + diagnostics, + gpu_context_ ? dvz_gpu_ctx_error_count(gpu_context_) : 0, + }; + } + + Datoviz_Frame_Result fail_backend(std::uint32_t diagnostics = 0) noexcept { + auto result = backend_failure(diagnostics); + destroy_render_resources(); + return result; + } + + void destroy_render_resources() noexcept { + if (runtime_) + dvz_drp2_runtime_destroy(runtime_); + runtime_ = nullptr; + for (auto& slot : slots_) + slot.destroy(); + slots_.clear(); + backend_meshes_.clear(); + if (scene_) + dvz_scene_destroy(scene_); + scene_ = nullptr; + figure_ = nullptr; + panel_ = nullptr; + } + + void destroy_gpu_resources() noexcept { + destroy_render_resources(); + if (gpu_context_) + dvz_gpu_ctx_destroy(gpu_context_); + gpu_context_ = nullptr; + device_ = nullptr; + allocator_ = nullptr; + queue_ = nullptr; + } + + std::once_flag render_thread_once_; + std::thread::id render_thread_; + DvzGpuCtx* gpu_context_{}; + DvzDevice* device_{}; + DvzVma* allocator_{}; + DvzQueue* queue_{}; + DvzScene* scene_{}; + DvzFigure* figure_{}; + DvzPanel* panel_{}; + DvzDrp2Runtime* runtime_{}; + DvzCapabilitySnapshot capabilities_{}; + std::uint32_t width_{}; + std::uint32_t height_{}; + std::vector slots_; + std::size_t next_slot_{}; + + std::unordered_map textures_; + std::unordered_map materials_; + std::unordered_map mesh_states_; + std::unordered_set attached_meshes_; + std::optional camera_; + std::unordered_map backend_meshes_; + + mutable std::mutex statistics_mutex_; + detail::Datoviz_Renderer_Statistics statistics_; + + friend class detail::Datoviz_Renderer_Access; +}; + +Datoviz_Renderer::Datoviz_Renderer() : impl_(std::make_unique()) {} +Datoviz_Renderer::~Datoviz_Renderer() = default; + +Datoviz_Frame_Result Datoviz_Renderer::render(const Scene3D_Frame_Data& frame) { + return impl_->render(frame); +} + +detail::Datoviz_Renderer_Statistics detail::Datoviz_Renderer_Access::statistics( + const Datoviz_Renderer& renderer) noexcept { + std::lock_guard lock(renderer.impl_->statistics_mutex_); + return renderer.impl_->statistics_; +} + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/backend/datoviz/Datoviz_Renderer.h b/render_3D/render_3D/backend/datoviz/Datoviz_Renderer.h new file mode 100644 index 0000000..3fafe1c --- /dev/null +++ b/render_3D/render_3D/backend/datoviz/Datoviz_Renderer.h @@ -0,0 +1,33 @@ +#pragma once + +#include "Datoviz_Frame.h" +#include "../../scene/Scene_Frame.h" + +#include + +namespace renderive::render_3D { + +namespace detail { +class Datoviz_Renderer_Access; +} + +class Datoviz_Renderer { +public: + Datoviz_Renderer(); + ~Datoviz_Renderer(); + + Datoviz_Renderer(const Datoviz_Renderer&) = delete; + Datoviz_Renderer& operator=(const Datoviz_Renderer&) = delete; + Datoviz_Renderer(Datoviz_Renderer&&) = delete; + Datoviz_Renderer& operator=(Datoviz_Renderer&&) = delete; + + [[nodiscard]] Datoviz_Frame_Result render(const Scene3D_Frame_Data& frame); + +private: + class Impl; + std::unique_ptr impl_; + + friend class detail::Datoviz_Renderer_Access; +}; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/backend/datoviz/Datoviz_Resources.h b/render_3D/render_3D/backend/datoviz/Datoviz_Resources.h new file mode 100644 index 0000000..4e64fe2 --- /dev/null +++ b/render_3D/render_3D/backend/datoviz/Datoviz_Resources.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace renderive::render_3D { + +class Datoviz_Renderer; + +namespace detail { + +struct Datoviz_Renderer_Statistics { + std::uint64_t rendered_frame_count{}; + std::uint64_t visual_create_count{}; + std::uint64_t visual_update_count{}; + std::uint64_t visual_destroy_count{}; +}; + +class Datoviz_Renderer_Access { +public: + [[nodiscard]] static Datoviz_Renderer_Statistics statistics( + const Datoviz_Renderer& renderer) noexcept; +}; + +} // namespace detail +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/base/Object_Access_p.h b/render_3D/render_3D/base/Object_Access_p.h new file mode 100644 index 0000000..fad8d24 --- /dev/null +++ b/render_3D/render_3D/base/Object_Access_p.h @@ -0,0 +1,93 @@ +#pragma once + +#include "../camera/Camera_p.h" +#include "../material/Material_p.h" +#include "../mesh/Mesh_p.h" +#include "../texture/Texture_p.h" + +namespace renderive::render_3D { +class Scene; +struct Scene3D_Frame_Data; +} + +namespace renderive::render_3D::detail { + +class Object_Access { +public: + [[nodiscard]] static Object_Id identity(const Camera& camera) noexcept { + return camera.d_func().id; + } + + [[nodiscard]] static Camera_Internal_State publish(Camera& camera) { + auto& implementation = camera.d_func(); + implementation.publish(); + return implementation.render_state_value(); + } + + [[nodiscard]] static Camera_Internal_State cache(const Camera& camera) { + return camera.d_func().read([](const Camera_Internal_State& state) { return state; }); + } + + [[nodiscard]] static Camera_Internal_State rendered(const Camera& camera) { + return camera.d_func().render_state_value(); + } + + [[nodiscard]] static Texture_Internal_State publish(Texture& texture) { + auto& implementation = texture.d_func(); + implementation.publish(); + return implementation.render_state_value(); + } + + [[nodiscard]] static Object_Id identity(const Texture& texture) noexcept { + return texture.d_func().id; + } + + [[nodiscard]] static Texture_Internal_State cache(const Texture& texture) { + return texture.d_func().read([](const Texture_Internal_State& state) { return state; }); + } + + [[nodiscard]] static Texture_Internal_State rendered(const Texture& texture) { + return texture.d_func().render_state_value(); + } + + [[nodiscard]] static Material_Internal_State publish(Material& material) { + auto& implementation = material.d_func(); + implementation.publish(); + return implementation.render_state_value(); + } + + [[nodiscard]] static Object_Id identity(const Material& material) noexcept { + return material.d_func().id; + } + + [[nodiscard]] static Material_Internal_State cache(const Material& material) { + return material.d_func().read( + [](const Material_Internal_State& state) { return state; }); + } + + [[nodiscard]] static Material_Internal_State rendered(const Material& material) { + return material.d_func().render_state_value(); + } + + [[nodiscard]] static Mesh_Internal_State publish(Mesh& mesh) { + auto& implementation = mesh.d_func(); + implementation.publish(); + return implementation.render_state_value(); + } + + [[nodiscard]] static Object_Id identity(const Mesh& mesh) noexcept { + return mesh.d_func().id; + } + + [[nodiscard]] static Mesh_Internal_State cache(const Mesh& mesh) { + return mesh.d_func().read([](const Mesh_Internal_State& state) { return state; }); + } + + [[nodiscard]] static Mesh_Internal_State rendered(const Mesh& mesh) { + return mesh.d_func().render_state_value(); + } + + [[nodiscard]] static Scene3D_Frame_Data make_frame(Scene& scene); +}; + +} // namespace renderive::render_3D::detail diff --git a/render_3D/render_3D/base/Object_Id.cpp b/render_3D/render_3D/base/Object_Id.cpp new file mode 100644 index 0000000..8243f78 --- /dev/null +++ b/render_3D/render_3D/base/Object_Id.cpp @@ -0,0 +1,12 @@ +#include "Object_Id.h" + +#include + +namespace renderive::render_3D::detail { + +Object_Id allocate_object_id() noexcept { + static std::atomic_uint64_t next_id{1}; + return Object_Id(next_id.fetch_add(1, std::memory_order_relaxed)); +} + +} // namespace renderive::render_3D::detail diff --git a/render_3D/render_3D/base/Object_Id.h b/render_3D/render_3D/base/Object_Id.h new file mode 100644 index 0000000..be9047f --- /dev/null +++ b/render_3D/render_3D/base/Object_Id.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +namespace renderive::render_3D { + +class Object_Id; + +namespace detail { +Object_Id allocate_object_id() noexcept; +} + +class Object_Id { +public: + constexpr Object_Id() noexcept = default; + + [[nodiscard]] constexpr bool valid() const noexcept { + return value_ != 0; + } + + friend constexpr bool operator==(Object_Id, Object_Id) noexcept = default; + friend constexpr auto operator<=>(Object_Id, Object_Id) noexcept = default; + +private: + explicit constexpr Object_Id(std::uint64_t value) noexcept : value_(value) {} + + std::uint64_t value_{}; + + friend Object_Id detail::allocate_object_id() noexcept; + friend struct Object_Id_Hash; +}; + +struct Object_Id_Hash { + [[nodiscard]] constexpr std::size_t operator()(Object_Id id) const noexcept { + return static_cast(id.value_ ^ (id.value_ >> 32U)); + } +}; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/base/Types.h b/render_3D/render_3D/base/Types.h new file mode 100644 index 0000000..77be820 --- /dev/null +++ b/render_3D/render_3D/base/Types.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +namespace renderive::render_3D { + +struct Vector2 { + float x{}; + float y{}; + friend bool operator==(const Vector2&, const Vector2&) = default; +}; + +struct Vector3 { + float x{}; + float y{}; + float z{}; + friend bool operator==(const Vector3&, const Vector3&) = default; +}; + +struct Color4 { + float red{1.0F}; + float green{1.0F}; + float blue{1.0F}; + float alpha{1.0F}; + friend bool operator==(const Color4&, const Color4&) = default; +}; + +struct Matrix4 { + std::array elements{ + 1.0F, 0.0F, 0.0F, 0.0F, + 0.0F, 1.0F, 0.0F, 0.0F, + 0.0F, 0.0F, 1.0F, 0.0F, + 0.0F, 0.0F, 0.0F, 1.0F, + }; + friend bool operator==(const Matrix4&, const Matrix4&) = default; +}; + +struct Geometry_Data { + std::vector vertices; + std::vector normals; + std::vector uv; + std::vector indices; +}; + +struct Image_Data { + std::uint32_t width{}; + std::uint32_t height{}; + std::vector rgba8; +}; + +using Geometry_Handle = std::shared_ptr; +using Image_Handle = std::shared_ptr; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/camera/Camera.cpp b/render_3D/render_3D/camera/Camera.cpp new file mode 100644 index 0000000..a25bf9d --- /dev/null +++ b/render_3D/render_3D/camera/Camera.cpp @@ -0,0 +1,106 @@ +#include "Camera.h" +#include "Camera_p.h" + +#include +#include + +namespace renderive::render_3D { +namespace { + +bool finite(Vector3 value) noexcept { + return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); +} + +void validate_view(Vector3 eye, Vector3 target, Vector3 up) { + if (!finite(eye) || !finite(target) || !finite(up)) + throw std::invalid_argument("camera vectors must be finite"); + if (eye == target) + throw std::invalid_argument("camera eye and target must differ"); + if (up == Vector3{}) + throw std::invalid_argument("camera up vector must be non-zero"); +} + +void validate_projection(float extent, float near_clip, float far_clip, const char* extent_name) { + if (!std::isfinite(extent) || extent <= 0.0F) + throw std::invalid_argument(extent_name); + if (!std::isfinite(near_clip) || !std::isfinite(far_clip) || near_clip <= 0.0F || + far_clip <= near_clip) + throw std::invalid_argument("camera clip range must be finite, positive, and ordered"); +} + +} // namespace + +void Camera_Validator::operator()(const Camera_Properties& properties) const { + validate_view(properties.eye, properties.target, properties.up); + validate_projection( + properties.orthographic ? properties.orthographic_height : properties.fov_y_radians, + properties.near_clip, properties.far_clip, + properties.orthographic ? "orthographic height must be finite and positive" + : "field of view must be finite and positive"); +} + +Camera::Camera(const Camera_Properties& properties) { + Camera_Validator{}(properties); + impl_ = std::make_unique(properties); +} + +Camera::~Camera() = default; + +Object_Id Camera::identity() const noexcept { + return d_func().id; +} + +void Camera::look_at(Vector3 eye, Vector3 target, Vector3 up) { + validate_view(eye, target, up); + d_func().update([&](detail::Camera_Internal_State& state) { + if (state.properties.eye == eye && state.properties.target == target && + state.properties.up == up) + return; + state.properties.eye = eye; + state.properties.target = target; + state.properties.up = up; + ++state.revision; + }); +} + +void Camera::use_perspective(float fov_y_radians, float near_clip, float far_clip) { + validate_projection( + fov_y_radians, near_clip, far_clip, "field of view must be finite and positive"); + d_func().update([&](detail::Camera_Internal_State& state) { + auto& properties = state.properties; + if (!properties.orthographic && properties.fov_y_radians == fov_y_radians && + properties.near_clip == near_clip && properties.far_clip == far_clip) + return; + properties.orthographic = false; + properties.fov_y_radians = fov_y_radians; + properties.near_clip = near_clip; + properties.far_clip = far_clip; + ++state.revision; + }); +} + +void Camera::use_orthographic(float height, float near_clip, float far_clip) { + validate_projection( + height, near_clip, far_clip, "orthographic height must be finite and positive"); + d_func().update([&](detail::Camera_Internal_State& state) { + auto& properties = state.properties; + if (properties.orthographic && properties.orthographic_height == height && + properties.near_clip == near_clip && properties.far_clip == far_clip) + return; + properties.orthographic = true; + properties.orthographic_height = height; + properties.near_clip = near_clip; + properties.far_clip = far_clip; + ++state.revision; + }); +} + +Camera::Impl& Camera::d_func() noexcept { + return *impl_; +} + +const Camera::Impl& Camera::d_func() const noexcept { + return *impl_; +} + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/camera/Camera.h b/render_3D/render_3D/camera/Camera.h new file mode 100644 index 0000000..6d4ba77 --- /dev/null +++ b/render_3D/render_3D/camera/Camera.h @@ -0,0 +1,59 @@ +#pragma once + +#include "../base/Object_Id.h" +#include "../base/Types.h" + +#include + +#include + +namespace renderive::render_3D { + +namespace detail { +class Object_Access; +} + +struct Camera_Properties { + Vector3 eye{0.0F, 0.0F, 3.0F}; + Vector3 target{}; + Vector3 up{0.0F, 1.0F, 0.0F}; + float fov_y_radians{0.785398163F}; + float near_clip{0.01F}; + float far_clip{1000.0F}; + bool orthographic{}; + float orthographic_height{2.0F}; +}; + +struct Camera_Validator { + void operator()(const Camera_Properties& properties) const; +}; + +class Camera { +public: + explicit Camera(const Camera_Properties& properties = {}); + ~Camera(); + + Camera(const Camera&) = delete; + Camera& operator=(const Camera&) = delete; + Camera(Camera&&) = delete; + Camera& operator=(Camera&&) = delete; + + [[nodiscard]] Object_Id identity() const noexcept; + + void look_at(Vector3 eye, Vector3 target, Vector3 up); + void use_perspective(float fov_y_radians, float near_clip, float far_clip); + void use_orthographic(float height, float near_clip, float far_clip); + +private: + class Impl; + std::unique_ptr impl_; + + [[nodiscard]] Impl& d_func() noexcept; + [[nodiscard]] const Impl& d_func() const noexcept; + + friend class detail::Object_Access; +}; + +using Camera_Builder = ::Property_Builder; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/camera/Camera_p.h b/render_3D/render_3D/camera/Camera_p.h new file mode 100644 index 0000000..62edeac --- /dev/null +++ b/render_3D/render_3D/camera/Camera_p.h @@ -0,0 +1,36 @@ +#pragma once + +#include "Camera.h" + +#include + +#include + +namespace renderive::render_3D::detail { + +struct Camera_State_Tag {}; + +struct Camera_Internal_State { + Camera_Properties properties; + std::uint64_t revision{1}; +}; + +} // namespace renderive::render_3D::detail + +namespace renderive::render_3D { + +class Camera::Impl final + : public ::Double_State_Strategy { + using Base = ::Double_State_Strategy; + +public: + explicit Impl(const Camera_Properties& properties) + : Base(detail::Camera_Internal_State{properties, 1}), id(detail::allocate_object_id()) {} + + const Object_Id id; + +private: + friend class detail::Object_Access; +}; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/main.cpp b/render_3D/render_3D/main.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/render_3D/render_3D/material/Material.cpp b/render_3D/render_3D/material/Material.cpp new file mode 100644 index 0000000..0f16582 --- /dev/null +++ b/render_3D/render_3D/material/Material.cpp @@ -0,0 +1,108 @@ +#include "Material.h" +#include "Material_p.h" + +#include +#include + +namespace renderive::render_3D { +namespace { + +void validate_unit(float value, const char* message) { + if (!std::isfinite(value) || value < 0.0F || value > 1.0F) + throw std::invalid_argument(message); +} + +void validate_color(Color4 color) { + validate_unit(color.red, "material red component must be in [0, 1]"); + validate_unit(color.green, "material green component must be in [0, 1]"); + validate_unit(color.blue, "material blue component must be in [0, 1]"); + validate_unit(color.alpha, "material alpha component must be in [0, 1]"); +} + +void validate_emissive(Vector3 emissive) { + if (!std::isfinite(emissive.x) || !std::isfinite(emissive.y) || + !std::isfinite(emissive.z) || emissive.x < 0.0F || emissive.y < 0.0F || + emissive.z < 0.0F) + throw std::invalid_argument("material emission must be finite and non-negative"); +} + +} // namespace + +void Material_Validator::operator()(const Material_Properties& properties) const { + validate_color(properties.base_color); + validate_unit(properties.roughness, "material roughness must be in [0, 1]"); + validate_unit(properties.specular, "material specular must be in [0, 1]"); + validate_unit(properties.metallic, "material metallic must be in [0, 1]"); + validate_unit(properties.rim_strength, "material rim strength must be in [0, 1]"); + validate_emissive(properties.emissive); +} + +Material::Material(const Material_Properties& properties) { + Material_Validator{}(properties); + impl_ = std::make_unique(properties); +} + +Material::~Material() = default; + +Object_Id Material::identity() const noexcept { + return d_func().id; +} + +void Material::tint(Color4 color) { + validate_color(color); + d_func().update([&](detail::Material_Internal_State& state) { + if (state.properties.base_color == color) + return; + state.properties.base_color = color; + ++state.revision; + }); +} + +void Material::set_surface(float roughness, float specular, float metallic) { + validate_unit(roughness, "material roughness must be in [0, 1]"); + validate_unit(specular, "material specular must be in [0, 1]"); + validate_unit(metallic, "material metallic must be in [0, 1]"); + d_func().update([&](detail::Material_Internal_State& state) { + auto& properties = state.properties; + if (properties.roughness == roughness && properties.specular == specular && + properties.metallic == metallic) + return; + properties.roughness = roughness; + properties.specular = specular; + properties.metallic = metallic; + ++state.revision; + }); +} + +void Material::set_emission(Vector3 emissive, float rim_strength) { + validate_emissive(emissive); + validate_unit(rim_strength, "material rim strength must be in [0, 1]"); + d_func().update([&](detail::Material_Internal_State& state) { + auto& properties = state.properties; + if (properties.emissive == emissive && properties.rim_strength == rim_strength) + return; + properties.emissive = emissive; + properties.rim_strength = rim_strength; + ++state.revision; + }); +} + +void Material::use_texture(renderive_Owner texture) { + d_func().update([texture = std::move(texture)]( + detail::Material_Internal_State& state) mutable { + if (state.properties.texture == texture) + return; + state.properties.texture = std::move(texture); + ++state.revision; + }); +} + +Material::Impl& Material::d_func() noexcept { + return *impl_; +} + +const Material::Impl& Material::d_func() const noexcept { + return *impl_; +} + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/material/Material.h b/render_3D/render_3D/material/Material.h new file mode 100644 index 0000000..5977a7b --- /dev/null +++ b/render_3D/render_3D/material/Material.h @@ -0,0 +1,61 @@ +#pragma once + +#include "../base/Object_Id.h" +#include "../base/Types.h" +#include "../texture/Texture.h" + +#include +#include + +#include + +namespace renderive::render_3D { + +namespace detail { +class Object_Access; +} + +struct Material_Properties { + Color4 base_color{}; + float roughness{0.62F}; + float specular{0.34F}; + float metallic{}; + Vector3 emissive{}; + float rim_strength{0.10F}; + renderive_Owner texture; +}; + +struct Material_Validator { + void operator()(const Material_Properties& properties) const; +}; + +class Material { +public: + explicit Material(const Material_Properties& properties = {}); + ~Material(); + + Material(const Material&) = delete; + Material& operator=(const Material&) = delete; + Material(Material&&) = delete; + Material& operator=(Material&&) = delete; + + [[nodiscard]] Object_Id identity() const noexcept; + + void tint(Color4 color); + void set_surface(float roughness, float specular, float metallic); + void set_emission(Vector3 emissive, float rim_strength); + void use_texture(renderive_Owner texture); + +private: + class Impl; + std::unique_ptr impl_; + + [[nodiscard]] Impl& d_func() noexcept; + [[nodiscard]] const Impl& d_func() const noexcept; + + friend class detail::Object_Access; +}; + +using Material_Builder = ::Property_Builder; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/material/Material_p.h b/render_3D/render_3D/material/Material_p.h new file mode 100644 index 0000000..e8d1660 --- /dev/null +++ b/render_3D/render_3D/material/Material_p.h @@ -0,0 +1,36 @@ +#pragma once + +#include "Material.h" + +#include + +#include + +namespace renderive::render_3D::detail { + +struct Material_State_Tag {}; + +struct Material_Internal_State { + Material_Properties properties; + std::uint64_t revision{1}; +}; + +} // namespace renderive::render_3D::detail + +namespace renderive::render_3D { + +class Material::Impl final + : public ::Double_State_Strategy { + using Base = ::Double_State_Strategy; + +public: + explicit Impl(const Material_Properties& properties) + : Base(detail::Material_Internal_State{properties, 1}), id(detail::allocate_object_id()) {} + + const Object_Id id; + +private: + friend class detail::Object_Access; +}; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/mesh/Mesh.cpp b/render_3D/render_3D/mesh/Mesh.cpp new file mode 100644 index 0000000..5777391 --- /dev/null +++ b/render_3D/render_3D/mesh/Mesh.cpp @@ -0,0 +1,104 @@ +#include "Mesh.h" +#include "Mesh_p.h" + +#include +#include +#include + +namespace renderive::render_3D { +namespace { + +void validate_geometry(const Geometry_Handle& geometry) { + if (!geometry || geometry->vertices.size() < 3) + throw std::invalid_argument("mesh geometry requires at least three vertices"); + if (!geometry->normals.empty() && geometry->normals.size() != geometry->vertices.size()) + throw std::invalid_argument("mesh normals must match the vertex count"); + if (!geometry->uv.empty() && geometry->uv.size() != geometry->vertices.size()) + throw std::invalid_argument("mesh UV coordinates must match the vertex count"); + if (geometry->indices.empty()) { + if (geometry->vertices.size() % 3U != 0U) + throw std::invalid_argument("non-indexed mesh vertex count must describe triangles"); + return; + } + if (geometry->indices.size() % 3U != 0U) + throw std::invalid_argument("mesh index count must describe triangles"); + for (const auto index : geometry->indices) { + if (index >= geometry->vertices.size()) + throw std::out_of_range("mesh index exceeds the vertex count"); + } +} + +void validate_transform(const Matrix4& transform) { + for (const auto value : transform.elements) { + if (!std::isfinite(value)) + throw std::invalid_argument("mesh transform must be finite"); + } +} + +} // namespace + +void Mesh_Validator::operator()(const Mesh_Properties& properties) const { + validate_transform(properties.transform); + validate_geometry(properties.geometry); +} + +Mesh::Mesh(const Mesh_Properties& properties) { + Mesh_Validator{}(properties); + impl_ = std::make_unique(properties); +} + +Mesh::~Mesh() = default; + +Object_Id Mesh::identity() const noexcept { + return d_func().id; +} + +void Mesh::place(Matrix4 transform) { + validate_transform(transform); + d_func().update([transform](detail::Mesh_Internal_State& state) { + if (state.properties.transform == transform) + return; + state.properties.transform = transform; + ++state.revision; + }); +} + +void Mesh::show(bool visible) { + d_func().update([visible](detail::Mesh_Internal_State& state) { + if (state.properties.visible == visible) + return; + state.properties.visible = visible; + ++state.revision; + }); +} + +void Mesh::replace_geometry(Geometry_Handle geometry) { + validate_geometry(geometry); + d_func().update([geometry = std::move(geometry)]( + detail::Mesh_Internal_State& state) mutable { + if (state.properties.geometry == geometry) + return; + state.properties.geometry = std::move(geometry); + ++state.revision; + }); +} + +void Mesh::use_material(renderive_Owner material) { + d_func().update([material = std::move(material)]( + detail::Mesh_Internal_State& state) mutable { + if (state.properties.material == material) + return; + state.properties.material = std::move(material); + ++state.revision; + }); +} + +Mesh::Impl& Mesh::d_func() noexcept { + return *impl_; +} + +const Mesh::Impl& Mesh::d_func() const noexcept { + return *impl_; +} + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/mesh/Mesh.h b/render_3D/render_3D/mesh/Mesh.h new file mode 100644 index 0000000..563207c --- /dev/null +++ b/render_3D/render_3D/mesh/Mesh.h @@ -0,0 +1,58 @@ +#pragma once + +#include "../base/Object_Id.h" +#include "../base/Types.h" +#include "../material/Material.h" + +#include +#include + +#include + +namespace renderive::render_3D { + +namespace detail { +class Object_Access; +} + +struct Mesh_Properties { + Matrix4 transform{}; + bool visible{true}; + Geometry_Handle geometry; + renderive_Owner material; +}; + +struct Mesh_Validator { + void operator()(const Mesh_Properties& properties) const; +}; + +class Mesh { +public: + explicit Mesh(const Mesh_Properties& properties); + ~Mesh(); + + Mesh(const Mesh&) = delete; + Mesh& operator=(const Mesh&) = delete; + Mesh(Mesh&&) = delete; + Mesh& operator=(Mesh&&) = delete; + + [[nodiscard]] Object_Id identity() const noexcept; + + void place(Matrix4 transform); + void show(bool visible); + void replace_geometry(Geometry_Handle geometry); + void use_material(renderive_Owner material); + +private: + class Impl; + std::unique_ptr impl_; + + [[nodiscard]] Impl& d_func() noexcept; + [[nodiscard]] const Impl& d_func() const noexcept; + + friend class detail::Object_Access; +}; + +using Mesh_Builder = ::Property_Builder; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/mesh/Mesh_p.h b/render_3D/render_3D/mesh/Mesh_p.h new file mode 100644 index 0000000..98a2085 --- /dev/null +++ b/render_3D/render_3D/mesh/Mesh_p.h @@ -0,0 +1,36 @@ +#pragma once + +#include "Mesh.h" + +#include + +#include + +namespace renderive::render_3D::detail { + +struct Mesh_State_Tag {}; + +struct Mesh_Internal_State { + Mesh_Properties properties; + std::uint64_t revision{1}; +}; + +} // namespace renderive::render_3D::detail + +namespace renderive::render_3D { + +class Mesh::Impl final + : public ::Double_State_Strategy { + using Base = ::Double_State_Strategy; + +public: + explicit Impl(const Mesh_Properties& properties) + : Base(detail::Mesh_Internal_State{properties, 1}), id(detail::allocate_object_id()) {} + + const Object_Id id; + +private: + friend class detail::Object_Access; +}; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/scene/Basic_Scene3D.h b/render_3D/render_3D/scene/Basic_Scene3D.h new file mode 100644 index 0000000..b31d300 --- /dev/null +++ b/render_3D/render_3D/scene/Basic_Scene3D.h @@ -0,0 +1,97 @@ +#pragma once + +#include "Scene.h" +#include "Scene_Frame.h" +#include "../backend/datoviz/Datoviz_Renderer.h" + +#include +#include +#include + +#include +namespace renderive::render_3D { + +namespace detail { + +class Manual_Frame_Control final : public ::Manual_Refresh_Strategy { + using Base = ::Manual_Refresh_Strategy; + +public: + Manual_Frame_Control() : Base({}, configuration()) {} + +private: + static Configuration configuration() noexcept { + Configuration result; + // A published Scene3D frame may contain ordered retained-Datoviz resource transitions. + // It therefore cannot be replaced automatically by a later published frame. + result.replace_pending_frame = false; + return result; + } +}; + +class Low_Latency_Frame_Control final : public ::Low_Latency_Strategy { + using Base = ::Low_Latency_Strategy; + +public: + Low_Latency_Frame_Control() : Base({}, configuration()) {} + +private: + static Configuration configuration() noexcept { + Configuration result; + result.frequency_hz = 60.0; + // A published Scene3D frame may contain ordered retained-Datoviz resource transitions. + // It therefore cannot be replaced automatically by a later published frame. + result.replace_pending_frame = false; + return result; + } +}; + +class Playback_Frame_Control final : public ::Flow_Refresh_Strategy {}; + +template +concept Scene3D_Frame_Control = + std::same_as || + std::same_as || + std::same_as; + +} // namespace detail + +template +class Basic_Scene3D final : public Scene { + static_assert(detail::Scene3D_Frame_Control); + +public: + explicit Basic_Scene3D(const Scene_Properties& properties = {}) : Scene(properties) {} + ~Basic_Scene3D() override = default; + + [[nodiscard]] bool prepare_frame() { + auto painter = frame_control_.acquire_painter(); + if (!painter) + return false; + static_cast(*painter) = this->make_frame_data(); + return true; + } + + [[nodiscard]] bool refresh() + requires std::same_as + { + return frame_control_.refresh(); + } + + [[nodiscard]] bool render_frame() { + auto frame = frame_control_.acquire_renderer(); + if (!frame) + return false; + return renderer_.render(static_cast(*frame)).succeeded(); + } + +private: + Frame_Control frame_control_; + Datoviz_Renderer renderer_; +}; + +using Scene3D = Basic_Scene3D; +using Manual_Scene3D = Basic_Scene3D; +using Playback_Scene3D = Basic_Scene3D; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/scene/Scene.cpp b/render_3D/render_3D/scene/Scene.cpp new file mode 100644 index 0000000..4018529 --- /dev/null +++ b/render_3D/render_3D/scene/Scene.cpp @@ -0,0 +1,244 @@ +#include "Scene.h" +#include "Scene_p.h" +#include "../base/Object_Access_p.h" + +#include +#include +#include +#include + +namespace renderive::render_3D { +namespace { + +void validate_color(Color4 color) { + const float components[]{color.red, color.green, color.blue, color.alpha}; + for (const auto component : components) { + if (!std::isfinite(component) || component < 0.0F || component > 1.0F) + throw std::invalid_argument("scene clear color components must be in [0, 1]"); + } +} + +Camera_Frame_State camera_frame( + Object_Id id, const detail::Camera_Internal_State& source) { + const auto& properties = source.properties; + return { + id, + source.revision, + properties.eye, + properties.target, + properties.up, + properties.fov_y_radians, + properties.near_clip, + properties.far_clip, + properties.orthographic, + properties.orthographic_height, + }; +} + +Material_Frame_State material_frame( + Object_Id id, const detail::Material_Internal_State& source) { + const auto& properties = source.properties; + return { + id, + source.revision, + properties.base_color, + properties.roughness, + properties.specular, + properties.metallic, + properties.emissive, + properties.rim_strength, + properties.texture ? std::optional( + detail::Object_Access::identity(*properties.texture)) + : std::nullopt, + }; +} + +Mesh_Frame_State mesh_frame(Object_Id id, const detail::Mesh_Internal_State& source) { + const auto& properties = source.properties; + return { + id, + source.revision, + properties.transform, + properties.visible, + properties.geometry, + properties.material ? std::optional( + detail::Object_Access::identity(*properties.material)) + : std::nullopt, + }; +} + +} // namespace + +void Scene_Validator::operator()(const Scene_Properties& properties) const { + if (properties.width == 0 || properties.height == 0) + throw std::invalid_argument("scene dimensions must be non-zero"); + validate_color(properties.clear_color); +} + +Scene_Edit::Scene_Edit(Scene& scene) noexcept : scene_(&scene) {} + +void Scene_Edit::attach(renderive_Owner mesh) { + if (!mesh) + throw std::invalid_argument("cannot attach an empty mesh owner"); + const auto id = detail::Object_Access::identity(*mesh); + auto& scene = scene_->d_func(); + const bool inserted = scene.meshes.emplace(id, std::move(mesh)).second; + if (inserted) + scene.pending_mesh_changes.push_back({Mesh_Structure_Operation::attach, id}); +} + +void Scene_Edit::detach(Object_Id mesh_id) { + if (!mesh_id.valid()) + return; + auto& scene = scene_->d_func(); + if (scene.meshes.erase(mesh_id) != 0) + scene.pending_mesh_changes.push_back({Mesh_Structure_Operation::detach, mesh_id}); +} + +void Scene_Edit::set_camera(renderive_Owner camera) { + scene_->d_func().camera = std::move(camera); +} + +Scene::Scene(const Scene_Properties& properties) { + Scene_Validator{}(properties); + impl_ = std::make_unique(properties); +} + +Scene::~Scene() = default; + +void Scene::resize(std::uint32_t width, std::uint32_t height) { + if (width == 0 || height == 0) + throw std::invalid_argument("scene dimensions must be non-zero"); + d_func().update([=](detail::Scene_Internal_State& state) { + if (state.properties.width == width && state.properties.height == height) + return; + state.properties.width = width; + state.properties.height = height; + ++state.revision; + }); +} + +void Scene::clear_with(Color4 color) { + validate_color(color); + d_func().update([=](detail::Scene_Internal_State& state) { + if (state.properties.clear_color == color) + return; + state.properties.clear_color = color; + ++state.revision; + }); +} + +void Scene::update(const std::function& edit) { + if (!edit) + return; + auto& implementation = d_func(); + std::lock_guard lock(implementation.structure_mutex); + Scene_Edit scene_edit(*this); + edit(scene_edit); +} + +Scene3D_Frame_Data Scene::make_frame_data() { + auto& implementation = d_func(); + std::lock_guard lock(implementation.structure_mutex); + + Scene3D_Frame_Data frame; + implementation.publish(); + const auto scene_state = implementation.render_state_value(); + frame.scene = { + scene_state.properties.width, + scene_state.properties.height, + scene_state.properties.clear_color, + scene_state.revision, + }; + + Object_Id next_camera_id; + std::uint64_t next_camera_revision{}; + if (implementation.camera) { + next_camera_id = detail::Object_Access::identity(*implementation.camera); + const auto state = detail::Object_Access::publish(*implementation.camera); + next_camera_revision = state.revision; + if (next_camera_id != implementation.emitted_camera_id || + next_camera_revision != implementation.emitted_camera_revision) + frame.camera_change = Camera_Frame_Change{ + true, camera_frame(next_camera_id, state)}; + } else if (implementation.emitted_camera_id.valid()) { + frame.camera_change = Camera_Frame_Change{}; + } + + std::map mesh_states; + std::map> materials; + std::unordered_map next_mesh_revisions; + for (const auto& [id, mesh] : implementation.meshes) { + auto state = detail::Object_Access::publish(*mesh); + next_mesh_revisions.emplace(id, state.revision); + if (state.properties.material) + materials.emplace( + detail::Object_Access::identity(*state.properties.material), + state.properties.material); + mesh_states.emplace(id, std::move(state)); + } + + std::map material_states; + std::map> textures; + std::unordered_map next_material_revisions; + for (const auto& [id, material] : materials) { + auto state = detail::Object_Access::publish(*material); + next_material_revisions.emplace(id, state.revision); + if (state.properties.texture) + textures.emplace( + detail::Object_Access::identity(*state.properties.texture), + state.properties.texture); + material_states.emplace(id, std::move(state)); + } + + std::unordered_map next_texture_revisions; + for (const auto& [id, texture] : textures) { + const auto state = detail::Object_Access::publish(*texture); + next_texture_revisions.emplace(id, state.revision); + const auto found = implementation.emitted_texture_revisions.find(id); + if (found == implementation.emitted_texture_revisions.end() || + found->second != state.revision) + frame.texture_updates.push_back({id, state.revision, state.image}); + } + + for (const auto& [id, state] : material_states) { + const auto found = implementation.emitted_material_revisions.find(id); + if (found == implementation.emitted_material_revisions.end() || + found->second != state.revision) + frame.material_updates.push_back(material_frame(id, state)); + } + + for (const auto& [id, state] : mesh_states) { + const auto found = implementation.emitted_mesh_revisions.find(id); + if (found == implementation.emitted_mesh_revisions.end() || + found->second != state.revision) + frame.mesh_updates.push_back(mesh_frame(id, state)); + } + + frame.mesh_structure_changes = implementation.pending_mesh_changes; + + implementation.emitted_camera_id = next_camera_id; + implementation.emitted_camera_revision = next_camera_revision; + implementation.emitted_mesh_revisions = std::move(next_mesh_revisions); + implementation.emitted_material_revisions = std::move(next_material_revisions); + implementation.emitted_texture_revisions = std::move(next_texture_revisions); + implementation.pending_mesh_changes.clear(); + return frame; +} + +Scene::Impl& Scene::d_func() noexcept { + return *impl_; +} + +const Scene::Impl& Scene::d_func() const noexcept { + return *impl_; +} + +namespace detail { + +Scene3D_Frame_Data Object_Access::make_frame(Scene& scene) { + return scene.make_frame_data(); +} + +} // namespace detail +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/scene/Scene.h b/render_3D/render_3D/scene/Scene.h new file mode 100644 index 0000000..420fc13 --- /dev/null +++ b/render_3D/render_3D/scene/Scene.h @@ -0,0 +1,85 @@ +#pragma once + +#include "../base/Types.h" +#include "../camera/Camera.h" +#include "../mesh/Mesh.h" + +#include +#include + +#include +#include +#include + +namespace renderive::render_3D { + +namespace detail { +class Object_Access; +} + +template +class Basic_Scene3D; + +struct Scene3D_Frame_Data; +class Scene; + +struct Scene_Properties { + std::uint32_t width{640}; + std::uint32_t height{480}; + Color4 clear_color{0.02F, 0.02F, 0.03F, 1.0F}; +}; + +struct Scene_Validator { + void operator()(const Scene_Properties& properties) const; +}; + +class Scene_Edit { +public: + Scene_Edit(const Scene_Edit&) = delete; + Scene_Edit& operator=(const Scene_Edit&) = delete; + Scene_Edit(Scene_Edit&&) = delete; + Scene_Edit& operator=(Scene_Edit&&) = delete; + + void attach(renderive_Owner mesh); + void detach(Object_Id mesh_id); + void set_camera(renderive_Owner camera); + +private: + explicit Scene_Edit(Scene& scene) noexcept; + + Scene* scene_{}; + + friend class Scene; +}; + +class Scene { +public: + explicit Scene(const Scene_Properties& properties = {}); + virtual ~Scene(); + + Scene(const Scene&) = delete; + Scene& operator=(const Scene&) = delete; + Scene(Scene&&) = delete; + Scene& operator=(Scene&&) = delete; + + void resize(std::uint32_t width, std::uint32_t height); + void clear_with(Color4 color); + void update(const std::function& edit); + +private: + class Impl; + std::unique_ptr impl_; + + [[nodiscard]] Impl& d_func() noexcept; + [[nodiscard]] const Impl& d_func() const noexcept; + [[nodiscard]] Scene3D_Frame_Data make_frame_data(); + + friend class Scene_Edit; + friend class detail::Object_Access; + template + friend class Basic_Scene3D; +}; + +using Scene_Builder = ::Property_Builder; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/scene/Scene_Frame.h b/render_3D/render_3D/scene/Scene_Frame.h new file mode 100644 index 0000000..2a57801 --- /dev/null +++ b/render_3D/render_3D/scene/Scene_Frame.h @@ -0,0 +1,83 @@ +#pragma once + +#include "../base/Object_Id.h" +#include "../base/Types.h" + +#include +#include +#include + +namespace renderive::render_3D { + +struct Scene_Frame_State { + std::uint32_t width{640}; + std::uint32_t height{480}; + Color4 clear_color{0.02F, 0.02F, 0.03F, 1.0F}; + std::uint64_t revision{1}; +}; + +struct Camera_Frame_State { + Object_Id id; + std::uint64_t revision{}; + Vector3 eye; + Vector3 target; + Vector3 up; + float fov_y_radians{}; + float near_clip{}; + float far_clip{}; + bool orthographic{}; + float orthographic_height{}; +}; + +struct Camera_Frame_Change { + bool active{}; + Camera_Frame_State state; +}; + +struct Texture_Frame_State { + Object_Id id; + std::uint64_t revision{}; + Image_Handle image; +}; + +struct Material_Frame_State { + Object_Id id; + std::uint64_t revision{}; + Color4 base_color; + float roughness{}; + float specular{}; + float metallic{}; + Vector3 emissive; + float rim_strength{}; + std::optional texture_id; +}; + +struct Mesh_Frame_State { + Object_Id id; + std::uint64_t revision{}; + Matrix4 transform; + bool visible{true}; + Geometry_Handle geometry; + std::optional material_id; +}; + +enum class Mesh_Structure_Operation : std::uint8_t { + attach, + detach, +}; + +struct Mesh_Structure_Change { + Mesh_Structure_Operation operation{}; + Object_Id id; +}; + +struct Scene3D_Frame_Data { + Scene_Frame_State scene; + std::optional camera_change; + std::vector texture_updates; + std::vector material_updates; + std::vector mesh_updates; + std::vector mesh_structure_changes; +}; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/scene/Scene_p.h b/render_3D/render_3D/scene/Scene_p.h new file mode 100644 index 0000000..7a242e1 --- /dev/null +++ b/render_3D/render_3D/scene/Scene_p.h @@ -0,0 +1,49 @@ +#pragma once + +#include "Scene.h" +#include "Scene_Frame.h" + +#include + +#include +#include +#include +#include + +namespace renderive::render_3D::detail { + +struct Scene_State_Tag {}; + +struct Scene_Internal_State { + Scene_Properties properties; + std::uint64_t revision{1}; +}; + +} // namespace renderive::render_3D::detail + +namespace renderive::render_3D { + +class Scene::Impl final + : public ::Double_State_Strategy { + using Base = ::Double_State_Strategy; + +public: + explicit Impl(const Scene_Properties& properties) + : Base(detail::Scene_Internal_State{properties, 1}) {} + + mutable std::mutex structure_mutex; + std::map> meshes; + renderive_Owner camera; + std::vector pending_mesh_changes; + + std::unordered_map emitted_mesh_revisions; + std::unordered_map emitted_material_revisions; + std::unordered_map emitted_texture_revisions; + Object_Id emitted_camera_id; + std::uint64_t emitted_camera_revision{}; + +private: + friend class Scene; +}; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/texture/Texture.cpp b/render_3D/render_3D/texture/Texture.cpp new file mode 100644 index 0000000..f7edf5a --- /dev/null +++ b/render_3D/render_3D/texture/Texture.cpp @@ -0,0 +1,51 @@ +#include "Texture.h" +#include "Texture_p.h" + +#include +#include +#include + +namespace renderive::render_3D { + +void Texture_Validator::operator()(const Texture_Properties& properties) const { + if (!properties.image) + throw std::invalid_argument("texture image is required"); + if (properties.image->width == 0 || properties.image->height == 0) + throw std::invalid_argument("texture dimensions must be non-zero"); + const auto pixel_count = static_cast(properties.image->width) * + static_cast(properties.image->height); + if (pixel_count > std::numeric_limits::max() / 4U || + properties.image->rgba8.size() != static_cast(pixel_count * 4U)) + throw std::invalid_argument("texture image must contain tightly packed RGBA8 pixels"); +} + +Texture::Texture(const Texture_Properties& properties) { + Texture_Validator{}(properties); + impl_ = std::make_unique(properties.image); +} + +Texture::~Texture() = default; + +Object_Id Texture::identity() const noexcept { + return d_func().id; +} + +void Texture::replace_image(Image_Handle image) { + Texture_Validator{}({image}); + d_func().update([image = std::move(image)](detail::Texture_Internal_State& state) mutable { + if (state.image == image) + return; + state.image = std::move(image); + ++state.revision; + }); +} + +Texture::Impl& Texture::d_func() noexcept { + return *impl_; +} + +const Texture::Impl& Texture::d_func() const noexcept { + return *impl_; +} + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/texture/Texture.h b/render_3D/render_3D/texture/Texture.h new file mode 100644 index 0000000..ea7d596 --- /dev/null +++ b/render_3D/render_3D/texture/Texture.h @@ -0,0 +1,49 @@ +#pragma once + +#include "../base/Object_Id.h" +#include "../base/Types.h" + +#include + +#include + +namespace renderive::render_3D { + +namespace detail { +class Object_Access; +} + +struct Texture_Properties { + Image_Handle image; +}; + +struct Texture_Validator { + void operator()(const Texture_Properties& properties) const; +}; + +class Texture { +public: + explicit Texture(const Texture_Properties& properties); + ~Texture(); + + Texture(const Texture&) = delete; + Texture& operator=(const Texture&) = delete; + Texture(Texture&&) = delete; + Texture& operator=(Texture&&) = delete; + + [[nodiscard]] Object_Id identity() const noexcept; + void replace_image(Image_Handle image); + +private: + class Impl; + std::unique_ptr impl_; + + [[nodiscard]] Impl& d_func() noexcept; + [[nodiscard]] const Impl& d_func() const noexcept; + + friend class detail::Object_Access; +}; + +using Texture_Builder = ::Property_Builder; + +} // namespace renderive::render_3D diff --git a/render_3D/render_3D/texture/Texture_p.h b/render_3D/render_3D/texture/Texture_p.h new file mode 100644 index 0000000..c7a65c3 --- /dev/null +++ b/render_3D/render_3D/texture/Texture_p.h @@ -0,0 +1,37 @@ +#pragma once + +#include "Texture.h" + +#include + +#include + +namespace renderive::render_3D::detail { + +struct Texture_State_Tag {}; + +struct Texture_Internal_State { + Image_Handle image; + std::uint64_t revision{1}; +}; + +} // namespace renderive::render_3D::detail + +namespace renderive::render_3D { + +class Texture::Impl final + : public ::Double_State_Strategy { + using Base = ::Double_State_Strategy; + +public: + explicit Impl(Image_Handle image) + : Base(detail::Texture_Internal_State{std::move(image), 1}), + id(detail::allocate_object_id()) {} + + const Object_Id id; + +private: + friend class detail::Object_Access; +}; + +} // namespace renderive::render_3D diff --git a/render_3D/tests/Datoviz_Renderer_Tests.cpp b/render_3D/tests/Datoviz_Renderer_Tests.cpp new file mode 100644 index 0000000..2e04788 --- /dev/null +++ b/render_3D/tests/Datoviz_Renderer_Tests.cpp @@ -0,0 +1,109 @@ +#include "Test_Fixtures.h" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace renderive::render_3D { + +TEST(Render3DDatovizRenderer, MeshAttachUpdateDetachUsesOneRetainedVisual) { + Scene scene({.width = 96, .height = 96}); + auto mesh = test::mesh_owner(); + auto camera = test::camera_owner(); + const auto mesh_id = mesh->identity(); + std::weak_ptr mesh_lifetime = mesh.share(); + scene.update([&](Scene_Edit& edit) { + edit.attach(mesh); + edit.set_camera(camera); + }); + + Datoviz_Renderer renderer; + auto result = renderer.render(detail::Object_Access::make_frame(scene)); + if (result.status == Datoviz_Frame_Status::gpu_unavailable) + GTEST_SKIP() << "Vulkan GPU context is unavailable"; + ASSERT_TRUE(result.succeeded()); + EXPECT_EQ(result.validation_error_count, 0U); + auto statistics = detail::Datoviz_Renderer_Access::statistics(renderer); + EXPECT_EQ(statistics.visual_create_count, 1U); + EXPECT_EQ(statistics.visual_destroy_count, 0U); + + Matrix4 translated; + translated.elements[12] = 0.1F; + mesh->place(translated); + result = renderer.render(detail::Object_Access::make_frame(scene)); + ASSERT_TRUE(result.succeeded()); + EXPECT_EQ(result.validation_error_count, 0U); + statistics = detail::Datoviz_Renderer_Access::statistics(renderer); + EXPECT_EQ(statistics.visual_create_count, 1U); + EXPECT_GE(statistics.visual_update_count, 1U); + + scene.update([&](Scene_Edit& edit) { edit.detach(mesh_id); }); + mesh.reset(); + EXPECT_TRUE(mesh_lifetime.expired()); + EXPECT_EQ( + detail::Datoviz_Renderer_Access::statistics(renderer).visual_destroy_count, 0U); + result = renderer.render(detail::Object_Access::make_frame(scene)); + ASSERT_TRUE(result.succeeded()); + EXPECT_EQ(result.validation_error_count, 0U); + statistics = detail::Datoviz_Renderer_Access::statistics(renderer); + EXPECT_EQ(statistics.visual_create_count, 1U); + EXPECT_EQ(statistics.visual_destroy_count, 1U); + EXPECT_EQ(statistics.rendered_frame_count, 3U); +} + +TEST(Render3DDatovizRenderer, RenderingFrameDoesNotLockTheMutableObjectCache) { + Scene scene({.width = 64, .height = 64}); + auto mesh = test::mesh_owner(); + scene.update([&](Scene_Edit& edit) { edit.attach(mesh); }); + const auto frame = detail::Object_Access::make_frame(scene); + + Datoviz_Renderer renderer; + auto rendering = std::async(std::launch::async, [&] { return renderer.render(frame); }); + for (int index = 0; index < 1000; ++index) + mesh->show((index & 1) == 0); + const auto result = rendering.get(); + if (result.status == Datoviz_Frame_Status::gpu_unavailable) + GTEST_SKIP() << "Vulkan GPU context is unavailable"; + EXPECT_TRUE(result.succeeded()); +} + +TEST(Render3DDatovizRenderer, RendererRejectsRetainedMutationFromAnotherThread) { + Scene scene({.width = 64, .height = 64}); + Datoviz_Renderer renderer; + const auto frame = detail::Object_Access::make_frame(scene); + const auto first = renderer.render(frame); + if (first.status == Datoviz_Frame_Status::gpu_unavailable) + GTEST_SKIP() << "Vulkan GPU context is unavailable"; + ASSERT_TRUE(first.succeeded()); + + const auto second = std::async(std::launch::async, [&] { return renderer.render(frame); }).get(); + EXPECT_EQ(second.status, Datoviz_Frame_Status::invalid_usage); +} + +TEST(Render3DDatovizRenderer, HotPathContainsNoDeviceOrQueueWideWait) { + const auto path = std::filesystem::path(__FILE__).parent_path().parent_path() / + "render_3D" / "backend" / "datoviz" / "Datoviz_Renderer.cpp"; + std::ifstream input(path, std::ios::binary); + ASSERT_TRUE(input.good()); + const std::string source{ + std::istreambuf_iterator(input), std::istreambuf_iterator()}; + EXPECT_EQ(source.find("dvz_device_wait("), std::string::npos); + EXPECT_EQ(source.find("dvz_queue_wait("), std::string::npos); + EXPECT_EQ(source.find("Object_Access"), std::string::npos); + EXPECT_EQ(source.find("Mesh::Impl"), std::string::npos); + EXPECT_EQ(source.find("Material::Impl"), std::string::npos); + EXPECT_EQ(source.find("Texture::Impl"), std::string::npos); + EXPECT_EQ(source.find("Camera::Impl"), std::string::npos); + EXPECT_EQ(source.find("_p.h"), std::string::npos); +} + +} // namespace renderive::render_3D diff --git a/render_3D/tests/Object_State_Tests.cpp b/render_3D/tests/Object_State_Tests.cpp new file mode 100644 index 0000000..69cc36a --- /dev/null +++ b/render_3D/tests/Object_State_Tests.cpp @@ -0,0 +1,49 @@ +#include "Test_Fixtures.h" + +#include + +#include + +namespace renderive::render_3D { + +TEST(Render3DObjectState, MeshMutationStaysInCacheUntilPublish) { + Mesh mesh({.geometry = test::triangle_geometry()}); + const auto initial = detail::Object_Access::publish(mesh); + + Matrix4 translated; + translated.elements[12] = 2.0F; + mesh.place(translated); + + const auto cached = detail::Object_Access::cache(mesh); + const auto still_rendered = detail::Object_Access::rendered(mesh); + EXPECT_EQ(cached.properties.transform, translated); + EXPECT_EQ(cached.revision, initial.revision + 1); + EXPECT_EQ(still_rendered.properties.transform, initial.properties.transform); + EXPECT_EQ(still_rendered.revision, initial.revision); + + const auto published = detail::Object_Access::publish(mesh); + EXPECT_EQ(published.properties.transform, translated); + EXPECT_EQ(published.revision, cached.revision); +} + +TEST(Render3DObjectState, ImmutableGeometryIsSharedAcrossDoubleStatePublication) { + const auto geometry = test::triangle_geometry(); + Mesh mesh({.geometry = geometry}); + const auto published = detail::Object_Access::publish(mesh); + EXPECT_EQ(published.properties.geometry, geometry); + + const auto replacement = test::triangle_geometry(0.1F); + mesh.replace_geometry(replacement); + EXPECT_EQ(detail::Object_Access::rendered(mesh).properties.geometry, geometry); + EXPECT_EQ(detail::Object_Access::publish(mesh).properties.geometry, replacement); +} + +TEST(Render3DObjectState, BuildersValidateWithoutCreatingBackendResources) { + Mesh_Builder builder; + EXPECT_THROW(builder.build(), std::invalid_argument); + const auto mesh = builder.set<&Mesh_Properties::geometry>(test::triangle_geometry()).build(); + ASSERT_NE(mesh, nullptr); + EXPECT_TRUE(mesh->identity().valid()); +} + +} // namespace renderive::render_3D diff --git a/render_3D/tests/Scene_Frame_Control_Tests.cpp b/render_3D/tests/Scene_Frame_Control_Tests.cpp new file mode 100644 index 0000000..8c6394e --- /dev/null +++ b/render_3D/tests/Scene_Frame_Control_Tests.cpp @@ -0,0 +1,46 @@ +#include + +#include + +#include +#include + +namespace renderive::render_3D { +using namespace std::chrono_literals; + +TEST(Render3DFrameControl, LowLatencyBackpressuresInsteadOfReplacingPublishedFrame) { + Scene3D scene({.width = 64, .height = 64}); + ASSERT_TRUE(scene.prepare_frame()); + auto second_prepare = std::async(std::launch::async, [&] { return scene.prepare_frame(); }); + EXPECT_EQ(second_prepare.wait_for(100ms), std::future_status::timeout); + + EXPECT_TRUE(scene.render_frame()); + ASSERT_EQ(second_prepare.wait_for(5s), std::future_status::ready); + EXPECT_TRUE(second_prepare.get()); + EXPECT_TRUE(scene.render_frame()); +} + +TEST(Render3DFrameControl, ManualRefreshReleasesTheWaitingPainter) { + Manual_Scene3D scene({.width = 64, .height = 64}); + ASSERT_TRUE(scene.prepare_frame()); + auto second_prepare = std::async(std::launch::async, [&] { return scene.prepare_frame(); }); + EXPECT_EQ(second_prepare.wait_for(100ms), std::future_status::timeout); + + ASSERT_TRUE(scene.refresh()); + ASSERT_EQ(second_prepare.wait_for(5s), std::future_status::ready); + EXPECT_TRUE(second_prepare.get()); + EXPECT_TRUE(scene.render_frame()); + ASSERT_TRUE(scene.refresh()); + EXPECT_TRUE(scene.render_frame()); +} + +TEST(Render3DFrameControl, PlaybackPreservesQueuedFrames) { + Playback_Scene3D scene({.width = 64, .height = 64}); + ASSERT_TRUE(scene.prepare_frame()); + ASSERT_TRUE(scene.prepare_frame()); + EXPECT_TRUE(scene.render_frame()); + EXPECT_TRUE(scene.render_frame()); + EXPECT_FALSE(scene.render_frame()); +} + +} // namespace renderive::render_3D diff --git a/render_3D/tests/Scene_Frame_Tests.cpp b/render_3D/tests/Scene_Frame_Tests.cpp new file mode 100644 index 0000000..468978d --- /dev/null +++ b/render_3D/tests/Scene_Frame_Tests.cpp @@ -0,0 +1,107 @@ +#include "Test_Fixtures.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace renderive::render_3D { + +TEST(Render3DSceneFrame, AttachOwnsMeshUntilDetach) { + Scene scene; + auto mesh = test::mesh_owner(); + const auto id = mesh->identity(); + std::weak_ptr lifetime = mesh.share(); + + scene.update([&](Scene_Edit& edit) { edit.attach(mesh); }); + mesh.reset(); + EXPECT_FALSE(lifetime.expired()); + + const auto frame = detail::Object_Access::make_frame(scene); + ASSERT_EQ(frame.mesh_structure_changes.size(), 1U); + EXPECT_EQ(frame.mesh_structure_changes.front().operation, Mesh_Structure_Operation::attach); + ASSERT_EQ(frame.mesh_updates.size(), 1U); + EXPECT_EQ(frame.mesh_updates.front().id, id); + EXPECT_FALSE(lifetime.expired()); + + scene.update([&](Scene_Edit& edit) { edit.detach(id); }); + EXPECT_TRUE(lifetime.expired()); + const auto detached = detail::Object_Access::make_frame(scene); + ASSERT_EQ(detached.mesh_structure_changes.size(), 1U); + EXPECT_EQ(detached.mesh_structure_changes.front().operation, Mesh_Structure_Operation::detach); +} + +TEST(Render3DSceneFrame, PublishedFrameIsIndependentFromLaterCacheMutation) { + Scene scene; + auto mesh = test::mesh_owner(); + scene.update([&](Scene_Edit& edit) { edit.attach(mesh); }); + const auto first = detail::Object_Access::make_frame(scene); + ASSERT_EQ(first.mesh_updates.size(), 1U); + + Matrix4 translated; + translated.elements[13] = 3.0F; + mesh->place(translated); + EXPECT_NE(first.mesh_updates.front().transform, translated); + + const auto second = detail::Object_Access::make_frame(scene); + ASSERT_EQ(second.mesh_updates.size(), 1U); + EXPECT_EQ(second.mesh_updates.front().transform, translated); + EXPECT_NE(first.mesh_updates.front().transform, second.mesh_updates.front().transform); +} + +TEST(Render3DSceneFrame, ImplRelationshipsBecomePointerFreeFrameValues) { + Scene scene; + auto image = std::make_shared(); + image->width = 1; + image->height = 1; + image->rgba8 = {255, 255, 255, 255}; + auto texture = renderive_Owner::make(Texture_Properties{image}); + auto material = test::material_owner(); + material->use_texture(texture); + auto mesh = test::mesh_owner(test::triangle_geometry(), material); + auto camera = test::camera_owner(); + scene.update([&](Scene_Edit& edit) { + edit.attach(mesh); + edit.set_camera(camera); + }); + + const auto frame = detail::Object_Access::make_frame(scene); + ASSERT_EQ(frame.texture_updates.size(), 1U); + ASSERT_EQ(frame.material_updates.size(), 1U); + ASSERT_EQ(frame.mesh_updates.size(), 1U); + ASSERT_TRUE(frame.camera_change); + EXPECT_TRUE(frame.camera_change->active); + EXPECT_EQ(frame.material_updates.front().texture_id, texture->identity()); + EXPECT_EQ(frame.mesh_updates.front().material_id, material->identity()); + EXPECT_EQ(frame.mesh_updates.front().geometry, detail::Object_Access::rendered(*mesh).properties.geometry); + + static_assert(!std::is_pointer_v); + static_assert(std::is_copy_constructible_v); +} + +TEST(Render3DSceneFrame, PublicFrameHeaderDoesNotExposeImplOrBackendTypes) { + const auto path = std::filesystem::path(__FILE__).parent_path().parent_path() / + "render_3D" / "scene" / "Scene_Frame.h"; + std::ifstream input(path, std::ios::binary); + ASSERT_TRUE(input.good()); + const std::string source{ + std::istreambuf_iterator(input), std::istreambuf_iterator()}; + EXPECT_EQ(source.find("Dvz"), std::string::npos); + EXPECT_EQ(source.find("::Impl"), std::string::npos); + EXPECT_EQ(source.find("Snapshot"), std::string::npos); + EXPECT_EQ(source.find("Mesh*"), std::string::npos); + EXPECT_EQ(source.find("Camera*"), std::string::npos); + EXPECT_EQ(source.find("Material*"), std::string::npos); + EXPECT_EQ(source.find("Texture*"), std::string::npos); + EXPECT_EQ(source.find("renderive_Owner"), std::string::npos); + EXPECT_EQ(source.find("_p.h"), std::string::npos); +} + +} // namespace renderive::render_3D diff --git a/render_3D/tests/Test_Fixtures.h b/render_3D/tests/Test_Fixtures.h new file mode 100644 index 0000000..3ad720c --- /dev/null +++ b/render_3D/tests/Test_Fixtures.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +#include + +#include + +namespace renderive::render_3D::test { + +inline Geometry_Handle triangle_geometry(float x_offset = 0.0F) { + auto geometry = std::make_shared(); + geometry->vertices = { + {-0.6F + x_offset, -0.5F, 0.0F}, + {0.6F + x_offset, -0.5F, 0.0F}, + {x_offset, 0.6F, 0.0F}, + }; + geometry->normals = { + {0.0F, 0.0F, 1.0F}, + {0.0F, 0.0F, 1.0F}, + {0.0F, 0.0F, 1.0F}, + }; + geometry->indices = {0, 1, 2}; + return geometry; +} + +inline renderive_Owner material_owner() { + return renderive_Owner::make(Material_Properties{ + .base_color = {0.85F, 0.25F, 0.15F, 1.0F}, + .roughness = 0.55F, + .specular = 0.30F, + .metallic = 0.05F, + }); +} + +inline renderive_Owner mesh_owner( + Geometry_Handle geometry = triangle_geometry(), + renderive_Owner material = material_owner()) { + return renderive_Owner::make(Mesh_Properties{ + .geometry = std::move(geometry), + .material = std::move(material), + }); +} + +inline renderive_Owner camera_owner() { + return renderive_Owner::make(); +} + +} // namespace renderive::render_3D::test diff --git a/render_3D/调试.md b/render_3D/调试.md index 6fa1821..b34df86 100644 --- a/render_3D/调试.md +++ b/render_3D/调试.md @@ -2,4 +2,7 @@ D:\ae\tools 可能会有有用的工具 -运行前使用环境脚本 env.ps1 \ No newline at end of file +运行前使用环境脚本 env.ps1 + +"C:\Program Files\JetBrains\CLion 2026.1\bin\cmake\win\x64\bin\cmake.exe" --build D: +\ae\proj\Renderive\cmake-build-vs2022_debug