From d99da004d0b930ccbbe29cc3547ba6965cf2e0e0 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 13 Aug 2026 19:47:37 +0800 Subject: [PATCH] =?UTF-8?q?3D=20=E7=AC=AC=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/datoviz.cmake | 71 ++++- render_3D/CMakeLists.txt | 58 ++-- render_3D/cmake/computer_graphics.cmake | 238 -------------- render_3D/cmake/dependencies.cmake | 26 ++ render_3D/cmake/export.cmake | 3 +- render_3D/cmake/module/FindPThreads4W.cmake | 26 -- .../package/PThreads4W/PThreads4WConfig.cmake | 31 ++ render_3D/render_3D/Render_3D.cpp | 300 ++++++++++++++++++ render_3D/render_3D/Render_3D.h | 147 +++++++++ .../render_3D/detail/Datoviz_Backend.cpp | 155 +++++++++ render_3D/render_3D/detail/Datoviz_Backend.h | 53 ++++ .../detail/Datoviz_Frame_Executor.cpp | 92 ++++++ .../render_3D/detail/Datoviz_Frame_Executor.h | 18 ++ .../detail/Datoviz_Scene_Binding.cpp | 114 +++++++ .../render_3D/detail/Datoviz_Scene_Binding.h | 40 +++ .../render_3D/detail/Frame_Target_Pool.cpp | 237 ++++++++++++++ .../render_3D/detail/Frame_Target_Pool.h | 72 +++++ .../detail/Ordered_Artifact_Transaction.h | 45 +++ .../detail/Ordered_Backend_Shutdown.h | 23 ++ render_3D/render_3D/detail/Render_3D_Core.h | 139 ++++++++ render_3D/render_3D/detail/Render_Domain.h | 75 +++++ render_3D/render_3D/export.h | 3 + render_3D/render_3D/main.cpp | 0 render_3D/tests/Datoviz_Transaction_Tests.cpp | 96 ++++++ render_3D/tests/Render_3D_Core_Tests.cpp | 160 ++++++++++ .../tests/Render_3D_Integration_Tests.cpp | 216 +++++++++++++ 26 files changed, 2141 insertions(+), 297 deletions(-) delete mode 100644 render_3D/cmake/computer_graphics.cmake create mode 100644 render_3D/cmake/dependencies.cmake delete mode 100644 render_3D/cmake/module/FindPThreads4W.cmake create mode 100644 render_3D/cmake/package/PThreads4W/PThreads4WConfig.cmake create mode 100644 render_3D/render_3D/Render_3D.cpp create mode 100644 render_3D/render_3D/Render_3D.h create mode 100644 render_3D/render_3D/detail/Datoviz_Backend.cpp create mode 100644 render_3D/render_3D/detail/Datoviz_Backend.h create mode 100644 render_3D/render_3D/detail/Datoviz_Frame_Executor.cpp create mode 100644 render_3D/render_3D/detail/Datoviz_Frame_Executor.h create mode 100644 render_3D/render_3D/detail/Datoviz_Scene_Binding.cpp create mode 100644 render_3D/render_3D/detail/Datoviz_Scene_Binding.h create mode 100644 render_3D/render_3D/detail/Frame_Target_Pool.cpp create mode 100644 render_3D/render_3D/detail/Frame_Target_Pool.h create mode 100644 render_3D/render_3D/detail/Ordered_Artifact_Transaction.h create mode 100644 render_3D/render_3D/detail/Ordered_Backend_Shutdown.h create mode 100644 render_3D/render_3D/detail/Render_3D_Core.h create mode 100644 render_3D/render_3D/detail/Render_Domain.h create mode 100644 render_3D/render_3D/export.h delete mode 100644 render_3D/render_3D/main.cpp create mode 100644 render_3D/tests/Datoviz_Transaction_Tests.cpp create mode 100644 render_3D/tests/Render_3D_Core_Tests.cpp create mode 100644 render_3D/tests/Render_3D_Integration_Tests.cpp diff --git a/cmake/datoviz.cmake b/cmake/datoviz.cmake index e29b65f..f76bce6 100644 --- a/cmake/datoviz.cmake +++ b/cmake/datoviz.cmake @@ -1,14 +1,32 @@ include_guard(GLOBAL) + +if (WIN32 AND (NOT DEFINED ENV{VULKAN_SDK} OR NOT EXISTS "$ENV{VULKAN_SDK}/Include/vulkan/vulkan.h")) + file(GLOB _renderive_vulkan_sdk_candidates LIST_DIRECTORIES TRUE "C:/VulkanSDK/*") + list(SORT _renderive_vulkan_sdk_candidates COMPARE NATURAL ORDER DESCENDING) + foreach (_renderive_vulkan_sdk_candidate IN LISTS _renderive_vulkan_sdk_candidates) + if (EXISTS "${_renderive_vulkan_sdk_candidate}/Include/vulkan/vulkan.h" AND + EXISTS "${_renderive_vulkan_sdk_candidate}/Bin/glslc.exe") + set(ENV{VULKAN_SDK} "${_renderive_vulkan_sdk_candidate}") + break() + endif () + endforeach () + unset(_renderive_vulkan_sdk_candidate) + unset(_renderive_vulkan_sdk_candidates) +endif () + block() set(datoviz_opts + -DBUILD_SHARED_LIBS=OFF + "-DCMAKE_C_FLAGS=-utf-8" + "-DCMAKE_CXX_FLAGS=-utf-8" -DDVZ_BUILD_CORE=ON -DDVZ_BUILD_CONTROLLER=ON -DDVZ_BUILD_VK=ON - -DDVZ_BUILD_CANVAS=ON + -DDVZ_BUILD_CANVAS=OFF -DDVZ_BUILD_DRP2=ON -DDVZ_BUILD_WEBGPU=OFF -DDVZ_BUILD_SCENE=ON - -DDVZ_BUILD_APP=ON + -DDVZ_BUILD_APP=OFF -DDVZ_BUILD_GUI=OFF -DDVZ_BUILD_TESTING=OFF -DDVZ_BUILD_EXAMPLES=OFF @@ -17,35 +35,64 @@ block() -DDVZ_ENABLE_CUDA=OFF -DDVZ_ENABLE_KVAZAAR=OFF -DDVZ_ENABLE_QT_BRIDGE=OFF - -DDVZ_ENABLE_SHADERC=OFF + -DDVZ_ENABLE_SHADERC=ON -DDVZ_WITH_FREETYPE=OFF -DDVZ_WITH_MSDF_ATLAS=OFF -DDVZ_WITH_MSDF_SVG=OFF -DDVZ_WITH_ZLIB=OFF - -DDVZ_WITH_GLFW=ON + -DDVZ_WITH_GLFW=OFF + -DDVZ_USE_MIMALLOC_RELEASE_DEFAULT=OFF ) - _register_git_cmake_library(renderive::datoviz + if (WIN32) + set(DATOVIZ_VULKAN_SDK "$ENV{VULKAN_SDK}") + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(DATOVIZ_SHADERC_RUNTIME "${DATOVIZ_VULKAN_SDK}/Bin/shaderc_sharedd.dll") + set(DATOVIZ_SHADERC_LIBRARY "${DATOVIZ_VULKAN_SDK}/Lib/shaderc_sharedd.lib") + else () + set(DATOVIZ_SHADERC_RUNTIME "${DATOVIZ_VULKAN_SDK}/Bin/shaderc_shared.dll") + set(DATOVIZ_SHADERC_LIBRARY "${DATOVIZ_VULKAN_SDK}/Lib/shaderc_shared.lib") + endif () + if (NOT EXISTS "${DATOVIZ_VULKAN_SDK}/Include/vulkan/vulkan.h" OR + NOT EXISTS "${DATOVIZ_VULKAN_SDK}/Bin/glslc.exe" OR + NOT EXISTS "${DATOVIZ_SHADERC_LIBRARY}" OR + NOT EXISTS "${DATOVIZ_SHADERC_RUNTIME}") + message(FATAL_ERROR "Render_3D requires a Vulkan SDK with glslc and shared shaderc") + endif () + list(APPEND datoviz_opts + "-DGLSLC_PATH=${DATOVIZ_VULKAN_SDK}/Bin" + "-DDVZ_SHADERC_INCLUDE_DIR=${DATOVIZ_VULKAN_SDK}/Include" + "-DDVZ_SHADERC_LIBRARY=${DATOVIZ_SHADERC_LIBRARY}" + "-DDVZ_SHADERC_RUNTIME_LIBRARY=${DATOVIZ_SHADERC_RUNTIME}" + ) + endif () + _register_git_cmake_library(render_3D::datoviz "https://github.com/datoviz/datoviz.git" "v0.4.0rc2" ) - rcl_cmake_library_set_cmake_options(renderive::datoviz ${datoviz_opts}) if (WIN32 AND NOT MINGW) - rcl_cmake_library_set_deps(renderive::datoviz renderive::pthreads4w) + rcl_get_effective_install_dir(render_3D::PThreads4W PTHREADS4W_ROOT) + set(PTHREADS4W_CONFIG_DIR "${CMAKE_CURRENT_LIST_DIR}/../render_3D/cmake/package/PThreads4W") + rcl_cmake_library_set_deps(render_3D::datoviz render_3D::PThreads4W) + list(APPEND datoviz_opts + "-DPTHREADS4W_ROOT=${PTHREADS4W_ROOT}" + "-DPThreads4W_DIR=${PTHREADS4W_CONFIG_DIR}" + ) endif () - rcl_get_effective_install_dir(renderive::datoviz DATOVIZ_ROOT) + rcl_cmake_library_set_cmake_options(render_3D::datoviz ${datoviz_opts}) + rcl_get_effective_install_dir(render_3D::datoviz DATOVIZ_ROOT) set(DATOVIZ_CMAKE_DIR "${DATOVIZ_ROOT}/lib/cmake/datoviz") - rcl_cmake_library_set_other_use_opt(renderive::datoviz + rcl_cmake_library_set_other_use_opt(render_3D::datoviz "-Ddatoviz_DIR=\"${DATOVIZ_CMAKE_DIR}\"" "-DDatoviz_DIR=\"${DATOVIZ_CMAKE_DIR}\"" ) - rcl_cmake_library_set_init_script(renderive::datoviz + rcl_cmake_library_set_init_script(render_3D::datoviz "set(DATOVIZ_ROOT \"${DATOVIZ_ROOT}\")" "set(datoviz_DIR \"${DATOVIZ_CMAKE_DIR}\")" "set(Datoviz_DIR \"${DATOVIZ_CMAKE_DIR}\")" ) - rcl_cmake_library_set_clear_script(renderive::datoviz + rcl_cmake_library_set_clear_script(render_3D::datoviz "unset(DATOVIZ_ROOT)" "unset(datoviz_DIR)" "unset(Datoviz_DIR)" ) -endblock() \ No newline at end of file +endblock() diff --git a/render_3D/CMakeLists.txt b/render_3D/CMakeLists.txt index 9c9b9ac..ff85671 100644 --- a/render_3D/CMakeLists.txt +++ b/render_3D/CMakeLists.txt @@ -1,16 +1,17 @@ -# 加载 datoviz的依赖库 -include(${CMAKE_CURRENT_LIST_DIR}/cmake/export.cmake) -set(Renderive_render_3D_dependencies - render_3D::PThreads4W - render_3D::cglm - render_3D::Vulkan-Hpp - render_3D::VulkanMemoryAllocator - render_3D::msdf-atlas-gen - render_3D::tinyobjloader -) +if (POLICY CMP0144) + cmake_policy(SET CMP0144 NEW) +endif () + +include("${CMAKE_CURRENT_LIST_DIR}/cmake/export.cmake") + +set(Renderive_render_3D_dependencies render_3D::datoviz) +if (WIN32 AND NOT MINGW) + list(APPEND Renderive_render_3D_dependencies render_3D::PThreads4W) +endif () if (RENDERIVE_BUILD_TESTS) list(APPEND Renderive_render_3D_dependencies global::GTest) endif () + rcl_add_dependency_action_targets(Renderive_render_3D_env ${Renderive_render_3D_dependencies}) set_target_properties(Renderive_render_3D_env PROPERTIES FOLDER Renderive_render_3D) library_is_installed_with_rely(Renderive_render_3D_dependencies_installed ${Renderive_render_3D_dependencies}) @@ -18,23 +19,26 @@ if (NOT Renderive_render_3D_dependencies_installed) rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_3D dependencies are not installed. Build Renderive_render_3D_env first") return() endif () + rcl_load_dependency_environment(${Renderive_render_3D_dependencies}) -# 官网下载安装 https://vulkan.lunarg.com/sdk/home -set(ENV{VULKAN_SDK} "C:/VulkanSDK/1.4.357.0") find_package(Vulkan REQUIRED) -message(STATUS "${B}Vulkan_VERSION=${Vulkan_VERSION}${E}") -find_package(PThreads4W REQUIRED) -find_package(cglm CONFIG REQUIRED) -find_package(VulkanMemoryAllocator CONFIG REQUIRED) -find_package(msdf-atlas-gen CONFIG REQUIRED) -find_package(tinyobjloader CONFIG REQUIRED) +if (WIN32 AND NOT MINGW) + rcl_get_effective_install_dir(render_3D::PThreads4W PTHREADS4W_ROOT) + find_package(PThreads4W CONFIG REQUIRED + PATHS "${CMAKE_CURRENT_LIST_DIR}/cmake/package/PThreads4W" + NO_DEFAULT_PATH + ) +endif () +find_package(datoviz CONFIG REQUIRED) if (RENDERIVE_BUILD_TESTS) find_package(GTest CONFIG REQUIRED) endif () + if (NOT TARGET Renderive_Kernel) rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_3D requires Renderive_Kernel") return() endif () + 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/") @@ -43,9 +47,16 @@ 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_Kernel datoviz::datoviz Vulkan::Vulkan +) +if (WIN32 AND NOT MINGW) + target_link_libraries(Renderive_render_3D PRIVATE PThreads4W::PThreads4W) +endif () if (MSVC) target_compile_options(Renderive_render_3D PRIVATE /utf-8) endif () + if (RENDERIVE_BUILD_TESTS) set(Renderive_render_3D_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Renderive_render_3D_test_sources "${Renderive_render_3D_test_dir}") @@ -60,7 +71,16 @@ if (RENDERIVE_BUILD_TESTS) 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) + target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE + Renderive_render_3D Renderive_Kernel datoviz::datoviz GTest::gtest_main + ) add_test(NAME "${Renderive_render_3D_test_target}" COMMAND "${Renderive_render_3D_test_target}") + set_property(TEST "${Renderive_render_3D_test_target}" PROPERTY TIMEOUT 60) + if (WIN32) + set_property(TEST "${Renderive_render_3D_test_target}" PROPERTY ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${DATOVIZ_ROOT}/bin" + "PATH=path_list_prepend:${PThreads4W_RUNTIME_DIR}" + ) + endif () endforeach () endif () diff --git a/render_3D/cmake/computer_graphics.cmake b/render_3D/cmake/computer_graphics.cmake deleted file mode 100644 index d1cecf5..0000000 --- a/render_3D/cmake/computer_graphics.cmake +++ /dev/null @@ -1,238 +0,0 @@ -include_guard(GLOBAL) -block() - set(freetype_opts) - _register_git_cmake_library(render_3D::freetype - https://github.com/freetype/freetype.git - VER-2-13-3 - ) - rcl_get_effective_install_dir(render_3D::freetype FREETYPE_ROOT) - set(FREETYPE_INCLUDE_DIRS "${FREETYPE_ROOT}/include/freetype2") - set(FREETYPE_INCLUDE_DIR_ft2build "${FREETYPE_ROOT}/include/freetype2") - set(FREETYPE_INCLUDE_DIR_freetype2 "${FREETYPE_ROOT}/include/freetype2") - if (CMAKE_BUILD_TYPE STREQUAL "Debug") - set(FREETYPE_LIBRARY "${FREETYPE_ROOT}/lib/freetyped.lib") - else () - set(FREETYPE_LIBRARY "${FREETYPE_ROOT}/lib/freetype.lib") - endif () - rcl_cmake_library_set_cmake_options(render_3D::freetype ${freetype_opts}) - rcl_cmake_library_set_other_use_opt(render_3D::freetype - "-DFREETYPE_INCLUDE_DIRS=\"${FREETYPE_INCLUDE_DIRS}\"" - "-DFREETYPE_INCLUDE_DIR_ft2build=\"${FREETYPE_INCLUDE_DIR_ft2build}\"" - "-DFREETYPE_INCLUDE_DIR_freetype2=\"${FREETYPE_INCLUDE_DIR_freetype2}\"" - "-DFREETYPE_LIBRARY=\"${FREETYPE_LIBRARY}\"" - "-DFREETYPE_LIBRARIES=\"${FREETYPE_LIBRARY}\"" - ) - rcl_cmake_library_set_init_script(render_3D::freetype - "set(FREETYPE_ROOT \"${FREETYPE_ROOT}\")" - "set(FREETYPE_INCLUDE_DIRS \"${FREETYPE_INCLUDE_DIRS}\")" - "set(FREETYPE_INCLUDE_DIR_ft2build \"${FREETYPE_INCLUDE_DIR_ft2build}\")" - "set(FREETYPE_INCLUDE_DIR_freetype2 \"${FREETYPE_INCLUDE_DIR_freetype2}\")" - "set(FREETYPE_LIBRARY \"${FREETYPE_LIBRARY}\")" - "set(FREETYPE_LIBRARIES \"${FREETYPE_LIBRARY}\")" - ) - rcl_cmake_library_set_clear_script(render_3D::freetype - "unset(FREETYPE_ROOT)" - "unset(FREETYPE_INCLUDE_DIRS)" - "unset(FREETYPE_INCLUDE_DIR_ft2build)" - "unset(FREETYPE_INCLUDE_DIR_freetype2)" - "unset(FREETYPE_LIBRARY)" - "unset(FREETYPE_LIBRARIES)" - ) - set(tinyxml2_opts - -Dtinyxml2_BUILD_TESTING=OFF - -Dtinyxml2_INSTALL_PKGCONFIG=OFF - -Dtinyxml2_SHARED_LIBS=OFF - -Dtinyxml2_INSTALL_CMAKEDIR=lib/cmake/tinyxml2 - ) - _register_git_cmake_library(render_3D::tinyxml2 - "https://github.com/leethomason/tinyxml2.git" - "11.0.0" - ) - rcl_get_effective_install_dir(render_3D::tinyxml2 TINYXML2_ROOT) - set(TINYXML2_CONFIG_DIR "${TINYXML2_ROOT}/lib/cmake/tinyxml2") - rcl_cmake_library_set_cmake_options(render_3D::tinyxml2 ${tinyxml2_opts}) - rcl_cmake_library_set_other_use_opt(render_3D::tinyxml2 - "-Dtinyxml2_DIR=\"${TINYXML2_CONFIG_DIR}\"" - ) - rcl_cmake_library_set_init_script(render_3D::tinyxml2 - "set(TINYXML2_ROOT \"${TINYXML2_ROOT}\")" - "set(tinyxml2_DIR \"${TINYXML2_CONFIG_DIR}\")" - ) - rcl_cmake_library_set_clear_script(render_3D::tinyxml2 - "unset(TINYXML2_ROOT)" - "unset(tinyxml2_DIR)" - ) - set(msdfgen_cmake_option ${base_options}) - list(APPEND msdfgen_cmake_option - -DMSDFGEN_USE_SKIA=OFF - -DMSDFGEN_USE_VCPKG=OFF - -DBUILD_SHARED_LIBS=ON - -DMSDFGEN_DYNAMIC_RUNTIME=ON - -DMSDFGEN_INSTALL=ON - ) - _register_git_cmake_library(render_3D::msdfgen - https://github.com/Chlumsky/msdfgen.git - v1.12 - ) - rcl_get_effective_install_dir(render_3D::msdfgen MSDFGEN_ROOT) - set(MSDFGEN_CONFIG_DIR "${MSDFGEN_ROOT}/lib/cmake/msdfgen") - rcl_cmake_library_set_cmake_options(render_3D::msdfgen ${msdfgen_cmake_option}) - rcl_cmake_library_set_deps(render_3D::msdfgen - global::zlib - global::libpng - render_3D::freetype - render_3D::tinyxml2 - ) - rcl_cmake_library_set_other_use_opt(render_3D::msdfgen - "-Dmsdfgen_DIR=\"${MSDFGEN_CONFIG_DIR}\"" - ) - rcl_cmake_library_set_init_script(render_3D::msdfgen - "set(MSDFGEN_ROOT \"${MSDFGEN_ROOT}\")" - "set(msdfgen_DIR \"${MSDFGEN_CONFIG_DIR}\")" - ) - rcl_cmake_library_set_clear_script(render_3D::msdfgen - "unset(MSDFGEN_ROOT)" - "unset(msdfgen_DIR)" - ) - set(msdfgl_cmake_option "") - if (WIN32) - set(PThreads4W_option ${base_options}) - list(APPEND PThreads4W_option - "-DCMAKE_C_FLAGS=-D_WINDLL" - "-DCMAKE_CXX_FLAGS=-D_WINDLL" - ) - _register_git_cmake_library(render_3D::PThreads4W - https://github.com/jwinarske/pthreads4w.git - "cmake" - ) - rcl_get_effective_install_dir(render_3D::PThreads4W PTHREADS4W_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::PThreads4W ${PThreads4W_option}) - endif () - set(glfw3_option ${base_options}) - _register_git_cmake_library(render_3D::glfw3 - https://github.com/glfw/glfw.git - "3.4" - ) - rcl_get_effective_install_dir(render_3D::glfw3 GLFW3_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::glfw3 ${glfw3_option}) - set(msdf-atlas-gen_option ${base_options}) - list(APPEND msdf-atlas-gen_option - -DMSDFGEN_DISABLE_SVG=OFF - -DMSDFGEN_USE_SKIA=OFF - -DMSDF_ATLAS_BUILD_STANDALONE=OFF - -DMSDF_ATLAS_USE_VCPKG=OFF - -DMSDF_ATLAS_USE_SKIA=OFF - -DMSDF_ATLAS_DYNAMIC_RUNTIME=ON - -DMSDF_ATLAS_MSDFGEN_EXTERNAL=OFF - -DMSDF_ATLAS_INSTALL=ON - -DMSDFGEN_INSTALL=ON - -DBUILD_SHARED_LIBS=OFF - ) - _register_git_cmake_library(render_3D::msdf-atlas-gen - https://github.com/Chlumsky/msdf-atlas-gen.git - v1.4 - ) - rcl_get_effective_install_dir(render_3D::msdf-atlas-gen MSDF_ATLAS_GEN_ROOT) - set(MSDF_ATLAS_GEN_CONFIG_DIR "${MSDF_ATLAS_GEN_ROOT}/lib/cmake/msdf-atlas-gen") - set(MSDF_ATLAS_GEN_MSDFGEN_CONFIG_DIR "${MSDF_ATLAS_GEN_ROOT}/lib/cmake/msdfgen") - rcl_cmake_library_set_cmake_options(render_3D::msdf-atlas-gen ${msdf-atlas-gen_option}) - rcl_cmake_library_set_deps(render_3D::msdf-atlas-gen - global::zlib - global::libpng - render_3D::freetype - render_3D::tinyxml2 - ) - rcl_cmake_library_set_other_use_opt(render_3D::msdf-atlas-gen - "-Dmsdf-atlas-gen_DIR=\"${MSDF_ATLAS_GEN_CONFIG_DIR}\"" - "-Dmsdfgen_DIR=\"${MSDF_ATLAS_GEN_MSDFGEN_CONFIG_DIR}\"" - ) - rcl_cmake_library_set_init_script(render_3D::msdf-atlas-gen - "set(MSDF_ATLAS_GEN_ROOT \"${MSDF_ATLAS_GEN_ROOT}\")" - "set(msdf-atlas-gen_DIR \"${MSDF_ATLAS_GEN_CONFIG_DIR}\")" - "set(msdfgen_DIR \"${MSDF_ATLAS_GEN_MSDFGEN_CONFIG_DIR}\")" - ) - rcl_cmake_library_set_clear_script(render_3D::msdf-atlas-gen - "unset(MSDF_ATLAS_GEN_ROOT)" - "unset(msdf-atlas-gen_DIR)" - "unset(msdfgen_DIR)" - ) - set(cglm_option ${base_options}) - _register_git_cmake_library(render_3D::cglm - "https://github.com/recp/cglm.git" - v0.9.6 - ) - rcl_get_effective_install_dir(render_3D::cglm CGLM_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::cglm ${cglm_option}) - set(glm_option ${base_options}) - _register_git_cmake_library(render_3D::glm - "https://github.com/g-truc/glm.git" - "1.0.3" - ) - rcl_get_effective_install_dir(render_3D::glm GLM_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::glm ${glm_option}) - set(tinyobjloader_option ${base_options}) - list(APPEND tinyobjloader_option - -DCMAKE_POLICY_VERSION_MINIMUM=3.20 - ) - _register_git_cmake_library(render_3D::tinyobjloader - "https://github.com/tinyobjloader/tinyobjloader.git" - "v1.0.7" - ) - rcl_get_effective_install_dir(render_3D::tinyobjloader TINYOBJLOADER_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::tinyobjloader ${tinyobjloader_option}) - set(vk-bootstrap_option ${base_options}) - _register_git_cmake_library(render_3D::vk-bootstrap - "https://github.com/charles-lunarg/vk-bootstrap.git" - "v1.4.357" - ) - rcl_get_effective_install_dir(render_3D::vk-bootstrap VK_BOOTSTRAP_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::vk-bootstrap ${vk-bootstrap_option}) - set(Vulkan-Hpp_option ${base_options}) - _register_git_cmake_library(render_3D::Vulkan-Hpp - "https://github.com/KhronosGroup/Vulkan-Hpp.git" - "v1.4.357" - ) - rcl_get_effective_install_dir(render_3D::Vulkan-Hpp VULKAN_HPP_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::Vulkan-Hpp ${Vulkan-Hpp_option}) - set(volk_option ${base_options}) - list(APPEND volk_option - -DVOLK_INSTALL=ON - -DVOLK_PULL_IN_VULKAN=ON - -DVOLK_HEADERS_ONLY=OFF - -DVOLK_NAMESPACE=ON - ) - _register_git_cmake_library(render_3D::volk - "https://github.com/zeux/volk.git" - "vulkan-sdk-1.4.357.0" - ) - rcl_get_effective_install_dir(render_3D::volk VOLK_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::volk ${volk_option}) - set(VulkanMemoryAllocator_option ${base_options}) - _register_git_cmake_library(render_3D::VulkanMemoryAllocator - "https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git" - "v3.3.0" - ) - rcl_get_effective_install_dir(render_3D::VulkanMemoryAllocator VULKAN_MEMORY_ALLOCATOR_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::VulkanMemoryAllocator ${VulkanMemoryAllocator_option}) - set(tinygltf_option ${base_options}) - _register_git_cmake_library(render_3D::tinygltf - "https://github.com/syoyo/tinygltf.git" - "v3.0.0" - ) - rcl_get_effective_install_dir(render_3D::tinygltf TINYGLTF_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::tinygltf ${tinygltf_option}) - set(fastgltf_option ${base_options}) - _register_git_cmake_library(render_3D::fastgltf - "https://github.com/spnda/fastgltf.git" - "v0.9.0" - ) - rcl_get_effective_install_dir(render_3D::fastgltf FASTGLTF_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::fastgltf ${fastgltf_option}) - set(VulkanSceneGraph_option ${base_options}) - _register_git_cmake_library(render_3D::VulkanSceneGraph - "https://github.com/vsg-dev/VulkanSceneGraph.git" - "v1.1.15" - ) - rcl_get_effective_install_dir(render_3D::VulkanSceneGraph VULKAN_SCENE_GRAPH_ROOT) - rcl_cmake_library_set_cmake_options(render_3D::VulkanSceneGraph ${VulkanSceneGraph_option}) -endblock() \ No newline at end of file diff --git a/render_3D/cmake/dependencies.cmake b/render_3D/cmake/dependencies.cmake new file mode 100644 index 0000000..61b914e --- /dev/null +++ b/render_3D/cmake/dependencies.cmake @@ -0,0 +1,26 @@ +include_guard(GLOBAL) + +block() + set(render_3D_dependency_options + -DBUILD_TESTING=OFF + -DBUILD_SHARED_LIBS=OFF + "-DCMAKE_DEBUG_POSTFIX=d" + "-DCMAKE_RELWITHDEBINFO_POSTFIX=" + "-DCMAKE_MINSIZEREL_POSTFIX=" + ) + + if (WIN32 AND NOT MINGW) + set(render_3D_pthreads_options ${render_3D_dependency_options}) + list(APPEND render_3D_pthreads_options + "-DCMAKE_C_FLAGS=-D_WINDLL" + "-DCMAKE_CXX_FLAGS=-D_WINDLL" + ) + _register_git_cmake_library(render_3D::PThreads4W + "https://github.com/jwinarske/pthreads4w.git" + "cmake" + ) + rcl_cmake_library_set_cmake_options(render_3D::PThreads4W ${render_3D_pthreads_options}) + endif () +endblock() + +include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/datoviz.cmake") diff --git a/render_3D/cmake/export.cmake b/render_3D/cmake/export.cmake index 7827f70..36437a9 100644 --- a/render_3D/cmake/export.cmake +++ b/render_3D/cmake/export.cmake @@ -1,3 +1,2 @@ rcl_init(render_3D) -include(${CMAKE_CURRENT_LIST_DIR}/computer_graphics.cmake) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/module") \ No newline at end of file +include("${CMAKE_CURRENT_LIST_DIR}/dependencies.cmake") diff --git a/render_3D/cmake/module/FindPThreads4W.cmake b/render_3D/cmake/module/FindPThreads4W.cmake deleted file mode 100644 index 29e398c..0000000 --- a/render_3D/cmake/module/FindPThreads4W.cmake +++ /dev/null @@ -1,26 +0,0 @@ -include_guard(GLOBAL) -rcl_get_effective_install_dir(render_3D::PThreads4W PTHREADS4W_ROOT) -message("render_3D::PThreads4W PTHREADS4W_ROOT=${PTHREADS4W_ROOT}") -set(PThreads4W_INCLUDE_DIR "${PTHREADS4W_ROOT}/include") -if (CMAKE_BUILD_TYPE STREQUAL "Debug") - set(PThreads4W_LIBRARY "${PTHREADS4W_ROOT}/lib/pthreadVC3d.lib") -else () - set(PThreads4W_LIBRARY "${PTHREADS4W_ROOT}/lib/pthreadVC3.lib") -endif () -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(PThreads4W - REQUIRED_VARS - PThreads4W_INCLUDE_DIR - PThreads4W_LIBRARY -) -if (PThreads4W_FOUND AND NOT TARGET PThreads4W::PThreads4W) - add_library(PThreads4W::PThreads4W UNKNOWN IMPORTED) - set_target_properties(PThreads4W::PThreads4W PROPERTIES - IMPORTED_LOCATION "${PThreads4W_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${PThreads4W_INCLUDE_DIR}" - ) -endif () -mark_as_advanced( - PThreads4W_INCLUDE_DIR - PThreads4W_LIBRARY -) \ No newline at end of file diff --git a/render_3D/cmake/package/PThreads4W/PThreads4WConfig.cmake b/render_3D/cmake/package/PThreads4W/PThreads4WConfig.cmake new file mode 100644 index 0000000..aa9b5c5 --- /dev/null +++ b/render_3D/cmake/package/PThreads4W/PThreads4WConfig.cmake @@ -0,0 +1,31 @@ +if (NOT PTHREADS4W_ROOT) + message(FATAL_ERROR "PTHREADS4W_ROOT is required by PThreads4WConfig.cmake") +endif () + +set(_pthreads4w_prefix "${PTHREADS4W_ROOT}") +if (EXISTS "${PTHREADS4W_ROOT}/x86_64/${CMAKE_BUILD_TYPE}/include/pthread.h") + set(_pthreads4w_prefix "${PTHREADS4W_ROOT}/x86_64/${CMAKE_BUILD_TYPE}") +endif () + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(_pthreads4w_library "${_pthreads4w_prefix}/lib/pthreadVC3d.lib") +else () + set(_pthreads4w_library "${_pthreads4w_prefix}/lib/pthreadVC3.lib") +endif () + +if (NOT EXISTS "${_pthreads4w_prefix}/include/pthread.h" OR NOT EXISTS "${_pthreads4w_library}") + message(FATAL_ERROR "PThreads4W installation is incomplete at ${PTHREADS4W_ROOT}") +endif () + +if (NOT TARGET PThreads4W::PThreads4W) + add_library(PThreads4W::PThreads4W UNKNOWN IMPORTED) + set_target_properties(PThreads4W::PThreads4W PROPERTIES + IMPORTED_LOCATION "${_pthreads4w_library}" + INTERFACE_INCLUDE_DIRECTORIES "${_pthreads4w_prefix}/include" + ) +endif () + +set(PThreads4W_FOUND TRUE) +set(PThreads4W_RUNTIME_DIR "${_pthreads4w_prefix}/bin") +unset(_pthreads4w_library) +unset(_pthreads4w_prefix) diff --git a/render_3D/render_3D/Render_3D.cpp b/render_3D/render_3D/Render_3D.cpp new file mode 100644 index 0000000..3d48dc1 --- /dev/null +++ b/render_3D/render_3D/Render_3D.cpp @@ -0,0 +1,300 @@ +#include "Render_3D.h" + +#include "detail/Datoviz_Backend.h" +#include "detail/Ordered_Backend_Shutdown.h" +#include "detail/Render_3D_Core.h" +#include "detail/Render_Domain.h" + +#include +#include +#include +#include +#include + +namespace renderive { +namespace { + +bool finite(Vec_3 value) { + return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); +} + +void validate_camera(const Camera_3D& camera) { + if (!finite(camera.eye) || !finite(camera.target) || !finite(camera.up) || + !std::isfinite(camera.vertical_field_of_view_radians) || + !std::isfinite(camera.near_clip) || !std::isfinite(camera.far_clip) || + !std::isfinite(camera.orthographic_height) || camera.near_clip <= 0.0F || + camera.far_clip <= camera.near_clip || camera.vertical_field_of_view_radians <= 0.0F || + camera.vertical_field_of_view_radians >= 3.141592654F || + camera.orthographic_height <= 0.0F) + throw std::invalid_argument("invalid Render_3D camera"); +} + +void validate_clear_color(const Clear_Color& color) { + const auto valid = [](float value) { return std::isfinite(value) && value >= 0.0F && value <= 1.0F; }; + if (!valid(color.red) || !valid(color.green) || !valid(color.blue) || !valid(color.alpha)) + throw std::invalid_argument("Render_3D clear color components must be in [0, 1]"); +} + +void validate_scale(float device_x, float device_y, float render, float user) { + const auto valid = [](float value) { return std::isfinite(value) && value > 0.0F; }; + if (!valid(device_x) || !valid(device_y) || !valid(render) || !valid(user)) + throw std::invalid_argument("Render_3D scale values must be finite and positive"); +} + +void validate_sphere(const Sphere_3D& sphere) { + if (!finite(sphere.position) || !std::isfinite(sphere.radius) || sphere.radius <= 0.0F) + throw std::invalid_argument("invalid Render_3D sphere"); + for (const float value : sphere.transform.values) { + if (!std::isfinite(value)) + throw std::invalid_argument("Render_3D transform contains a non-finite value"); + } +} + +} // namespace + +struct Render_3D::Configuration { + Frame_3D_Control_Mode frame_control_mode{Frame_3D_Control_Mode::Low_Latency}; + double maximum_frames_per_second{60.0}; + std::size_t frame_slot_count{2}; + std::uint32_t gpu_index{}; + bool validation_enabled{}; + detail::Render_3D_State initial_state; +}; + +struct Render_3D::Impl { + explicit Impl(const Configuration& configuration) + : states(configuration.initial_state), + scheduler(configuration.frame_control_mode, configuration.maximum_frames_per_second) { + try { + render_domain.invoke([&] { + backend = std::make_unique( + configuration.gpu_index, configuration.validation_enabled, + configuration.initial_state); + targets = backend->create_target_pool(configuration.frame_slot_count); + }); + } catch (...) { + render_domain.invoke([&] { + targets.reset(); + backend.reset(); + }); + throw; + } + } + + ~Impl() { + accepting_frames.store(false, std::memory_order_release); + render_domain.invoke([&] { + if (backend == nullptr) + return; + if (targets != nullptr) + targets->wait_all(); + detail::ordered_backend_shutdown( + [&] { backend->destroy_runtime(); }, + [&] { targets.reset(); }, + [&] { backend->destroy_scene(); }, + [&] { backend->destroy_gpu_context(); }); + backend.reset(); + }); + } + + [[nodiscard]] bool prepare_frame() { + return accepting_frames.load(std::memory_order_acquire) && scheduler.prepare(states); + } + + [[nodiscard]] bool render_prepared_frame() { + if (!accepting_frames.load(std::memory_order_acquire)) + return false; + return render_domain.invoke([&] { + return scheduler.render([&](const detail::Render_3D_Frame_Data& frame) { + return backend->render(frame, *targets); + }); + }); + } + + void wait_for_render() { + if (!accepting_frames.load(std::memory_order_acquire)) + return; + render_domain.invoke([&] { targets->wait_all(); }); + } + + [[nodiscard]] Object_3D_Id allocate_object_id() { + auto value = next_object_id.load(std::memory_order_relaxed); + for (;;) { + if (value == 0 || value == std::numeric_limits::max()) + throw std::overflow_error("Render_3D object identifiers are exhausted"); + if (next_object_id.compare_exchange_weak( + value, value + 1, std::memory_order_relaxed)) + return {value}; + } + } + + detail::Render_Domain render_domain; + std::unique_ptr backend; + std::unique_ptr targets; + detail::Logical_State_Buffer states; + detail::Frame_Scheduler scheduler; + std::atomic next_object_id{1}; + std::atomic accepting_frames{true}; +}; + +Render_3D::Builder& Render_3D::Builder::frame_control( + Frame_3D_Control_Mode mode, double maximum_frames_per_second) { + frame_control_mode_ = mode; + maximum_frames_per_second_ = maximum_frames_per_second; + return *this; +} + +Render_3D::Builder& Render_3D::Builder::viewport(Extent_2D extent) { + viewport_ = extent; + return *this; +} + +Render_3D::Builder& Render_3D::Builder::frame_slots(std::size_t count) { + frame_slot_count_ = count; + return *this; +} + +Render_3D::Builder& Render_3D::Builder::gpu( + std::uint32_t index, bool validation_enabled) { + gpu_index_ = index; + validation_enabled_ = validation_enabled; + return *this; +} + +Render_3D::Builder& Render_3D::Builder::camera(Camera_3D value) { + camera_ = value; + return *this; +} + +Render_3D::Builder& Render_3D::Builder::clear_color(Clear_Color value) { + clear_color_ = value; + return *this; +} + +Render_3D::Builder& Render_3D::Builder::scale( + float device_x, float device_y, float render, float user) { + device_scale_x_ = device_x; + device_scale_y_ = device_y; + render_scale_ = render; + user_scale_ = user; + return *this; +} + +std::unique_ptr Render_3D::Builder::build() const { + if (!std::isfinite(maximum_frames_per_second_) || maximum_frames_per_second_ <= 0.0) + throw std::invalid_argument("Render_3D frame frequency must be finite and positive"); + if (frame_slot_count_ < 2) + throw std::invalid_argument("Render_3D requires at least two frame slots"); + validate_camera(camera_); + validate_clear_color(clear_color_); + validate_scale(device_scale_x_, device_scale_y_, render_scale_, user_scale_); + + Configuration configuration; + configuration.frame_control_mode = frame_control_mode_; + configuration.maximum_frames_per_second = maximum_frames_per_second_; + configuration.frame_slot_count = frame_slot_count_; + configuration.gpu_index = gpu_index_; + configuration.validation_enabled = validation_enabled_; + configuration.initial_state.viewport = viewport_; + configuration.initial_state.camera = camera_; + configuration.initial_state.clear_color = clear_color_; + configuration.initial_state.device_scale_x = device_scale_x_; + configuration.initial_state.device_scale_y = device_scale_y_; + configuration.initial_state.render_scale = render_scale_; + configuration.initial_state.user_scale = user_scale_; + return std::unique_ptr(new Render_3D(configuration)); +} + +Render_3D::Render_3D(const Configuration& configuration) + : impl_(std::make_unique(configuration)) {} + +Render_3D::~Render_3D() = default; + +void Render_3D::resize(Extent_2D extent) { + impl_->states.update([extent](detail::Render_3D_State& state) { state.viewport = extent; }); +} + +void Render_3D::set_camera(Camera_3D camera) { + validate_camera(camera); + impl_->states.update([camera](detail::Render_3D_State& state) { state.camera = camera; }); +} + +void Render_3D::set_clear_color(Clear_Color color) { + validate_clear_color(color); + impl_->states.update([color](detail::Render_3D_State& state) { state.clear_color = color; }); +} + +void Render_3D::set_scale(float device_x, float device_y, float render, float user) { + validate_scale(device_x, device_y, render, user); + impl_->states.update([=](detail::Render_3D_State& state) { + state.device_scale_x = device_x; + state.device_scale_y = device_y; + state.render_scale = render; + state.user_scale = user; + }); +} + +Object_3D_Id Render_3D::add_sphere(Sphere_3D sphere) { + validate_sphere(sphere); + const Object_3D_Id id = impl_->allocate_object_id(); + impl_->states.update([id, sphere](detail::Render_3D_State& state) { + state.objects.push_back({id, sphere, 1}); + }); + return id; +} + +bool Render_3D::replace_sphere(Object_3D_Id id, Sphere_3D sphere) { + if (!id) + return false; + validate_sphere(sphere); + bool replaced{}; + impl_->states.update([&](detail::Render_3D_State& state) { + const auto iterator = std::find_if( + state.objects.begin(), state.objects.end(), + [id](const detail::Object_3D_State& object) { return object.id == id; }); + if (iterator == state.objects.end()) + return; + iterator->sphere = sphere; + ++iterator->revision; + replaced = true; + }); + return replaced; +} + +bool Render_3D::erase_object(Object_3D_Id id) { + if (!id) + return false; + bool erased{}; + impl_->states.update([&](detail::Render_3D_State& state) { + const auto iterator = std::find_if( + state.objects.begin(), state.objects.end(), + [id](const detail::Object_3D_State& object) { return object.id == id; }); + if (iterator == state.objects.end()) + return; + state.objects.erase(iterator); + erased = true; + }); + return erased; +} + +bool Render_3D::prepare_frame() { + return impl_->prepare_frame(); +} + +bool Render_3D::refresh_manual_frame() { + return impl_->accepting_frames.load(std::memory_order_acquire) && impl_->scheduler.refresh(); +} + +bool Render_3D::discard_pending_frame() { + return impl_->accepting_frames.load(std::memory_order_acquire) && impl_->scheduler.discard_pending(); +} + +bool Render_3D::render_prepared_frame() { + return impl_->render_prepared_frame(); +} + +void Render_3D::wait_for_render() { + impl_->wait_for_render(); +} + +} // namespace renderive diff --git a/render_3D/render_3D/Render_3D.h b/render_3D/render_3D/Render_3D.h new file mode 100644 index 0000000..eafe99e --- /dev/null +++ b/render_3D/render_3D/Render_3D.h @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace renderive { + +enum class Frame_3D_Control_Mode : std::uint8_t { + Manual, + Low_Latency, + Playback +}; + +struct Extent_2D { + std::uint32_t width{}; + std::uint32_t height{}; + + [[nodiscard]] constexpr bool empty() const noexcept { return width == 0 || height == 0; } + bool operator==(const Extent_2D&) const = default; +}; + +struct Vec_3 { + float x{}; + float y{}; + float z{}; + bool operator==(const Vec_3&) const = default; +}; + +struct Rgba_8 { + std::uint8_t red{255}; + std::uint8_t green{255}; + std::uint8_t blue{255}; + std::uint8_t alpha{255}; + bool operator==(const Rgba_8&) const = default; +}; + +struct Clear_Color { + float red{}; + float green{}; + float blue{}; + float alpha{1.0F}; + bool operator==(const Clear_Color&) const = default; +}; + +struct Matrix_4x4 { + std::array values{ + 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}; + bool operator==(const Matrix_4x4&) const = default; +}; + +enum class Camera_Projection : std::uint8_t { + Perspective, + Orthographic +}; + +struct Camera_3D { + Vec_3 eye{0.0F, 0.0F, 4.0F}; + Vec_3 target{}; + Vec_3 up{0.0F, 1.0F, 0.0F}; + Camera_Projection projection{Camera_Projection::Perspective}; + float vertical_field_of_view_radians{0.785398163F}; + float near_clip{0.01F}; + float far_clip{1000.0F}; + float orthographic_height{2.0F}; + bool operator==(const Camera_3D&) const = default; +}; + +struct Sphere_3D { + Vec_3 position{}; + float radius{1.0F}; + Rgba_8 color{}; + Matrix_4x4 transform{}; + bool visible{true}; + bool operator==(const Sphere_3D&) const = default; +}; + +struct Object_3D_Id { + std::uint64_t value{}; + + [[nodiscard]] explicit constexpr operator bool() const noexcept { return value != 0; } + auto operator<=>(const Object_3D_Id&) const = default; +}; + +class Render_3D final { +public: + class Builder final { + public: + Builder& frame_control(Frame_3D_Control_Mode mode, double maximum_frames_per_second = 60.0); + Builder& viewport(Extent_2D extent); + Builder& frame_slots(std::size_t count); + Builder& gpu(std::uint32_t index, bool validation_enabled = false); + Builder& camera(Camera_3D value); + Builder& clear_color(Clear_Color value); + Builder& scale(float device_x, float device_y, float render, float user); + [[nodiscard]] std::unique_ptr build() const; + + private: + Frame_3D_Control_Mode frame_control_mode_{Frame_3D_Control_Mode::Low_Latency}; + double maximum_frames_per_second_{60.0}; + Extent_2D viewport_{800, 600}; + std::size_t frame_slot_count_{2}; + std::uint32_t gpu_index_{}; + bool validation_enabled_{}; + Camera_3D camera_; + Clear_Color clear_color_; + float device_scale_x_{1.0F}; + float device_scale_y_{1.0F}; + float render_scale_{1.0F}; + float user_scale_{1.0F}; + }; + + ~Render_3D(); + Render_3D(const Render_3D&) = delete; + Render_3D& operator=(const Render_3D&) = delete; + Render_3D(Render_3D&&) = delete; + Render_3D& operator=(Render_3D&&) = delete; + + void resize(Extent_2D extent); + void set_camera(Camera_3D camera); + void set_clear_color(Clear_Color color); + void set_scale(float device_x, float device_y, float render, float user); + + [[nodiscard]] Object_3D_Id add_sphere(Sphere_3D sphere); + [[nodiscard]] bool replace_sphere(Object_3D_Id id, Sphere_3D sphere); + [[nodiscard]] bool erase_object(Object_3D_Id id); + + [[nodiscard]] bool prepare_frame(); + [[nodiscard]] bool refresh_manual_frame(); + [[nodiscard]] bool discard_pending_frame(); + [[nodiscard]] bool render_prepared_frame(); + void wait_for_render(); + +private: + struct Configuration; + struct Impl; + + explicit Render_3D(const Configuration& configuration); + std::unique_ptr impl_; +}; + +} // namespace renderive diff --git a/render_3D/render_3D/detail/Datoviz_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Backend.cpp new file mode 100644 index 0000000..93a7db4 --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Backend.cpp @@ -0,0 +1,155 @@ +#include "Datoviz_Backend.h" + +#include "Datoviz_Frame_Executor.h" + +#include + +#include +#include + +namespace renderive::detail { + +Datoviz_Backend::Datoviz_Backend( + std::uint32_t gpu_index, bool validation_enabled, const Render_3D_State& initial_state) { + try { + DvzGpuCtxConfig configuration = dvz_gpu_ctx_config(); + dvz_gpu_ctx_config_validation(&configuration, validation_enabled); + dvz_gpu_ctx_config_gpu(&configuration, gpu_index); + dvz_gpu_ctx_config_enable_canvas_extensions(&configuration, false); + gpu_context_ = dvz_gpu_ctx(&configuration); + if (gpu_context_ == nullptr) + throw std::runtime_error("failed to create Datoviz GPU context"); + if (dvz_gpu_ctx_device(gpu_context_) == nullptr || + dvz_gpu_ctx_alloc(gpu_context_) == nullptr || + dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN) == nullptr) + throw std::runtime_error("Datoviz GPU context does not expose required resources"); + + create_runtime(); + create_scene(initial_state.viewport); + } catch (...) { + destroy_runtime(); + destroy_scene(); + destroy_gpu_context(); + throw; + } +} + +Datoviz_Backend::~Datoviz_Backend() { + destroy_runtime(); + destroy_scene(); + destroy_gpu_context(); +} + +std::unique_ptr Datoviz_Backend::create_target_pool(std::size_t slot_count) { + return std::make_unique(gpu_context_, slot_count); +} + +bool Datoviz_Backend::render( + const Render_3D_Frame_Data& frame, Frame_Target_Pool& targets) { + if (frame.state.viewport.empty()) + return false; + if (runtime_ == nullptr || scene_binding_ == nullptr) { + if (!rebuild(frame, targets)) + return false; + } + if (!scene_binding_->apply(frame.state, frame.revision)) { + std::fputs("Render_3D: failed to apply logical state to the Datoviz scene\n", stderr); + (void)rebuild(frame, targets); + return false; + } + + try { + Frame_Target& target = targets.acquire(frame.frame_sequence, frame.state.viewport); + if (Datoviz_Frame_Executor::execute(*this, targets, target, frame)) + return true; + target.abort(); + } catch (const std::exception& error) { + std::fprintf(stderr, "Render_3D: backend transaction threw: %s\n", error.what()); + } catch (...) { + std::fputs("Render_3D: backend transaction failed with an unknown error\n", stderr); + } + (void)rebuild(frame, targets); + return false; +} + +void Datoviz_Backend::destroy_runtime() noexcept { + if (runtime_ != nullptr) { + dvz_drp2_runtime_destroy(runtime_); + runtime_ = nullptr; + } +} + +void Datoviz_Backend::destroy_scene() noexcept { + scene_binding_.reset(); + if (scene_ != nullptr) { + dvz_scene_destroy(scene_); + scene_ = nullptr; + } +} + +void Datoviz_Backend::destroy_gpu_context() noexcept { + if (gpu_context_ != nullptr) { + dvz_gpu_ctx_destroy(gpu_context_); + gpu_context_ = nullptr; + } +} + +void Datoviz_Backend::create_runtime() { + DvzDrp2RuntimeConfig configuration = dvz_drp2_runtime_vklite_config( + dvz_gpu_ctx_device(gpu_context_), dvz_gpu_ctx_alloc(gpu_context_)); + runtime_ = dvz_drp2_runtime_vklite(&configuration); + if (runtime_ == nullptr) + throw std::runtime_error("failed to create Datoviz DRP2 runtime"); +} + +void Datoviz_Backend::create_scene(Extent_2D extent) { + scene_ = dvz_scene(); + if (scene_ == nullptr) + throw std::runtime_error("failed to create Datoviz scene"); + const DvzCapabilitySnapshot capabilities = dvz_capability_snapshot(); + if (dvz_scene_set_capabilities(scene_, &capabilities) != DVZ_OK) + throw std::runtime_error("failed to configure Datoviz scene capabilities"); + scene_binding_ = std::make_unique(scene_, extent); +} + +bool Datoviz_Backend::rebuild( + const Render_3D_Frame_Data& frame, Frame_Target_Pool& targets) noexcept { + targets.wait_all(); + destroy_runtime(); + targets.runtime_released_targets(); + destroy_scene(); + try { + create_runtime(); + create_scene(frame.state.viewport); + return scene_binding_->apply(frame.state, frame.revision); + } catch (...) { + destroy_runtime(); + destroy_scene(); + return false; + } +} + +DvzSceneFrameArtifact* Datoviz_Backend::emit( + const DvzFramePlanEmitConfig& configuration) { + const DvzCapabilitySnapshot capabilities = dvz_capability_snapshot(); + return scene_binding_->emit(capabilities, configuration); +} + +bool Datoviz_Backend::attach(const DvzStreamFrame& frame) { + return dvz_drp2_runtime_attach_frame_target(runtime_, Datoviz_Color_Target_Id, &frame); +} + +bool Datoviz_Backend::execute(const DvzDrp2CommandStream& stream) { + const DvzDrp2ValidationResult result = dvz_drp2_runtime_execute(runtime_, &stream); + if (!result.ok) { + const DvzDrp2Command* command = dvz_drp2_stream_get(&stream, result.command_index); + std::fprintf( + stderr, "Render_3D: DRP2 execution failed (code=%u command=%u type=%u count=%u)\n", + static_cast(result.code), result.command_index, + command == nullptr ? 0U : static_cast(dvz_drp2_command_type(command)), + dvz_drp2_stream_count(&stream)); + } + return result.ok; +} + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Datoviz_Backend.h b/render_3D/render_3D/detail/Datoviz_Backend.h new file mode 100644 index 0000000..85c6463 --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Backend.h @@ -0,0 +1,53 @@ +#pragma once + +#include "Datoviz_Scene_Binding.h" +#include "Frame_Target_Pool.h" +#include "Render_3D_Core.h" + +#include +#include + +#include +#include +#include + +namespace renderive::detail { + +class Datoviz_Frame_Executor; +struct Datoviz_Transaction_Api; +inline constexpr std::uint64_t Datoviz_Color_Target_Id = 4242; + +class Datoviz_Backend final { +public: + Datoviz_Backend( + std::uint32_t gpu_index, bool validation_enabled, const Render_3D_State& initial_state); + ~Datoviz_Backend(); + + Datoviz_Backend(const Datoviz_Backend&) = delete; + Datoviz_Backend& operator=(const Datoviz_Backend&) = delete; + + [[nodiscard]] std::unique_ptr create_target_pool(std::size_t slot_count); + [[nodiscard]] bool render(const Render_3D_Frame_Data& frame, Frame_Target_Pool& targets); + + void destroy_runtime() noexcept; + void destroy_scene() noexcept; + void destroy_gpu_context() noexcept; + +private: + friend class Datoviz_Frame_Executor; + friend struct Datoviz_Transaction_Api; + + void create_runtime(); + void create_scene(Extent_2D extent); + [[nodiscard]] bool rebuild(const Render_3D_Frame_Data& frame, Frame_Target_Pool& targets) noexcept; + [[nodiscard]] DvzSceneFrameArtifact* emit(const DvzFramePlanEmitConfig& configuration); + [[nodiscard]] bool attach(const DvzStreamFrame& frame); + [[nodiscard]] bool execute(const DvzDrp2CommandStream& stream); + + DvzGpuCtx* gpu_context_{}; + DvzDrp2Runtime* runtime_{}; + DvzScene* scene_{}; + std::unique_ptr scene_binding_; +}; + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Datoviz_Frame_Executor.cpp b/render_3D/render_3D/detail/Datoviz_Frame_Executor.cpp new file mode 100644 index 0000000..2ecb667 --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Frame_Executor.cpp @@ -0,0 +1,92 @@ +#include "Datoviz_Frame_Executor.h" + +#include "Datoviz_Backend.h" +#include "Frame_Target_Pool.h" +#include "Ordered_Artifact_Transaction.h" + +#include + +#include + +namespace renderive::detail { +namespace { + +DvzFramePlanEmitConfig emit_configuration(const Render_3D_State& state) { + DvzFramePlanEmitConfig configuration = dvz_frame_plan_emit_config(); + configuration.shader_format = DVZ_SCENE_SHADER_FORMAT_GLSL; + configuration.external_color_target = true; + configuration.color_target_id = Datoviz_Color_Target_Id; + configuration.color_target_format = DVZ_FORMAT_R8G8B8A8_UNORM; + configuration.target_width = state.viewport.width; + configuration.target_height = state.viewport.height; + configuration.device_scale_x = state.device_scale_x; + configuration.device_scale_y = state.device_scale_y; + configuration.render_scale = state.render_scale; + configuration.user_scale = state.user_scale; + configuration.clear_color[0] = state.clear_color.red; + configuration.clear_color[1] = state.clear_color.green; + configuration.clear_color[2] = state.clear_color.blue; + configuration.clear_color[3] = state.clear_color.alpha; + return configuration; +} + +} // namespace + +struct Datoviz_Transaction_Api { + Datoviz_Backend& backend; + Frame_Target_Pool& targets; + Frame_Target& target; + DvzFramePlanEmitConfig configuration; + + [[nodiscard]] DvzSceneFrameArtifact* emit() { return backend.emit(configuration); } + + [[nodiscard]] static const DvzDrp2CommandStream* stream(DvzSceneFrameArtifact& artifact) { + return dvz_scene_frame_artifact_stream(&artifact); + } + + [[nodiscard]] bool attach() { + const DvzStreamFrame frame = target.stream_frame(); + const bool attached = backend.attach(frame); + if (attached) + targets.external_target_replaced(); + return attached; + } + + [[nodiscard]] bool execute(const DvzDrp2CommandStream& stream) { + return backend.execute(stream); + } + + static void destroy(DvzSceneFrameArtifact& artifact) { + dvz_scene_frame_artifact_destroy(&artifact); + } +}; + +bool Datoviz_Frame_Executor::execute( + Datoviz_Backend& backend, Frame_Target_Pool& targets, Frame_Target& target, + const Render_3D_Frame_Data& frame) { + if (!target.begin()) { + std::fputs("Render_3D: failed to begin the owned command buffer\n", stderr); + return false; + } + + Datoviz_Transaction_Api api{ + backend, targets, target, emit_configuration(frame.state)}; + const auto transaction = execute_ordered_artifact_transaction(api); + if (!static_cast(transaction)) { + std::fprintf( + stderr, + "Render_3D: Datoviz transaction failed (emit=%d stream=%d attach=%d execute=%d execute_ok=%d)\n", + transaction.artifact_emitted, transaction.stream_available, + transaction.target_attached, transaction.stream_executed, + transaction.execution_succeeded); + target.abort(); + return false; + } + if (!target.submit_and_wait()) { + std::fputs("Render_3D: failed to submit or complete the owned command buffer\n", stderr); + return false; + } + return true; +} + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Datoviz_Frame_Executor.h b/render_3D/render_3D/detail/Datoviz_Frame_Executor.h new file mode 100644 index 0000000..0e82a17 --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Frame_Executor.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Render_3D_Core.h" + +namespace renderive::detail { + +class Datoviz_Backend; +class Frame_Target; +class Frame_Target_Pool; + +class Datoviz_Frame_Executor final { +public: + [[nodiscard]] static bool execute( + Datoviz_Backend& backend, Frame_Target_Pool& targets, Frame_Target& target, + const Render_3D_Frame_Data& frame); +}; + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Datoviz_Scene_Binding.cpp b/render_3D/render_3D/detail/Datoviz_Scene_Binding.cpp new file mode 100644 index 0000000..0619483 --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Scene_Binding.cpp @@ -0,0 +1,114 @@ +#include "Datoviz_Scene_Binding.h" + +#include +#include +#include +#include + +namespace renderive::detail { + +Datoviz_Scene_Binding::Datoviz_Scene_Binding(DvzScene* scene, Extent_2D initial_extent) + : scene_(scene) { + if (scene_ == nullptr) + throw std::invalid_argument("Datoviz scene binding requires a scene"); + const auto width = std::max(1U, initial_extent.width); + const auto height = std::max(1U, initial_extent.height); + figure_ = dvz_figure(scene_, width, height, 0); + if (figure_ == nullptr) + throw std::runtime_error("failed to create Datoviz figure"); + panel_ = dvz_panel_full(figure_); + if (panel_ == nullptr) + throw std::runtime_error("failed to create Datoviz panel"); +} + +bool Datoviz_Scene_Binding::apply(const Render_3D_State& state, std::uint64_t revision) { + if (revision == applied_revision_) + return true; + if (!state.viewport.empty() && + dvz_figure_resize(figure_, state.viewport.width, state.viewport.height) != DVZ_OK) + return false; + if (!apply_camera(state.camera)) + return false; + + std::unordered_set live_ids; + live_ids.reserve(state.objects.size()); + for (const auto& object : state.objects) { + live_ids.insert(object.id.value); + auto [iterator, inserted] = visuals_.try_emplace(object.id.value); + auto& binding = iterator->second; + if (inserted) { + binding.visual = dvz_sphere(scene_, 0); + if (binding.visual == nullptr || + dvz_panel_add_visual(panel_, binding.visual, nullptr) != DVZ_OK || + dvz_visual_set_depth_test(binding.visual, true) != DVZ_OK) + return false; + } + if (binding.applied_revision != object.revision && !apply_object(object, binding)) + return false; + } + + for (auto iterator = visuals_.begin(); iterator != visuals_.end();) { + if (!live_ids.contains(iterator->first)) { + if (dvz_visual_set_visible(iterator->second.visual, false) != DVZ_OK) + return false; + iterator = visuals_.erase(iterator); + } else + ++iterator; + } + applied_revision_ = revision; + return true; +} + +DvzSceneFrameArtifact* Datoviz_Scene_Binding::emit( + const DvzCapabilitySnapshot& capabilities, const DvzFramePlanEmitConfig& configuration) { + DvzDiagnosticReport report{}; + dvz_diagnostic_report_init(&report); + return dvz_figure_emit_frame(figure_, &capabilities, &report, &configuration); +} + +bool Datoviz_Scene_Binding::apply_camera(const Camera_3D& camera) { + DvzPanelView3DDesc description = dvz_panel_view3d_desc(); + description.view.eye[0] = camera.eye.x; + description.view.eye[1] = camera.eye.y; + description.view.eye[2] = camera.eye.z; + description.view.target[0] = camera.target.x; + description.view.target[1] = camera.target.y; + description.view.target[2] = camera.target.z; + description.view.up[0] = camera.up.x; + description.view.up[1] = camera.up.y; + description.view.up[2] = camera.up.z; + description.projection.type = camera.projection == Camera_Projection::Perspective + ? DVZ_CAMERA_PERSPECTIVE + : DVZ_CAMERA_ORTHOGRAPHIC; + description.projection.fov_y = camera.vertical_field_of_view_radians; + description.projection.near_clip = camera.near_clip; + description.projection.far_clip = camera.far_clip; + description.projection.ortho_height = camera.orthographic_height; + return dvz_panel_set_view3d_desc(panel_, &description) == DVZ_OK; +} + +bool Datoviz_Scene_Binding::apply_object( + const Object_3D_State& object, Visual_Binding& binding) { + const std::array position{object.sphere.position.x, object.sphere.position.y, object.sphere.position.z}; + const std::array color{ + object.sphere.color.red, object.sphere.color.green, + object.sphere.color.blue, object.sphere.color.alpha}; + const float radius = object.sphere.radius; + mat4 transform{}; + for (std::size_t row = 0; row < 4; ++row) { + for (std::size_t column = 0; column < 4; ++column) + transform[row][column] = object.sphere.transform.values[row * 4 + column]; + } + + if (dvz_visual_set_data(binding.visual, "position", position.data(), 1) != DVZ_OK || + dvz_visual_set_data(binding.visual, "color", color.data(), 1) != DVZ_OK || + dvz_visual_set_data(binding.visual, "radius", &radius, 1) != DVZ_OK || + dvz_visual_set_transform(binding.visual, transform) != DVZ_OK || + dvz_visual_set_visible(binding.visual, object.sphere.visible) != DVZ_OK) + return false; + + binding.applied_revision = object.revision; + return true; +} + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Datoviz_Scene_Binding.h b/render_3D/render_3D/detail/Datoviz_Scene_Binding.h new file mode 100644 index 0000000..7100340 --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Scene_Binding.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Render_3D_Core.h" + +#include + +#include +#include + +namespace renderive::detail { + +class Datoviz_Scene_Binding final { +public: + Datoviz_Scene_Binding(DvzScene* scene, Extent_2D initial_extent); + + Datoviz_Scene_Binding(const Datoviz_Scene_Binding&) = delete; + Datoviz_Scene_Binding& operator=(const Datoviz_Scene_Binding&) = delete; + + [[nodiscard]] bool apply(const Render_3D_State& state, std::uint64_t revision); + [[nodiscard]] DvzSceneFrameArtifact* emit( + const DvzCapabilitySnapshot& capabilities, const DvzFramePlanEmitConfig& configuration); + +private: + struct Visual_Binding { + DvzVisual* visual{}; + std::uint64_t applied_revision{}; + }; + + [[nodiscard]] bool apply_camera(const Camera_3D& camera); + [[nodiscard]] static bool apply_object( + const Object_3D_State& object, Visual_Binding& binding); + + DvzScene* scene_{}; + DvzFigure* figure_{}; + DvzPanel* panel_{}; + std::unordered_map visuals_; + std::uint64_t applied_revision_{}; +}; + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Frame_Target_Pool.cpp b/render_3D/render_3D/detail/Frame_Target_Pool.cpp new file mode 100644 index 0000000..6b4cd19 --- /dev/null +++ b/render_3D/render_3D/detail/Frame_Target_Pool.cpp @@ -0,0 +1,237 @@ +#include "Frame_Target_Pool.h" + +#include +#include + +namespace renderive::detail { +namespace { + +template +Resource* allocate_wrapper(Allocate allocate, const char* name) { + Resource* resource = allocate(); + if (resource == nullptr) + throw std::runtime_error(name); + return resource; +} + +} // namespace + +Frame_Target::Frame_Target( + DvzGpuCtx* gpu_context, Extent_2D extent, std::uint64_t generation) + : gpu_context_(gpu_context), extent_(extent), generation_(generation) { + if (gpu_context == nullptr || extent.empty()) + throw std::invalid_argument("invalid Render_3D frame target configuration"); + DvzDevice* device = dvz_gpu_ctx_device(gpu_context); + DvzVma* allocator = dvz_gpu_ctx_alloc(gpu_context); + DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context, DVZ_QUEUE_MAIN); + if (device == nullptr || allocator == nullptr || queue == nullptr) + throw std::invalid_argument("Render_3D frame target requires a complete GPU context"); + + try { + image_ = allocate_wrapper(dvz_images_create_wrapper, "failed to allocate Datoviz image wrapper"); + dvz_images(device, allocator, VK_IMAGE_TYPE_2D, 1, image_); + dvz_images_format(image_, VK_FORMAT_R8G8B8A8_UNORM); + dvz_images_size(image_, extent.width, extent.height, 1); + dvz_images_tiling(image_, VK_IMAGE_TILING_OPTIMAL); + dvz_images_usage(image_, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + dvz_images_alloc_flags(image_, DVZ_ALLOC_FLAGS_NONE); + if (dvz_images_create(image_) != 0) + throw std::runtime_error("failed to create Render_3D color image"); + + image_view_ = allocate_wrapper(dvz_image_views_create_wrapper, "failed to allocate Datoviz image-view wrapper"); + dvz_image_views(image_, image_view_); + dvz_image_views_type(image_view_, VK_IMAGE_VIEW_TYPE_2D); + dvz_image_views_aspect(image_view_, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_image_views_mip(image_view_, 0, 1); + dvz_image_views_layers(image_view_, 0, 1); + if (dvz_image_views_create(image_view_) != 0) + throw std::runtime_error("failed to create Render_3D color image view"); + + commands_ = allocate_wrapper(dvz_commands_create_wrapper, "failed to allocate Datoviz command wrapper"); + dvz_commands(device, queue, 1, commands_); + if (dvz_commands_handle(commands_) == VK_NULL_HANDLE) + throw std::runtime_error("failed to create Render_3D command buffer"); + + fence_ = allocate_wrapper(dvz_fence_create_wrapper, "failed to allocate Datoviz fence wrapper"); + dvz_fence(device, true, fence_); + if (dvz_fence_handle(fence_) == VK_NULL_HANDLE) + throw std::runtime_error("failed to create Render_3D fence"); + + submit_ = allocate_wrapper(dvz_submit_create_wrapper, "failed to allocate Datoviz submit wrapper"); + } catch (...) { + destroy(); + throw; + } +} + +Frame_Target::~Frame_Target() { + destroy(); +} + +bool Frame_Target::begin() { + wait(); + dvz_cmd_reset(commands_); + if (dvz_cmd_begin_result(commands_) != 0) + return false; + recording_ = true; + if (completed_layout_ == VK_IMAGE_LAYOUT_UNDEFINED) + transition_to_color_attachment(); + return true; +} + +DvzStreamFrame Frame_Target::stream_frame() const { + DvzStreamFrame frame{}; + frame.image = dvz_image_handle(image_, 0); + frame.command_buffer = dvz_commands_handle(commands_); + frame.image_view = dvz_image_views_handle(image_view_, 0); + frame.extent = {extent_.width, extent_.height}; + frame.color_format = VK_FORMAT_R8G8B8A8_UNORM; + frame.image_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + frame.usage = DVZ_STREAM_FRAME_USAGE_RENDER_TARGET | DVZ_STREAM_FRAME_USAGE_COPY_SRC; + frame.command_buffer_recording = recording_; + frame.image_borrowed = true; + frame.image_view_borrowed = true; + frame.command_buffer_borrowed = true; + frame.handles_dirty = true; + frame.resource_generation = generation_; + frame.image_valid = frame.image != VK_NULL_HANDLE && frame.image_view != VK_NULL_HANDLE; + frame.memory_fd = -1; + frame.wait_semaphore_fd = -1; + return frame; +} + +bool Frame_Target::submit_and_wait() { + if (!recording_ || dvz_cmd_end_result(commands_) != 0) { + abort(); + return false; + } + recording_ = false; + dvz_fence_reset(fence_); + dvz_submit(submit_); + dvz_submit_command(submit_, dvz_commands_handle(commands_)); + DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN); + if (dvz_submit_send(submit_, dvz_queue_handle(queue), dvz_fence_handle(fence_)) != VK_SUCCESS) + return false; + submitted_ = true; + if (!dvz_fence_wait(fence_)) { + dvz_device_wait(dvz_gpu_ctx_device(gpu_context_)); + submitted_ = false; + return false; + } + submitted_ = false; + completed_layout_ = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + return true; +} + +void Frame_Target::abort() noexcept { + if (submitted_) + wait(); + if (recording_ && commands_ != nullptr) + dvz_cmd_reset(commands_); + recording_ = false; +} + +void Frame_Target::wait() noexcept { + if (submitted_ && fence_ != nullptr) { + if (!dvz_fence_wait(fence_)) + dvz_device_wait(dvz_gpu_ctx_device(gpu_context_)); + submitted_ = false; + completed_layout_ = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } +} + +void Frame_Target::transition_to_color_attachment() { + DvzBarriers barriers{}; + dvz_barriers(&barriers); + auto* barrier = dvz_barriers_image(&barriers, dvz_image_handle(image_, 0)); + if (barrier == nullptr) + throw std::runtime_error("failed to allocate Render_3D image barrier"); + dvz_barrier_image_stage(barrier, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + dvz_barrier_image_access(barrier, 0, VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + dvz_barrier_image_layout(barrier, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + dvz_barrier_image_aspect(barrier, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_barrier_image_mip(barrier, 0, 1); + dvz_barrier_image_layers(barrier, 0, 1); + dvz_cmd_barriers(commands_, &barriers); +} + +void Frame_Target::destroy() noexcept { + wait(); + if (fence_ != nullptr) { + dvz_fence_destroy(fence_); + dvz_fence_free(fence_); + fence_ = nullptr; + } + if (submit_ != nullptr) { + dvz_submit_free(submit_); + submit_ = nullptr; + } + if (commands_ != nullptr) { + dvz_commands_destroy(commands_); + dvz_commands_free(commands_); + commands_ = nullptr; + } + if (image_view_ != nullptr) { + dvz_image_views_destroy(image_view_); + dvz_image_views_free(image_view_); + image_view_ = nullptr; + } + if (image_ != nullptr) { + dvz_images_destroy(image_); + dvz_images_free(image_); + image_ = nullptr; + } +} + +Frame_Target_Pool::Frame_Target_Pool(DvzGpuCtx* gpu_context, std::size_t slot_count) + : gpu_context_(gpu_context), slot_count_(slot_count) { + if (gpu_context == nullptr || slot_count < 2) + throw std::invalid_argument("Render_3D requires at least two valid GPU frame slots"); +} + +Frame_Target_Pool::~Frame_Target_Pool() { + wait_all(); +} + +Frame_Target& Frame_Target_Pool::acquire( + std::uint64_t frame_sequence, Extent_2D extent) { + if (frame_sequence == 0 || extent.empty()) + throw std::invalid_argument("cannot acquire an empty Render_3D target"); + if (targets_.empty() || targets_.front()->extent_ != extent) { + auto replacement = make_generation(extent); + wait_all(); + for (auto& target : targets_) + retired_targets_.push_back(std::move(target)); + targets_ = std::move(replacement); + } + Frame_Target& target = *targets_[(frame_sequence - 1) % targets_.size()]; + target.wait(); + return target; +} + +void Frame_Target_Pool::external_target_replaced() { + retired_targets_.clear(); +} + +void Frame_Target_Pool::runtime_released_targets() { + retired_targets_.clear(); +} + +void Frame_Target_Pool::wait_all() noexcept { + for (auto& target : targets_) + target->wait(); + for (auto& target : retired_targets_) + target->wait(); +} + +std::vector> Frame_Target_Pool::make_generation(Extent_2D extent) { + const std::uint64_t next_generation = generation_ + 1; + std::vector> result; + result.reserve(slot_count_); + for (std::size_t index = 0; index < slot_count_; ++index) + result.push_back(std::make_unique(gpu_context_, extent, next_generation)); + generation_ = next_generation; + return result; +} + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Frame_Target_Pool.h b/render_3D/render_3D/detail/Frame_Target_Pool.h new file mode 100644 index 0000000..3b55764 --- /dev/null +++ b/render_3D/render_3D/detail/Frame_Target_Pool.h @@ -0,0 +1,72 @@ +#pragma once + +#include "render_3D/Render_3D.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace renderive::detail { + +class Frame_Target final { +public: + Frame_Target(DvzGpuCtx* gpu_context, Extent_2D extent, std::uint64_t generation); + ~Frame_Target(); + + Frame_Target(const Frame_Target&) = delete; + Frame_Target& operator=(const Frame_Target&) = delete; + + [[nodiscard]] bool begin(); + [[nodiscard]] DvzStreamFrame stream_frame() const; + [[nodiscard]] bool submit_and_wait(); + void abort() noexcept; + void wait() noexcept; + +private: + friend class Frame_Target_Pool; + + void transition_to_color_attachment(); + void destroy() noexcept; + + DvzGpuCtx* gpu_context_{}; + Extent_2D extent_; + std::uint64_t generation_{}; + DvzImages* image_{}; + DvzImageViews* image_view_{}; + DvzCommands* commands_{}; + DvzFence* fence_{}; + DvzSubmit* submit_{}; + VkImageLayout completed_layout_{VK_IMAGE_LAYOUT_UNDEFINED}; + bool recording_{}; + bool submitted_{}; +}; + +class Frame_Target_Pool final { +public: + Frame_Target_Pool(DvzGpuCtx* gpu_context, std::size_t slot_count); + ~Frame_Target_Pool(); + + Frame_Target_Pool(const Frame_Target_Pool&) = delete; + Frame_Target_Pool& operator=(const Frame_Target_Pool&) = delete; + + [[nodiscard]] Frame_Target& acquire(std::uint64_t frame_sequence, Extent_2D extent); + void external_target_replaced(); + void runtime_released_targets(); + void wait_all() noexcept; + +private: + [[nodiscard]] std::vector> make_generation(Extent_2D extent); + + DvzGpuCtx* gpu_context_{}; + std::size_t slot_count_{}; + std::uint64_t generation_{}; + std::vector> targets_; + std::vector> retired_targets_; +}; + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Ordered_Artifact_Transaction.h b/render_3D/render_3D/detail/Ordered_Artifact_Transaction.h new file mode 100644 index 0000000..b08fb9a --- /dev/null +++ b/render_3D/render_3D/detail/Ordered_Artifact_Transaction.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +namespace renderive::detail { + +struct Artifact_Transaction_Result { + bool artifact_emitted{}; + bool stream_available{}; + bool target_attached{}; + bool stream_executed{}; + bool execution_succeeded{}; + + [[nodiscard]] explicit operator bool() const noexcept { + return artifact_emitted && stream_available && target_attached && + stream_executed && execution_succeeded; + } +}; + +template +[[nodiscard]] Artifact_Transaction_Result execute_ordered_artifact_transaction(Api& api) { + Artifact_Transaction_Result result; + auto* artifact = api.emit(); + if (artifact == nullptr) + return result; + result.artifact_emitted = true; + + struct Artifact_Guard { + Api& api; + decltype(artifact) artifact; + ~Artifact_Guard() { api.destroy(*artifact); } + } guard{api, artifact}; + + auto* stream = api.stream(*artifact); + if (stream == nullptr) + return result; + result.stream_available = true; + + result.target_attached = api.attach(); + result.stream_executed = true; + result.execution_succeeded = api.execute(*stream); + return result; +} + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Ordered_Backend_Shutdown.h b/render_3D/render_3D/detail/Ordered_Backend_Shutdown.h new file mode 100644 index 0000000..b3bdf95 --- /dev/null +++ b/render_3D/render_3D/detail/Ordered_Backend_Shutdown.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace renderive::detail { + +template +requires std::invocable && std::invocable && + std::invocable && std::invocable +void ordered_backend_shutdown( + Destroy_Runtime&& destroy_runtime, + Destroy_Targets&& destroy_targets, + Destroy_Scene&& destroy_scene, + Destroy_Gpu_Context&& destroy_gpu_context) { + std::invoke(std::forward(destroy_runtime)); + std::invoke(std::forward(destroy_targets)); + std::invoke(std::forward(destroy_scene)); + std::invoke(std::forward(destroy_gpu_context)); +} + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Render_3D_Core.h b/render_3D/render_3D/detail/Render_3D_Core.h new file mode 100644 index 0000000..26e8bad --- /dev/null +++ b/render_3D/render_3D/detail/Render_3D_Core.h @@ -0,0 +1,139 @@ +#pragma once + +#include "render_3D/Render_3D.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::detail { + +struct Object_3D_State { + Object_3D_Id id; + Sphere_3D sphere; + std::uint64_t revision{1}; + bool operator==(const Object_3D_State&) const = default; +}; + +struct Render_3D_State { + Extent_2D viewport{800, 600}; + Camera_3D camera; + Clear_Color clear_color; + float device_scale_x{1.0F}; + float device_scale_y{1.0F}; + float render_scale{1.0F}; + float user_scale{1.0F}; + std::vector objects; + bool operator==(const Render_3D_State&) const = default; +}; + +struct Render_3D_Frame_Data { + Render_3D_State state; + std::uint64_t revision{}; + std::uint64_t frame_sequence{}; +}; + +struct Logical_State_Base {}; + +class Logical_State_Buffer final + : public ::Double_State_Strategy { + using Base = ::Double_State_Strategy; + +public: + struct Published_Frame { + Render_3D_State state; + std::uint64_t revision{}; + }; + + explicit Logical_State_Buffer(const Render_3D_State& initial) : Base(initial) {} + + [[nodiscard]] Published_Frame publish_frame() { + std::lock_guard publication_lock(publication_mutex_); + Base::publish(); + return {Base::render_state_value(), Base::state_revision()}; + } + +private: + std::mutex publication_mutex_; +}; + +class Frame_Scheduler final { + using Manual = ::Manual_Refresh_Strategy; + using Low_Latency = ::Low_Latency_Strategy; + using Playback = ::Flow_Refresh_Strategy; + using Strategy = std::variant, std::unique_ptr, std::unique_ptr>; + +public: + Frame_Scheduler(Frame_3D_Control_Mode mode, double maximum_frames_per_second) + : strategy_(make_strategy(mode, maximum_frames_per_second)) {} + + [[nodiscard]] bool prepare(Logical_State_Buffer& states) { + return std::visit( + [&](auto& strategy) { + auto lease = strategy->acquire_painter(); + if (!lease) + return false; + auto published = states.publish_frame(); + lease->state = std::move(published.state); + lease->revision = published.revision; + lease->frame_sequence = lease->statistics.sequence; + return true; + }, + strategy_); + } + + [[nodiscard]] bool refresh() { + auto* manual = std::get_if>(&strategy_); + return manual != nullptr && (*manual)->refresh(); + } + + [[nodiscard]] bool discard_pending() { + if (auto* manual = std::get_if>(&strategy_)) + return (*manual)->discard_pending_frame(); + if (auto* low_latency = std::get_if>(&strategy_)) + return (*low_latency)->discard_pending_frame(); + return false; + } + + template + requires std::invocable + [[nodiscard]] bool render(Render&& render) { + return std::visit( + [&](auto& strategy) { + auto lease = strategy->acquire_renderer(); + if (!lease) + return false; + return static_cast(std::invoke(std::forward(render), static_cast(*lease))); + }, + strategy_); + } + +private: + static Strategy make_strategy(Frame_3D_Control_Mode mode, double maximum_frames_per_second) { + switch (mode) { + case Frame_3D_Control_Mode::Manual: + return Strategy(std::in_place_type>, std::make_unique()); + case Frame_3D_Control_Mode::Low_Latency: + return Strategy( + std::in_place_type>, + std::make_unique(Observer_State<>{}, typename Low_Latency::Configuration{maximum_frames_per_second})); + case Frame_3D_Control_Mode::Playback: + return Strategy(std::in_place_type>, std::make_unique()); + } + throw std::invalid_argument("unknown Render_3D frame control mode"); + } + + Strategy strategy_; +}; + +} // namespace renderive::detail diff --git a/render_3D/render_3D/detail/Render_Domain.h b/render_3D/render_3D/detail/Render_Domain.h new file mode 100644 index 0000000..96c460c --- /dev/null +++ b/render_3D/render_3D/detail/Render_Domain.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::detail { + +class Render_Domain final { +public: + Render_Domain() : thread_([this] { run(); }) {} + + ~Render_Domain() { + { + std::lock_guard lock(mutex_); + stopping_ = true; + } + condition_.notify_one(); + if (thread_.joinable()) + thread_.join(); + } + + Render_Domain(const Render_Domain&) = delete; + Render_Domain& operator=(const Render_Domain&) = delete; + + template + auto invoke(Function&& function) -> std::invoke_result_t { + using Result = std::invoke_result_t; + auto task = std::make_shared>(std::forward(function)); + auto result = task->get_future(); + { + std::lock_guard lock(mutex_); + if (stopping_) + throw std::runtime_error("Render_3D render domain is stopping"); + tasks_.emplace([task] { (*task)(); }); + } + condition_.notify_one(); + if constexpr (std::is_void_v) { + result.get(); + } else { + return result.get(); + } + } + +private: + void run() { + for (;;) { + std::function task; + { + std::unique_lock lock(mutex_); + condition_.wait(lock, [&] { return stopping_ || !tasks_.empty(); }); + if (stopping_ && tasks_.empty()) + return; + task = std::move(tasks_.front()); + tasks_.pop(); + } + task(); + } + } + + std::mutex mutex_; + std::condition_variable condition_; + std::queue> tasks_; + bool stopping_{}; + std::thread thread_; +}; + +} // namespace renderive::detail diff --git a/render_3D/render_3D/export.h b/render_3D/render_3D/export.h new file mode 100644 index 0000000..c59c228 --- /dev/null +++ b/render_3D/render_3D/export.h @@ -0,0 +1,3 @@ +#pragma once + +#include "Render_3D.h" 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/tests/Datoviz_Transaction_Tests.cpp b/render_3D/tests/Datoviz_Transaction_Tests.cpp new file mode 100644 index 0000000..0aada99 --- /dev/null +++ b/render_3D/tests/Datoviz_Transaction_Tests.cpp @@ -0,0 +1,96 @@ +#include + +#include "render_3D/detail/Ordered_Artifact_Transaction.h" +#include "render_3D/detail/Ordered_Backend_Shutdown.h" + +#include +#include + +namespace renderive::detail { +namespace { + +struct Fake_Artifact { + int stream = 7; +}; + +struct Fake_Transaction_Api { + std::vector events; + Fake_Artifact artifact; + bool emit_succeeds = true; + bool attach_succeeds = true; + bool execute_succeeds = true; + std::size_t execute_count{}; + + Fake_Artifact* emit() { + events.emplace_back("emit"); + return emit_succeeds ? &artifact : nullptr; + } + + int* stream(Fake_Artifact& value) { + events.emplace_back("stream"); + return &value.stream; + } + + bool attach() { + events.emplace_back("attach"); + return attach_succeeds; + } + + bool execute(const int&) { + events.emplace_back("execute"); + ++execute_count; + return execute_succeeds; + } + + void destroy(Fake_Artifact&) { events.emplace_back("destroy"); } +}; + +TEST(DatovizTransaction, SuccessfulEmitExecutesExactlyOnceInCausalOrder) { + Fake_Transaction_Api api; + + EXPECT_TRUE(static_cast(execute_ordered_artifact_transaction(api))); + EXPECT_EQ(api.execute_count, 1U); + EXPECT_EQ(api.events, std::vector({"emit", "stream", "attach", "execute", "destroy"})); +} + +TEST(DatovizTransaction, AttachFailureStillConsumesEmittedArtifactExactlyOnce) { + Fake_Transaction_Api api; + api.attach_succeeds = false; + + EXPECT_FALSE(static_cast(execute_ordered_artifact_transaction(api))); + EXPECT_EQ(api.execute_count, 1U); + EXPECT_EQ(api.events, std::vector({"emit", "stream", "attach", "execute", "destroy"})); +} + +TEST(DatovizTransaction, ExecuteFailureConsumesAndDestroysArtifactBeforeRecovery) { + Fake_Transaction_Api api; + api.execute_succeeds = false; + + EXPECT_FALSE(static_cast(execute_ordered_artifact_transaction(api))); + EXPECT_EQ(api.execute_count, 1U); + EXPECT_EQ(api.events, std::vector({"emit", "stream", "attach", "execute", "destroy"})); +} + +TEST(DatovizTransaction, FailedEmitDoesNotStartAttachOrExecute) { + Fake_Transaction_Api api; + api.emit_succeeds = false; + + EXPECT_FALSE(static_cast(execute_ordered_artifact_transaction(api))); + EXPECT_EQ(api.execute_count, 0U); + EXPECT_EQ(api.events, std::vector({"emit"})); +} + +TEST(DatovizOwnership, ShutdownOrderProtectsBorrowedDeviceAndTargets) { + std::vector events; + + ordered_backend_shutdown( + [&] { events.emplace_back("runtime"); }, + [&] { events.emplace_back("targets"); }, + [&] { events.emplace_back("scene"); }, + [&] { events.emplace_back("gpu_ctx"); }); + + EXPECT_EQ(events, std::vector({"runtime", "targets", "scene", "gpu_ctx"})); +} + +} // namespace +} // namespace renderive::detail diff --git a/render_3D/tests/Render_3D_Core_Tests.cpp b/render_3D/tests/Render_3D_Core_Tests.cpp new file mode 100644 index 0000000..b58adf3 --- /dev/null +++ b/render_3D/tests/Render_3D_Core_Tests.cpp @@ -0,0 +1,160 @@ +#include + +#include "render_3D/detail/Render_3D_Core.h" + +#include +#include +#include +#include + +namespace renderive::detail { +namespace { + +Render_3D_State state_with_width(std::uint32_t width) { + Render_3D_State state; + state.viewport = {width, 600}; + return state; +} + +TEST(Render3DState, WriterDoesNotChangePublishedStateUntilPublish) { + Logical_State_Buffer states(state_with_width(800)); + + const auto front = states.publish_frame(); + + states.update([](Render_3D_State& state) { state.viewport.width = 1920; }); + + EXPECT_EQ(front.state.viewport.width, 800U); + const auto frame = states.publish_frame(); + EXPECT_EQ(frame.state.viewport.width, 1920U); + EXPECT_EQ(frame.revision, 2U); +} + +TEST(Render3DFrameStrategy, LowLatencyDropsBeforeRenderTransaction) { + Logical_State_Buffer states(state_with_width(1)); + Frame_Scheduler scheduler(Frame_3D_Control_Mode::Low_Latency, 60.0); + + for (const std::uint32_t width : {10U, 11U, 12U}) { + states.update([width](Render_3D_State& state) { state.viewport.width = width; }); + ASSERT_TRUE(scheduler.prepare(states)); + } + + std::vector emitted; + std::vector sequences; + ASSERT_TRUE(scheduler.render([&](const Render_3D_Frame_Data& frame) { + emitted.push_back(frame.state.viewport.width); + sequences.push_back(frame.frame_sequence); + return true; + })); + + EXPECT_EQ(emitted, std::vector({12U})); + EXPECT_EQ(sequences, std::vector({3U})); +} + +TEST(Render3DFrameStrategy, ManualRefreshControlsVisibilityAndReplay) { + Logical_State_Buffer states(state_with_width(1)); + Frame_Scheduler scheduler(Frame_3D_Control_Mode::Manual, 60.0); + + states.update([](Render_3D_State& state) { state.viewport.width = 20; }); + ASSERT_TRUE(scheduler.prepare(states)); + + std::vector rendered; + std::vector sequences; + const auto render = [&](const Render_3D_Frame_Data& frame) { + rendered.push_back(frame.state.viewport.width); + sequences.push_back(frame.frame_sequence); + return true; + }; + EXPECT_FALSE(scheduler.render(render)); + ASSERT_TRUE(scheduler.refresh()); + EXPECT_TRUE(scheduler.render(render)); + EXPECT_TRUE(scheduler.render(render)); + EXPECT_EQ(rendered, std::vector({20U, 20U})); + EXPECT_EQ(sequences, std::vector({1U, 1U})); +} + +TEST(Render3DFrameStrategy, PlaybackExecutesEveryPublishedStateInOrder) { + Logical_State_Buffer states(state_with_width(1)); + Frame_Scheduler scheduler(Frame_3D_Control_Mode::Playback, 60.0); + + for (const std::uint32_t width : {30U, 31U, 32U}) { + states.update([width](Render_3D_State& state) { state.viewport.width = width; }); + ASSERT_TRUE(scheduler.prepare(states)); + } + + std::vector rendered; + std::vector sequences; + const auto render = [&](const Render_3D_Frame_Data& frame) { + rendered.push_back(frame.state.viewport.width); + sequences.push_back(frame.frame_sequence); + return true; + }; + EXPECT_TRUE(scheduler.render(render)); + EXPECT_TRUE(scheduler.render(render)); + EXPECT_TRUE(scheduler.render(render)); + EXPECT_FALSE(scheduler.render(render)); + EXPECT_EQ(rendered, std::vector({30U, 31U, 32U})); + EXPECT_EQ(sequences, std::vector({1U, 2U, 3U})); +} + +TEST(Render3DFrameStrategy, DiscardNeverStartsRenderTransaction) { + Logical_State_Buffer states(state_with_width(40)); + Frame_Scheduler scheduler(Frame_3D_Control_Mode::Low_Latency, 60.0); + + ASSERT_TRUE(scheduler.prepare(states)); + ASSERT_TRUE(scheduler.discard_pending()); + + std::size_t render_transaction_count{}; + EXPECT_FALSE(scheduler.render([&](const Render_3D_Frame_Data&) { + ++render_transaction_count; + return true; + })); + EXPECT_EQ(render_transaction_count, 0U); +} + +TEST(Render3DState, ConcurrentPublishNeverExposesPartialLogicalState) { + Logical_State_Buffer states(state_with_width(1)); + Frame_Scheduler scheduler(Frame_3D_Control_Mode::Low_Latency, 60.0); + std::atomic producer_done{}; + std::atomic invalid_snapshot{}; + std::atomic non_monotonic_revision{}; + std::atomic maximum_width{}; + + std::thread producer([&] { + for (std::uint32_t width = 2; width <= 500; ++width) { + states.update([width](Render_3D_State& state) { + state.viewport.width = width; + state.viewport.height = width * 2; + }); + (void)scheduler.prepare(states); + } + producer_done.store(true, std::memory_order_release); + }); + + std::thread renderer([&] { + std::uint64_t last_revision{}; + while (!producer_done.load(std::memory_order_acquire) || maximum_width.load() < 500) { + (void)scheduler.render([&](const Render_3D_Frame_Data& frame) { + if (frame.state.viewport.height != frame.state.viewport.width * 2) + invalid_snapshot.store(true, std::memory_order_relaxed); + if (frame.revision < last_revision) + non_monotonic_revision.store(true, std::memory_order_relaxed); + last_revision = frame.revision; + auto observed = maximum_width.load(std::memory_order_relaxed); + while (observed < frame.state.viewport.width && + !maximum_width.compare_exchange_weak(observed, frame.state.viewport.width)) { + } + return true; + }); + std::this_thread::yield(); + } + }); + + producer.join(); + renderer.join(); + EXPECT_FALSE(invalid_snapshot.load()); + EXPECT_FALSE(non_monotonic_revision.load()); + EXPECT_EQ(maximum_width.load(), 500U); +} + +} // namespace +} // namespace renderive::detail diff --git a/render_3D/tests/Render_3D_Integration_Tests.cpp b/render_3D/tests/Render_3D_Integration_Tests.cpp new file mode 100644 index 0000000..9cf6232 --- /dev/null +++ b/render_3D/tests/Render_3D_Integration_Tests.cpp @@ -0,0 +1,216 @@ +#include + +#include "render_3D/Render_3D.h" +#include "render_3D/detail/Datoviz_Backend.h" +#include "render_3D/detail/Ordered_Backend_Shutdown.h" + +#include +#include +#include +#include +#include + +namespace renderive { +namespace { + +std::unique_ptr build_or_skip(Frame_3D_Control_Mode mode) { + try { + Render_3D::Builder builder; + return builder + .frame_control(mode) + .viewport({96, 64}) + .frame_slots(2) + .gpu(0, false) + .clear_color({0.02F, 0.03F, 0.04F, 1.0F}) + .build(); + } catch (const std::exception&) { + return {}; + } + return {}; +} + +bool publish_and_render(Render_3D& renderer, Frame_3D_Control_Mode mode) { + if (!renderer.prepare_frame()) + return false; + if (mode == Frame_3D_Control_Mode::Manual && !renderer.refresh_manual_frame()) + return false; + return renderer.render_prepared_frame(); +} + +struct Backend_Harness { + explicit Backend_Harness(const detail::Render_3D_State& state) { + backend = std::make_unique(0, false, state); + targets = backend->create_target_pool(2); + } + + ~Backend_Harness() { + if (backend == nullptr) + return; + if (targets != nullptr) + targets->wait_all(); + detail::ordered_backend_shutdown( + [&] { backend->destroy_runtime(); }, + [&] { targets.reset(); }, + [&] { backend->destroy_scene(); }, + [&] { backend->destroy_gpu_context(); }); + } + + std::unique_ptr backend; + std::unique_ptr targets; +}; + +class Render3DStrategyIntegration + : public testing::TestWithParam {}; + +TEST_P(Render3DStrategyIntegration, UsesExternalTargetsAcrossResizeGenerations) { + const auto mode = GetParam(); + auto renderer = build_or_skip(mode); + if (renderer == nullptr) + GTEST_SKIP() << "Vulkan/Datoviz backend unavailable"; + ASSERT_NE(renderer, nullptr); + + Sphere_3D sphere; + sphere.radius = 0.35F; + sphere.color = {220, 80, 40, 255}; + const Object_3D_Id id = renderer->add_sphere(sphere); + + ASSERT_TRUE(publish_and_render(*renderer, mode)); + for (int frame = 0; frame < 2; ++frame) { + sphere.position.x += 0.02F; + ASSERT_TRUE(renderer->replace_sphere(id, sphere)); + ASSERT_TRUE(publish_and_render(*renderer, mode)); + } + for (const Extent_2D extent : {Extent_2D{128, 72}, Extent_2D{80, 80}, Extent_2D{160, 90}}) { + renderer->resize(extent); + sphere.position.x += 0.05F; + ASSERT_TRUE(renderer->replace_sphere(id, sphere)); + ASSERT_TRUE(publish_and_render(*renderer, mode)); + } + renderer->wait_for_render(); +} + +INSTANTIATE_TEST_SUITE_P( + AllKernelStrategies, Render3DStrategyIntegration, + testing::Values( + Frame_3D_Control_Mode::Low_Latency, + Frame_3D_Control_Mode::Manual, + Frame_3D_Control_Mode::Playback)); + +TEST(Render3DIntegration, BuiltObjectOutlivesBuilderConfiguration) { + std::unique_ptr renderer; + try { + { + Render_3D::Builder builder; + renderer = builder.viewport({64, 64}).frame_slots(2).build(); + } + } catch (const std::exception& error) { + GTEST_SKIP() << "Vulkan/Datoviz backend unavailable: " << error.what(); + } + + ASSERT_NE(renderer, nullptr); + (void)renderer->add_sphere({}); + ASSERT_TRUE(renderer->prepare_frame()); + EXPECT_TRUE(renderer->render_prepared_frame()); +} + +TEST(Render3DIntegration, RepeatedCreateRenderResizeDestroyIsStable) { + for (int iteration = 0; iteration < 3; ++iteration) { + auto renderer = build_or_skip(Frame_3D_Control_Mode::Low_Latency); + if (renderer == nullptr) + GTEST_SKIP() << "Vulkan/Datoviz backend unavailable"; + ASSERT_NE(renderer, nullptr); + (void)renderer->add_sphere({}); + ASSERT_TRUE(publish_and_render(*renderer, Frame_3D_Control_Mode::Low_Latency)); + renderer->resize({static_cast(100 + iteration * 8), 72}); + ASSERT_TRUE(publish_and_render(*renderer, Frame_3D_Control_Mode::Low_Latency)); + } +} + +TEST(Render3DIntegration, SlotTargetsRotateAndBackendRebuildsFromLogicalState) { + detail::Render_3D_State state; + state.viewport = {64, 64}; + std::unique_ptr harness; + try { + harness = std::make_unique(state); + } catch (const std::exception& error) { + GTEST_SKIP() << "Vulkan/Datoviz backend unavailable: " << error.what(); + } + + detail::Frame_Target& slot_0 = harness->targets->acquire(1, state.viewport); + ASSERT_TRUE(slot_0.begin()); + const DvzStreamFrame slot_0_frame = slot_0.stream_frame(); + EXPECT_TRUE(slot_0_frame.command_buffer_recording); + EXPECT_TRUE(slot_0_frame.image_borrowed); + EXPECT_TRUE(slot_0_frame.image_view_borrowed); + EXPECT_TRUE(slot_0_frame.command_buffer_borrowed); + slot_0.abort(); + + detail::Frame_Target& slot_1 = harness->targets->acquire(2, state.viewport); + const DvzStreamFrame slot_1_frame = slot_1.stream_frame(); + EXPECT_NE(slot_0_frame.image, slot_1_frame.image); + EXPECT_EQ(slot_0_frame.resource_generation, slot_1_frame.resource_generation); + + detail::Frame_Target& reused_slot_0 = harness->targets->acquire(3, state.viewport); + const DvzStreamFrame reused_slot_0_frame = reused_slot_0.stream_frame(); + EXPECT_EQ(slot_0_frame.image, reused_slot_0_frame.image); + EXPECT_EQ(slot_0_frame.command_buffer, reused_slot_0_frame.command_buffer); + + detail::Render_3D_Frame_Data frame{state, 1, 4}; + ASSERT_TRUE(harness->backend->render(frame, *harness->targets)); + + harness->backend->destroy_runtime(); + frame.state.clear_color.red = 0.25F; + ++frame.revision; + ++frame.frame_sequence; + EXPECT_TRUE(harness->backend->render(frame, *harness->targets)); +} + +TEST(Render3DIntegration, ConcurrentStateUpdatesAndRenderingStopBeforeDestroy) { + auto renderer = build_or_skip(Frame_3D_Control_Mode::Low_Latency); + if (renderer == nullptr) + GTEST_SKIP() << "Vulkan/Datoviz backend unavailable"; + + Sphere_3D sphere; + const Object_3D_Id id = renderer->add_sphere(sphere); + std::atomic first_frame_prepared{}; + std::atomic failed{}; + + std::thread producer([&] { + try { + for (int update = 0; update < 6; ++update) { + sphere.position.x = static_cast(update) * 0.03F; + if (!renderer->replace_sphere(id, sphere) || !renderer->prepare_frame()) { + failed.store(true, std::memory_order_release); + return; + } + if (update == 0) + first_frame_prepared.store(true, std::memory_order_release); + std::this_thread::yield(); + } + } catch (...) { + failed.store(true, std::memory_order_release); + } + }); + + std::thread consumer([&] { + while (!first_frame_prepared.load(std::memory_order_acquire) && + !failed.load(std::memory_order_acquire)) + std::this_thread::yield(); + try { + for (int frame = 0; frame < 6; ++frame) + (void)renderer->render_prepared_frame(); + } catch (...) { + failed.store(true, std::memory_order_release); + } + }); + + producer.join(); + consumer.join(); + ASSERT_FALSE(failed.load(std::memory_order_acquire)); + ASSERT_TRUE(renderer->prepare_frame()); + ASSERT_TRUE(renderer->render_prepared_frame()); + renderer->wait_for_render(); +} + +} // namespace +} // namespace renderive