首次提交 基础架构完成

This commit is contained in:
2026-08-19 12:44:45 +08:00
commit a6b350e087
14 changed files with 2380 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
include_guard(GLOBAL)
block()
rcl_init(render_kernel)
set(oneTBB_option ${base_options})
list(APPEND oneTBB_option
-DBUILD_SHARED_LIBS=ON
-DTBB_BUILD=ON
-DTBB_INSTALL=ON
-DTBB_TEST=OFF
-DTBB_EXAMPLES=OFF
-DTBB_STRICT=OFF
-DTBB4PY_BUILD=OFF
-DTBBMALLOC_BUILD=OFF
-DTBBMALLOC_PROXY_BUILD=OFF
)
_register_git_cmake_library(render_kernel::oneTBB
"https://github.com/uxlfoundation/oneTBB.git"
"v2023.1.0"
)
rcl_get_effective_install_dir(render_kernel::oneTBB ONETBB_ROOT)
set(TBB_CONFIG_DIR "${ONETBB_ROOT}/lib/cmake/TBB")
rcl_cmake_library_set_cmake_options(render_kernel::oneTBB ${oneTBB_option})
rcl_cmake_library_set_other_use_opt(render_kernel::oneTBB
"-DTBB_DIR=\"${TBB_CONFIG_DIR}\""
)
rcl_cmake_library_set_init_script(render_kernel::oneTBB
"set(ONETBB_ROOT \"${ONETBB_ROOT}\")"
"set(TBB_DIR \"${TBB_CONFIG_DIR}\")"
)
rcl_cmake_library_set_clear_script(render_kernel::oneTBB
"unset(ONETBB_ROOT)"
"unset(TBB_DIR)"
)
endblock()
+96
View File
@@ -0,0 +1,96 @@
include(${CMAKE_CURRENT_LIST_DIR}/cmake/rely.cmake)
set(Aethera_Kernel_dependencies render_kernel::oneTBB global::magic_enum global::expected)
if (Aethera_BUILD_TESTS)
set(Aethera_Kernel_test_targets)
list(APPEND Aethera_Kernel_dependencies global::GTest)
endif ()
rcl_add_dependency_action_targets(Aethera_Kernel_env ${Aethera_Kernel_dependencies})
set_target_properties(Aethera_Kernel_env PROPERTIES FOLDER Aethera_Kernel)
library_get_missing_with_rely(Aethera_Kernel_dependencies_installed ${Aethera_Kernel_dependencies})
if (Aethera_Kernel_dependencies_installed)
rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Aethera_Kernel dependencies are not installed. Build Aethera_Kernel_env first")
return()
endif ()
rcl_load_dependency_environment(${Aethera_Kernel_dependencies})
find_package(Threads REQUIRED)
find_package(TBB CONFIG REQUIRED)
find_package(magic_enum CONFIG REQUIRED)
find_package(tl-expected CONFIG REQUIRED)
if (Aethera_BUILD_TESTS)
find_package(GTest CONFIG REQUIRED)
endif ()
set(Aethera_Kernel_source_dir "${CMAKE_CURRENT_LIST_DIR}/src/kernel")
append_glob_source(Aethera_Kernel_sources "${Aethera_Kernel_source_dir}")
add_library(Aethera_Kernel STATIC ${Aethera_Kernel_sources})
add_library(Renderive::Kernel ALIAS Aethera_Kernel)
set_target_properties(Aethera_Kernel PROPERTIES EXPORT_NAME Kernel)
target_include_directories(Aethera_Kernel PUBLIC
"$<BUILD_INTERFACE:${Aethera_Kernel_source_dir}>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)
target_compile_features(Aethera_Kernel PUBLIC cxx_std_20)
target_link_libraries(Aethera_Kernel PUBLIC
TBB::tbb
magic_enum::magic_enum
tl::expected)
target_link_libraries(Aethera_Kernel PRIVATE Threads::Threads)
function(Aethera_stage_kernel_runtime target)
if (WIN32)
add_custom_command(TARGET "${target}" POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<TARGET_FILE:TBB::tbb>"
"$<TARGET_FILE_DIR:${target}>"
COMMENT "Staging oneTBB runtime for ${target}"
VERBATIM)
endif ()
endfunction()
if (MSVC)
target_compile_options(Aethera_Kernel PRIVATE /utf-8)
endif ()
add_executable(Aethera_Kernel_exe "${CMAKE_CURRENT_LIST_DIR}/src/test/main.cpp")
target_link_libraries(Aethera_Kernel_exe PRIVATE Aethera_Kernel)
install(TARGETS Aethera_Kernel
EXPORT RenderiveTargets
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(DIRECTORY "${Aethera_Kernel_source_dir}/renderive"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.inl")
if (Aethera_BUILD_TESTS)
set(Aethera_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests")
append_glob_source(Aethera_Kernel_test_sources "${Aethera_Kernel_test_dir}")
# Concrete 2D/3D scene tests belong to their owning modules. The former
# header-only Scene_Context fixtures were removed with the Builder-based
# scene API, so Kernel's suite must remain independently linkable.
list(FILTER Aethera_Kernel_test_sources EXCLUDE REGEX
"(Real_Time_Data_Test|Renderable_Base_Test|Color_Cache_Test|Renderable_Test|Scene_Base_Test|Scene_Test|Dynamic_Renderable_Lifecycle_Test|Render_Plan_Execution_Test|Scene_Inheritance_Test|Scene_Memory_Resource_Test|Scene_State_Observer_Test|Scene2D_Context_Test|Scene2D_Render_Order_Test|Scene3D_Context_Test|Threading_Contract_Test)\\.cpp$")
foreach (Aethera_Kernel_test_source IN LISTS Aethera_Kernel_test_sources)
if (NOT Aethera_Kernel_test_source MATCHES "\\.(c|cc|cpp|cxx)$")
continue()
endif ()
file(RELATIVE_PATH Aethera_Kernel_test_name "${Aethera_Kernel_test_dir}" "${Aethera_Kernel_test_source}")
string(REGEX REPLACE "\\.[^.]+$" "" Aethera_Kernel_test_name "${Aethera_Kernel_test_name}")
string(MAKE_C_IDENTIFIER "${Aethera_Kernel_test_name}" Aethera_Kernel_test_name)
set(Aethera_Kernel_test_target "Aethera_Kernel_${Aethera_Kernel_test_name}")
add_executable("${Aethera_Kernel_test_target}" "${Aethera_Kernel_test_source}")
target_include_directories("${Aethera_Kernel_test_target}" PRIVATE
"${Aethera_Kernel_test_dir}"
"${CMAKE_CURRENT_LIST_DIR}/../render_2D"
"${CMAKE_CURRENT_LIST_DIR}/../render_3D"
)
target_link_libraries("${Aethera_Kernel_test_target}" PRIVATE Aethera_Kernel GTest::gtest_main)
Aethera_stage_kernel_runtime("${Aethera_Kernel_test_target}")
add_test(NAME "${Aethera_Kernel_test_target}" COMMAND "${Aethera_Kernel_test_target}")
set_tests_properties("${Aethera_Kernel_test_target}" PROPERTIES
LABELS "Aethera_Kernel"
ENVIRONMENT "Aethera_ERROR_MODE=exception")
list(APPEND Aethera_Kernel_test_targets "${Aethera_Kernel_test_target}")
endforeach ()
add_custom_target(Aethera_Kernel_check
COMMAND "${CMAKE_CTEST_COMMAND}" --test-dir "${CMAKE_BINARY_DIR}"
-C "$<CONFIG>" -L "^Aethera_Kernel$" --output-on-failure
DEPENDS ${Aethera_Kernel_test_targets}
USES_TERMINAL)
endif ()
View File
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
#include "base.hpp"
namespace aethera::_2D {
struct Color_Cache {};
struct Renderable : Impl<Renderable, Root, Tagged_Buffer<Color_Cache>> {
struct Prop : Prev_Prop {};
struct State : Prev_State {
bool operator==(const State&) const = default;
};
private:
using Call = void (*)(Renderable*);
Call paint_call{};
bool paint_dirty{true};
protected:
template <typename Object>
void bind_object_crtp() {
Prev_Object::template bind_object_crtp<Object>();
paint_call = [](Renderable* renderable) {
auto* object = static_cast<Object*>(renderable);
auto& private_data = static_cast<typename Object::Private&>(object->d);
private_data.template paint<Object>(object);
};
}
public:
void mark_paint_dirty() {
paint_dirty = true;
}
bool take_paint_dirty() {
if (!paint_dirty) return false;
paint_dirty = false;
return true;
}
void paint() {
paint_call(this);
}
struct Private : Prev_Private {
template <typename Object>
void after_prepare_data(Object* object) {
object->mark_paint_dirty();
}
template <typename Object>
void paint(Object*) {}
};
};
struct Layout_Tag {};
struct Extra_Tag {};
struct Secene : Impl<
Secene,
Root,
Rely_Type<Prepare_Data_Tag>,
Rely_Type<Layout_Tag>,
Rely_Type<Extra_Tag>
> {
struct Prop : Prev_Prop {};
struct State : Prev_State {};
private:
using Call = void (*)(const Secene*);
Call prepare_execute_call{};
Call paint_execute_call{};
std::pmr::vector<Renderable*> renderables;
std::pmr::unordered_map<Root*, Renderable*> renderable_nodes;
void bind_renderables() {
renderable_nodes.clear();
for (auto* object : renderables) renderable_nodes.emplace(static_cast<Root*>(object), object);
}
protected:
template <typename Object>
void bind_object_crtp() {
Prev_Object::template bind_object_crtp<Object>();
prepare_execute_call = [](const Secene* scene) {
auto* object = static_cast<const Object*>(scene);
(void)object->template render_rely<Prepare_Data_Tag>().execute_prepare();
};
paint_execute_call = [](const Secene* scene) {
auto* object = static_cast<const Object*>(scene);
(void)object->template render_rely<Prepare_Data_Tag>().for_each_dag(
[&](const auto& node) {
auto current = scene->renderable_nodes.find(node.object);
if (current == scene->renderable_nodes.end()) return;
auto* renderable = current->second;
if (!renderable->take_paint_dirty()) return;
renderable->paint();
}
);
};
}
public:
Secene() : renderables(memory_resource()),
renderable_nodes(memory_resource()) {}
void execute_prepare() const {
prepare_execute_call(this);
}
void execute_paint() const {
paint_execute_call(this);
}
template <typename Object>
struct Builder : Prev_Builder<Object> {
using Base = Prev_Builder<Object>;
using Base::Base;
template <typename Render_Object> requires std::derived_from<Render_Object, Renderable>
Builder& add_renderable(std::initializer_list<Render_Object*> list) {
for (auto* object : list) {
this->object->renderables.push_back(object);
this->template add_rely_node<Prepare_Data_Tag>(object);
}
return *this;
}
std::unique_ptr<Object> build() {
this->object->bind_renderables();
return std::move(this->object);
}
};
struct Private : Prev_Private {
template <typename Object>
void after_exchange(
Object* object,
typename Object::Prop*,
typename Object::State*,
const typename Object::Prop*,
const typename Object::State*) {
std::pmr::unordered_set<Root*> exchanged{object->memory_resource()};
object->for_each_render_rely(
[&](const Rely& rely) {
rely.for_each(
[&](const auto& node) {
if (exchanged.insert(node.object).second) node.object->exchange_object();
}
);
}
);
}
};
};
}
struct SEP_root : aethera::Root {};
struct SEP : aethera::Impl<SEP, SEP_root, aethera::Tagged_Buffer<std::vector<int>>> {
struct Prop : Prev_Prop {
int value{};
};
struct State : Prev_State {
int a{};
};
struct Private : Prev_Private {
template <typename Object>
void before_exchange(
Object*,
typename Object::Prop*,
typename Object::State* inside_state,
const typename Object::Prop*,
const typename Object::State*) {
++inside_state->a;
}
};
};
using SEP_2 = aethera::Attach_Object<SEP>;
struct SEP_Child : aethera::Impl<SEP_Child, SEP> {
struct Prop : Prev_Prop {
int child_value{};
};
struct State : Prev_State {
int b{};
};
struct Private : Prev_Private {
template <typename Object>
void before_exchange(
Object*,
typename Object::Prop*,
typename Object::State* inside_state,
const typename Object::Prop*,
const typename Object::State*) {
inside_state->b += 10;
}
};
};
using SEP_Child_2 = aethera::Attach_Object<SEP_Child>;
struct Data_Cache {
int value{};
};
struct Data_Object : aethera::Impl<Data_Object, SEP_root, aethera::Tagged_Buffer<Data_Cache>> {
struct Prop : Prev_Prop {};
struct State : Prev_State {
int value{};
int other{};
};
int id;
std::vector<int>* order;
Data_Object(int id, std::vector<int>* order) : id(id), order(order) {}
struct Private : Prev_Private {
int previous_value{};
template <typename Object, typename Owner, typename Member>
void before_state_set(Object*, Member Owner::* member, typename Object::State* state) {
if constexpr (std::same_as<decltype(member), decltype(&Data_Object::State::value)>) {
if (member == &Data_Object::State::value) previous_value = state->*member;
}
}
template <typename Object, typename Owner, typename Member>
void after_state_set(Object* object, Member Owner::* member, typename Object::State* state) {
if constexpr (std::same_as<decltype(member), decltype(&Data_Object::State::value)>) {
if (member == &Data_Object::State::value && state->*member != previous_value) object->mark_prepare_dirty();
}
}
template <typename Object>
void prepare_data(Object* object) {
object->order->push_back(object->id);
object->template buffer<Data_Cache>().value = object->id;
}
};
};
+390
View File
@@ -0,0 +1,390 @@
#include "../kernel/render_2D.hpp"
using Data_Object_2 = aethera::Attach_Object<Data_Object>;
struct Test_Cache {
int value{};
};
struct Test_Cache_Tag {};
struct Test_Renderable : aethera::Impl<
Test_Renderable,
aethera::_2D::Renderable,
aethera::Tagged_Buffer<Test_Cache>,
aethera::Tagged_Buffer<Test_Cache_Tag, Test_Cache>
> {
struct Prop : Prev_Prop {};
struct State : Prev_State {
int value{};
int other{};
bool operator==(const State&) const = default;
};
int id;
std::vector<int>* order;
Test_Renderable(int id, std::vector<int>* order) : id(id), order(order) {}
struct Private : Prev_Private {
int previous_value{};
template <typename Object, typename Owner, typename Member>
void before_state_set(Object*, Member Owner::* member, typename Object::State* state) {
if constexpr (std::same_as<decltype(member), decltype(&Test_Renderable::State::value)>) {
if (member == &Test_Renderable::State::value) previous_value = state->*member;
}
}
template <typename Object, typename Owner, typename Member>
void after_state_set(Object* object, Member Owner::* member, typename Object::State* state) {
if constexpr (std::same_as<decltype(member), decltype(&Test_Renderable::State::value)>) {
if (member == &Test_Renderable::State::value && state->*member != previous_value) object->mark_prepare_dirty();
}
}
template <typename Object>
void prepare_data(Object* object) {
object->order->push_back(object->id);
object->template buffer<Test_Cache>().value = object->id;
}
template <typename Object>
void paint(Object* object) {
object->order->push_back(object->id * 10);
}
};
};
using Test_Renderable_2 = aethera::Attach_Object<Test_Renderable>;
struct Pmr_Buffer_Tag {};
struct Pmr_Object : aethera::Impl<
Pmr_Object,
SEP_root,
aethera::Tagged_Buffer<Pmr_Buffer_Tag, std::pmr::vector<int>>
> {
struct Prop : Prev_Prop {};
struct State : Prev_State {};
struct Private : Prev_Private {};
};
using Pmr_Object_2 = aethera::Attach_Object<Pmr_Object>;
struct Counting_Resource : std::pmr::memory_resource {
std::pmr::memory_resource* upstream{std::pmr::new_delete_resource()};
std::size_t allocations{};
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
++allocations;
return upstream->allocate(bytes, alignment);
}
void do_deallocate(void* pointer, std::size_t bytes, std::size_t alignment) override {
upstream->deallocate(pointer, bytes, alignment);
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
};
static_assert(aethera::Object<SEP>);
static_assert(aethera::Object<SEP_Child>);
static_assert(aethera::Object<Data_Object>);
static_assert(aethera::Object<aethera::_2D::Renderable>);
static_assert(aethera::Object<aethera::_2D::Secene>);
static_assert(aethera::Object<Test_Renderable>);
static_assert(aethera::Object<Pmr_Object>);
int main() {
using Secene_Object = aethera::Attach_Object<aethera::_2D::Secene>;
using Prepare_Tag = aethera::Prepare_Data_Tag;
using Layout_Tag = aethera::_2D::Layout_Tag;
using Extra_Tag = aethera::_2D::Extra_Tag;
std::vector<int> order;
std::vector<int> direct_order;
Data_Object_2 direct_data(9, &direct_order);
direct_data.prepare_data();
direct_data.exchange_object();
if (direct_order != std::vector<int>({9})) return 112;
if (direct_data.render_buffer<Data_Cache>().value != 9) return 113;
Test_Renderable_2 direct_renderable(8, &direct_order);
direct_renderable.paint();
if (direct_order != std::vector<int>({9, 80})) return 114;
Secene_Object direct_scene;
direct_scene.execute_prepare();
direct_scene.execute_paint();
Counting_Resource buffer_resource;
Pmr_Object_2::Builder pmr_buffer_builder;
pmr_buffer_builder.set_pmr(buffer_resource);
std::pmr::vector<int> pmr_initial;
pmr_initial.assign({1, 2, 3});
pmr_buffer_builder.set<Pmr_Buffer_Tag>(pmr_initial);
auto pmr_buffer_object = pmr_buffer_builder.build();
if (pmr_buffer_object->pmr().resource() != &buffer_resource) return 108;
if (pmr_buffer_object->render_buffer<Pmr_Buffer_Tag>().get_allocator().resource() != pmr_buffer_object->memory_resource()) return 109;
pmr_buffer_object->buffer<Pmr_Buffer_Tag>().push_back(4);
if (buffer_resource.allocations == 0) return 110;
Counting_Resource scene_resource;
// Buffer Builder initialization writes cache/render together.
Data_Object_2::Builder data_builder(4, &order);
data_builder.set<Data_Cache>(Data_Cache{40});
auto data_object = data_builder.build();
Test_Renderable_2::Builder c_builder(3, &order);
c_builder.set<Test_Cache>(Test_Cache{30});
c_builder.set<Test_Cache_Tag>(Test_Cache{300});
auto c_object = c_builder.build();
Test_Renderable_2::Builder b_builder(2, &order);
b_builder.set<Test_Cache>(Test_Cache{20});
auto b_object = b_builder.build();
Test_Renderable_2::Builder a_builder(1, &order);
a_builder.set<Test_Cache>(Test_Cache{10});
auto a_object = a_builder.build();
if (data_object->render_buffer<Data_Cache>().value != 40) return 1;
if (c_object->render_buffer<Test_Cache>().value != 30) return 2;
if (c_object->render_buffer<Test_Cache_Tag>().value != 300) return 3;
if (b_object->render_buffer<Test_Cache>().value != 20) return 4;
if (a_object->render_buffer<Test_Cache>().value != 10) return 5;
// Builder initial rely editing is direct; check() validates all rely graphs together.
Secene_Object::Builder scene;
scene.set_pmr(scene_resource);
scene.add_renderable({a_object.get(), b_object.get(), c_object.get()});
scene.add_rely<
Prepare_Tag,
&Data_Object::State::value
>(c_object.get(), data_object.get());
scene.add_rely<
Prepare_Tag,
&Test_Renderable::State::value
>(b_object.get(), c_object.get());
scene.add_rely<
Prepare_Tag,
Test_Cache
>(a_object.get(), b_object.get());
scene.add_rely<
Prepare_Tag,
Test_Cache_Tag
>(a_object.get(), c_object.get());
scene.edit_rely<Layout_Tag, Extra_Tag>(
[&](auto& layout, auto& extra) {
layout.template add_rely<&Data_Object::State::other>(
c_object.get(),
data_object.get()
);
extra.template add_dirty_rely<Layout_Tag>(
b_object.get(),
c_object.get()
);
}
);
scene.add_rely<
Prepare_Tag,
Test_Cache
>(c_object.get(), a_object.get());
auto invalid_initial = scene.check();
if (invalid_initial) return 6;
if (invalid_initial.error() != aethera::Rely_Error::cycle) return 7;
scene.remove_rely<
Prepare_Tag,
Test_Cache
>(c_object.get(), a_object.get());
auto initial_check = scene.check();
if (!initial_check) return 8;
auto scene_object = scene.build();
if (scene_object->pmr().resource() != &scene_resource) return 103;
if (scene_object->render_rely<Prepare_Tag>().memory_resource() != scene_object->memory_resource()) return 104;
if (scene_resource.allocations == 0) return 105;
// Rely keeps node/edge introspection and DAG order in the same graph.
const auto& prepare_graph = scene_object->render_rely<Prepare_Tag>();
if (prepare_graph.size() != 4) return 9;
if (!prepare_graph.contains(data_object.get())) return 10;
if (!prepare_graph.contains_rely(c_object.get(), data_object.get())) return 11;
if (!prepare_graph.contains_rely(b_object.get(), c_object.get())) return 12;
if (!prepare_graph.contains_rely(a_object.get(), b_object.get())) return 13;
if (prepare_graph.find_edges(a_object.get(), c_object.get()).size() != 1) return 14;
auto initial_dag = prepare_graph.dag();
if (!initial_dag) return 15;
if (initial_dag->size() != 4) return 16;
if ((*initial_dag)[0]->object != data_object.get()) return 17;
if ((*initial_dag)[1]->object != c_object.get()) return 18;
if ((*initial_dag)[2]->object != b_object.get()) return 19;
if ((*initial_dag)[3]->object != a_object.get()) return 20;
std::vector<aethera::Root*> view_order;
bool view_query_ok{true};
auto view_walk = scene_object->for_each_rely_dag<Prepare_Tag>(
[&](const aethera::Rely::View& view, const aethera::Rely::Node& node) {
view_order.push_back(node.object);
if (node.object == c_object.get()) {
auto dependencies = view.get_rely(node.object);
view_query_ok = view_query_ok &&
dependencies.size() == 1 &&
dependencies[0]->object == data_object.get();
}
if (node.object == a_object.get()) {
auto dependencies = view.get_rely(node.object);
bool has_b{};
bool has_c{};
for (const auto* dependency : dependencies) {
has_b = has_b || dependency->object == b_object.get();
has_c = has_c || dependency->object == c_object.get();
}
auto dependents = view.find_rely_to(c_object.get());
bool has_b_dependent{};
bool has_a_dependent{};
for (const auto* dependent : dependents) {
has_b_dependent = has_b_dependent || dependent->object == b_object.get();
has_a_dependent = has_a_dependent || dependent->object == a_object.get();
}
view_query_ok = view_query_ok &&
dependencies.size() == 2 &&
has_b &&
has_c &&
dependents.size() == 2 &&
has_b_dependent &&
has_a_dependent &&
view.find_edges(a_object.get(), c_object.get()).size() == 1;
}
}
);
if (!view_walk) return 100;
if (!view_query_ok) return 101;
if (view_order != std::vector<aethera::Root*>({
data_object.get(),
c_object.get(),
b_object.get(),
a_object.get()
}))
return 102;
if (!data_object->need_prepare_data()) return 21;
if (!c_object->need_prepare_data()) return 22;
if (!b_object->need_prepare_data()) return 23;
if (!a_object->need_prepare_data()) return 24;
// Initial prepare follows DAG topology; paint only visits Renderable nodes.
scene_object->execute_prepare();
if (order != std::vector<int>({4, 3, 2, 1})) return 25;
if (data_object->need_prepare_data()) return 26;
if (c_object->need_prepare_data()) return 27;
if (b_object->need_prepare_data()) return 28;
if (a_object->need_prepare_data()) return 29;
order.clear();
scene_object->execute_paint();
if (order != std::vector<int>({30, 20, 10})) return 30;
order.clear();
// State member dirty -> prepare dirty -> Buffer dirty can continue infecting downstream nodes.
c_object->update_state<&Test_Renderable::State::value>(1);
if (!c_object->need_prepare_data()) return 31;
if (!b_object->need_prepare_data()) return 32;
if (a_object->need_prepare_data()) return 33;
scene_object->execute_prepare();
if (order != std::vector<int>({3, 2, 1})) return 34;
order.clear();
scene_object->execute_paint();
if (order != std::vector<int>({30, 20, 10})) return 35;
order.clear();
// Explicit Buffer Tag is an independent dirty source from the one-parameter Tagged_Buffer Tag.
c_object->buffer<Test_Cache_Tag>().value = 301;
if (!a_object->need_prepare_data()) return 36;
if (b_object->need_prepare_data()) return 37;
if (c_object->need_prepare_data()) return 38;
if (c_object->render_buffer<Test_Cache_Tag>().value != 300) return 39;
scene_object->execute_prepare();
if (order != std::vector<int>({1})) return 40;
order.clear();
scene_object->execute_paint();
if (order != std::vector<int>({10})) return 41;
order.clear();
// State source can target a custom dirty Tag, which can itself be another rely source.
data_object->update_state<&Data_Object::State::other>(1);
if (data_object->need_prepare_data()) return 42;
if (!c_object->dirty<Layout_Tag>()) return 43;
if (!b_object->dirty<Extra_Tag>()) return 44;
if (a_object->dirty<Extra_Tag>()) return 45;
if (!c_object->take_dirty<Layout_Tag>()) return 46;
if (!b_object->take_dirty<Extra_Tag>()) return 47;
// Runtime multi-rely edit is transactional: any invalid graph rolls every edited cache graph back.
auto invalid_runtime = scene_object->edit_rely<Prepare_Tag, Extra_Tag>(
[&](auto& prepare, auto& extra) {
prepare.template add_rely<Test_Cache>(
c_object.get(),
a_object.get()
);
extra.template add_rely<&Data_Object::State::other>(
a_object.get(),
data_object.get()
);
}
);
if (invalid_runtime) return 48;
if (invalid_runtime.error() != aethera::Rely_Error::cycle) return 49;
if (scene_object->rely<Prepare_Tag>().contains_rely(c_object.get(), a_object.get())) return 50;
if (scene_object->rely<Extra_Tag>().contains_rely(a_object.get(), data_object.get())) return 51;
// A valid multi-rely transaction commits all edited cache graphs together.
bool editor_view_ok{};
auto valid_runtime = scene_object->edit_rely<Prepare_Tag, Extra_Tag>(
[&](auto& prepare, auto& extra) {
editor_view_ok =
prepare.contains(a_object.get()) &&
prepare.contains_rely(a_object.get(), b_object.get()) &&
prepare.find_edges(a_object.get(), b_object.get()).size() == 1;
prepare.template remove_rely<Test_Cache>(
a_object.get(),
b_object.get()
);
extra.template add_rely<&Data_Object::State::other>(
a_object.get(),
data_object.get()
);
}
);
if (!editor_view_ok) return 52;
if (!valid_runtime) return 53;
if (scene_object->rely<Prepare_Tag>().contains_rely(a_object.get(), b_object.get())) return 54;
if (!scene_object->rely<Extra_Tag>().contains_rely(a_object.get(), data_object.get())) return 55;
if (!scene_object->render_rely<Prepare_Tag>().contains_rely(a_object.get(), b_object.get())) return 56;
if (scene_object->render_rely<Extra_Tag>().contains_rely(a_object.get(), data_object.get())) return 57;
// Cache graph edits do not affect active render graphs before exchange().
data_object->update_state<&Data_Object::State::other>(2);
if (!c_object->dirty<Layout_Tag>()) return 58;
if (!b_object->dirty<Extra_Tag>()) return 59;
if (a_object->dirty<Extra_Tag>()) return 60;
c_object->take_dirty<Layout_Tag>();
b_object->take_dirty<Extra_Tag>();
scene_object->exchange();
if (c_object->render_buffer<Test_Cache_Tag>().value != 301) return 61;
if (scene_object->render_rely<Prepare_Tag>().contains_rely(a_object.get(), b_object.get())) return 62;
if (!scene_object->render_rely<Extra_Tag>().contains_rely(a_object.get(), data_object.get())) return 63;
// After exchange(), the new render graph becomes the active dirty propagation graph.
data_object->update_state<&Data_Object::State::other>(3);
if (!c_object->dirty<Layout_Tag>()) return 64;
if (!b_object->dirty<Extra_Tag>()) return 65;
if (!a_object->dirty<Extra_Tag>()) return 66;
c_object->take_dirty<Layout_Tag>();
b_object->take_dirty<Extra_Tag>();
a_object->take_dirty<Extra_Tag>();
// Removed one-parameter Tagged_Buffer edge no longer propagates, while another Buffer Tag edge remains active.
b_object->buffer<Test_Cache>().value = 222;
if (a_object->need_prepare_data()) return 67;
c_object->buffer<Test_Cache_Tag>().value = 302;
if (!a_object->need_prepare_data()) return 68;
scene_object->execute_prepare();
if (order != std::vector<int>({1})) return 69;
order.clear();
scene_object->execute_paint();
if (order != std::vector<int>({10})) return 70;
order.clear();
// Inherited Prop/State/Private hooks and one-parameter Tagged_Buffer still compose through Impl layers.
SEP_Child_2::Builder sep_builder;
sep_builder.set(&SEP_Child::Prop::value, 123);
sep_builder.set(&SEP_Child::Prop::child_value, 456);
sep_builder.set<std::vector<int>>(std::vector<int>{1, 2, 3});
auto sep = sep_builder.build();
if (sep->d.render->value != 0) return 71;
if (sep->d.render->child_value != 0) return 72;
if (sep->render_buffer<std::vector<int>>() != std::vector<int>({1, 2, 3})) return 73;
sep->update_state<&SEP_Child::State::a>(1);
sep->update_state<&SEP_Child::State::b>(2);
sep->exchange(
[](const SEP_Child::State& state) {
if (state.a != 2) std::terminate();
if (state.b != 12) std::terminate();
}
);
if (sep->d.render->value != 123) return 74;
if (sep->d.render->child_value != 456) return 75;
Counting_Resource global_resource;
auto* old_default = aethera::Pmr::set_default_resource(global_resource);
{
Pmr_Object_2::Builder global_builder;
std::pmr::vector<int> global_initial;
global_initial.assign({5, 6, 7});
global_builder.set<Pmr_Buffer_Tag>(global_initial);
auto global_object = global_builder.build();
if (global_object->pmr().resource() != &global_resource) return 106;
if (global_object->render_buffer<Pmr_Buffer_Tag>().get_allocator().resource() != global_object->memory_resource()) return 107;
if (global_resource.allocations == 0) return 111;
}
aethera::Pmr::set_default_resource(old_default);
return 0;
}