From 4a48e62aea6708588f4f202afe17e7b110ecbf57 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 13 Aug 2026 11:25:38 +0800 Subject: [PATCH] =?UTF-8?q?builder=20=E5=92=8C=20=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E4=BF=AE=E6=94=B9=E9=9A=94=E7=A6=BB=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Kernel/renderive_threading_sync_draft.md | 790 ------------------ .../base/Real_Time_Data_Base.cpp | 31 +- .../base/Real_Time_Data_Base.hpp | 5 + .../renderable/Renderive_Id_Allocator.cpp | 24 + .../renderable/Renderive_Id_Allocator.hpp | 5 + .../renderable/base/Renderable_Base.cpp | 92 +- .../renderable/base/Renderable_Base.hpp | 19 +- .../renderable/base/Renderive_Owner.hpp | 64 ++ .../scene/base/Frame_Render_Snapshot.hpp | 3 +- .../src/renderive/scene/base/Scene_Base.cpp | 652 +++++---------- .../src/renderive/scene/base/Scene_Base.hpp | 120 +-- .../real_time_data/Real_Time_Data_Test.cpp | 116 ++- .../renderable/base/Renderable_Base_Test.cpp | 68 +- .../renderable/concept/Renderable_Test.cpp | 12 +- .../Dynamic_Renderable_Lifecycle_Test.cpp | 382 +++------ .../scene/Render_Plan_Execution_Test.cpp | 64 +- .../renderive/scene/Scene2D_Context_Test.cpp | 114 +-- .../scene/Scene2D_Render_Order_Test.cpp | 42 +- .../renderive/scene/Scene3D_Context_Test.cpp | 59 +- .../scene/Scene_Memory_Resource_Test.cpp | 48 +- .../renderive/scene/Scene_Test_Helpers.hpp | 24 + .../renderive/scene/base/Scene_Base_Test.cpp | 457 ++++------ .../renderive/state/State_Test_Types.hpp | 5 +- .../threading/Threading_Contract_Test.cpp | 28 +- render_2D/axis/Abs_Axis.cpp | 2 +- render_2D/axis/Abs_Axis.h | 2 +- render_2D/axis/Axis_Builder.h | 33 +- render_2D/axis/Axis_State_Strategy.hpp | 5 +- render_2D/axis/Frequency_Axis.cpp | 4 +- render_2D/axis/Frequency_Axis.h | 2 +- render_2D/axis/Numeric_Axis.cpp | 4 +- render_2D/axis/Numeric_Axis.h | 2 +- render_2D/axis/Time_Axis.cpp | 4 +- render_2D/axis/Time_Axis.h | 2 +- render_2D/plottable/Afterglow.cpp | 10 +- render_2D/plottable/Afterglow.h | 2 +- render_2D/plottable/Constellation_Diagram.cpp | 10 +- render_2D/plottable/Constellation_Diagram.h | 2 +- render_2D/plottable/Frequency_Trace.cpp | 10 +- render_2D/plottable/Frequency_Trace.h | 2 +- render_2D/plottable/Plottable.h | 5 +- .../plottable/Selection_Rectangle_Overlay.cpp | 10 +- .../plottable/Selection_Rectangle_Overlay.h | 2 +- render_2D/plottable/Spectrum.cpp | 10 +- render_2D/plottable/Spectrum.h | 2 +- render_2D/plottable/Sweep_Spectrum.cpp | 10 +- render_2D/plottable/Sweep_Spectrum.h | 2 +- render_2D/plottable/Waterfall.cpp | 10 +- render_2D/plottable/Waterfall.h | 2 +- render_2D/renderable/Renderable.cpp | 12 +- render_2D/renderable/Renderable.h | 3 +- render_2D/renderable/Renderable_Builder.h | 69 +- render_2D/scene/Scene.cpp | 4 +- render_2D/scene/Scene.h | 48 +- .../tests/render_2D_Integration_Tests.cpp | 445 ++++++---- web_server/app/Gallery_Plot_Session.cpp | 101 ++- web_server/app/Web_Plot_Session.cpp | 49 +- 57 files changed, 1512 insertions(+), 2592 deletions(-) delete mode 100644 Kernel/renderive_threading_sync_draft.md create mode 100644 Kernel/src/renderive/renderable/Renderive_Id_Allocator.cpp create mode 100644 Kernel/src/renderive/renderable/Renderive_Id_Allocator.hpp create mode 100644 Kernel/src/renderive/renderable/base/Renderive_Owner.hpp create mode 100644 Kernel/tests/renderive/scene/Scene_Test_Helpers.hpp diff --git a/Kernel/renderive_threading_sync_draft.md b/Kernel/renderive_threading_sync_draft.md deleted file mode 100644 index 8c2a5fe..0000000 --- a/Kernel/renderive_threading_sync_draft.md +++ /dev/null @@ -1,790 +0,0 @@ -# Renderive 线程同步模型 - -> 本文只描述线程同步、所有权、锁和并发约束,不提出新的架构改造方案。 -> -> 当前版本按已经回退到原有 Frame Strategy 语义后的设计理解编写。 -> -> 文中分为: -> -> - **已确认**:已经明确的设计语义。 -> - **待确认**:需要结合最终回退代码或由项目设计者确认的细节。 -> -> 本文不把 `viewport`、`configuration`、`render graph` 重新设计成额外的加锁对象,也不把 `Render_Lease` 改成短 claim/CAS。 - -## 1. 总体线程模型 - -Renderive 的线程模型分成四类执行活动: - -1. Plot / Scene 控制调用。 -2. Frame Strategy 的帧生产、渲染和发布。 -3. Scene Render DAG 的 Taskflow 并行执行。 -4. Real-Time Data(RTD)生产、维护和读取。 - -在 `render_2D` 层,一个 `Plot_Core` 持有一个具体的 `Scene2D_Context`。因此日常讨论 2D Plot 时,可以把“一个 Scene 的渲染执行域”理解成“一个 Plot 的渲染执行域”。 - -概念关系: - -```text -Plot_Core -└── Scene2D_Context - ├── Renderable A - │ └── local DAG - ├── Renderable B - │ └── local DAG - ├── Renderable C - │ └── local DAG - └── global Render DAG -``` - -多个 Renderable 的局部 DAG 组合成当前 Scene / Plot 的全局 Render DAG。 - -## 2. Scene / Plot 控制域 - -### 2.1 控制域负责的内容 - -控制域负责修改 Plot / Scene 的结构和配置,例如: - -```text -attach / detach Renderable -Renderable 配置修改 -Renderable DAG 结构修改请求 -viewport 修改 -layer / dependency 修改 -Frame Strategy 配置修改 -``` - -### 2.2 控制域和正在执行的 Render 的关系 - -**已确认:** - -控制操作不能破坏当前正在执行的 Render DAG 所依赖的数据和结构。 - -当前已有的 Scene 串行化 / render-idle 机制负责保证部分控制操作不会和不允许并发的 Scene 操作重叠。 - -这里的同步应该依赖 Scene 原有线程契约,而不是给每一个普通字段都额外套一把 mutex。 - -### 2.3 viewport - -**已确认:** - -`viewport` 本身不应该因为 Render DAG 读取而额外加锁。 - -Render DAG 内不应该通过一个带 mutex 的 `Plot_Core::viewport_size()` 热路径反复取 viewport。 - -需要区分: - -```text -控制侧修改 viewport -``` - -和: - -```text -当前 Frame 渲染使用 viewport -``` - -具体由现有 Scene / Frame 调用顺序保证一致性。 - -**待确认:** - -- `set_viewport_size()` 是否被明确限定只能由某一控制线程调用。 -- Frame 开始后是否允许其他线程修改 Plot viewport。 -- 当前 Frame 实际使用的 viewport 在哪一个现有对象中固定下来。 - -本文不擅自为 viewport 新增 mutex、atomic 或新的 publication 结构。 - -## 3. Renderable configuration - -**已确认:** - -`Renderable configuration` 不应该在 Render DAG 热路径为了读取配置而加锁。 - -配置修改属于控制行为。 - -渲染执行读取配置应该依靠原有 Scene 控制串行化和配置修改规则保证安全,而不是: - -```text -Prepare/Paint - ↓ -configuration() - ↓ -configuration_mutex_ -``` - -这种每次执行节点都重新同步的方式。 - -**待确认:** - -- 配置修改是否全部必须经过 `Scene_Base::lock_render_idle()` 或等价机制。 -- 是否存在允许控制线程在 Scene render 期间直接修改 configuration 的公开 API。 -- 如果存在,哪些字段允许并发修改,哪些字段必须等待 render idle。 - -在这些约束确认前,不应新增 per-Renderable configuration mutex 作为兜底。 - -## 4. Renderable graph - -### 4.1 Graph 的语义 - -每个 Renderable 拥有自己的局部 DAG。 - -局部 DAG 可以: - -```text -fan-out -fan-in -一对多 -多对一 -局部串行 -局部并行 -多入口 -多出口 -子图组合 -``` - -它不是树,只要求整体保持无环。 - -例如: - -```text - ┌─ B ─┐ -A ──────┼─ C ─┼─ F - └─ D ─┘ -``` - -也是合法局部 DAG。 - -### 4.2 Graph 同步 - -**已确认:** - -Render DAG 执行阶段读取当前 Renderable graph 不应该再通过 `render_graph_mutex_` 做节点级同步。 - -Graph 结构变化属于控制 / rebuild 行为。 - -当前 Frame 已经开始执行以后,不应该一边执行同一份 graph,一边原地修改这份 graph。 - -因此正确线程语义必须是: - -```text -旧 graph 正在被当前 Render 使用 - || -控制侧请求下一次 graph 结构变化 -``` - -而不是: - -```text -worker 正在遍历 graph - || -另一个线程原地修改同一个 graph 对象 -``` - -**待确认:** - -- `rebuild_render_graph()` 的真正执行线程。 -- dirty 标记和真正 rebuild 是否分离。 -- 当前实现是否已经通过不可变 graph 对象 / shared ownership 保证旧 graph 生命周期。 -- graph rebuild 是否只能发生在 Scene 的串行 publication / render-idle 区域。 - -本文不新增 graph mutex;最终文档应根据回退代码把上述四点写死。 - -## 5. Frame Strategy - -Frame Strategy 管理的是帧生命周期和帧 ownership,不负责 Render DAG 内部节点的数据同步。 - -不同 Strategy 可以拥有不同的 Frame 队列 / buffer / publish 规则,例如: - -```text -Manual -Flow -Low Latency -其他 Frame Strategy -``` - -但 Render DAG 的 Prepare / Paint / Composite 语义不因 Strategy 改变。 - -## 6. Render_Lease - -### 6.1 已确认语义 - -**已确认:原有 `Render_Lease` 生命周期互斥设计保留。** - -`Render_Lease` 不是仅用于“哪个 renderer 抢到 Frame”的短 claim。 - -它的锁可以覆盖: - -```text -acquire renderer - ↓ -scene.render() - ↓ -Taskflow 执行 - ↓ -wait_for_render() - ↓ -Render 完成 - ↓ -Render_Lease 结束 -``` - -如果 Strategy 的 `render_mutex_` 同时承担: - -```text -当前 Frame 正在 render - 与 -该 Frame publish / swap -``` - -之间的生命周期互斥,那么该锁必须覆盖完整 render 生命周期。 - -核心不变量: - -> **没有完成渲染的 Frame 不能被发布出去。** - -因此: - -```text -rendering Frame -``` - -和: - -```text -publish / swap same Frame -``` - -必须按当前 Strategy 设计保持互斥。 - -### 6.2 这把锁不属于 DAG 内部数据锁 - -即使 `Render_Lease` 外层持有 `render_mutex_`,也不意味着 DAG worker 内部靠这把锁串行执行。 - -正确关系是: - -```text -caller thread -└── 持有 Frame Strategy 生命周期锁 - └── 等待 Taskflow - ├── worker 0 - ├── worker 1 - ├── worker 2 - └── worker N -``` - -Taskflow workers 仍然根据 DAG dependency 并行执行。 - -因此必须区分: - -```text -Frame Strategy 生命周期锁 -``` - -和: - -```text -Render DAG 数据访问锁 -``` - -前者允许存在;后者应该尽量不存在于本来无需同步的数据上。 - -### 6.3 禁止擅自改成短 claim - -除非 Strategy 本身重新设计并明确提供等价的: - -```text -render 未完成 - ↓ -禁止 publish -``` - -保证,否则不能把完整 Render Lease: - -```text -lock → render → wait → unlock -``` - -擅自改成: - -```text -lock → claim → unlock → render -``` - -更不能因为想“无锁”就自行改成 CAS。 - -## 7. 三缓冲 / 多缓冲交换 - -### 7.1 Frame buffer ownership - -Frame Strategy 的 buffer ownership 必须保证: - -```text -producer 正在写的 buffer -renderer 正在渲染的 buffer -consumer / publisher 可见的 buffer -``` - -不会在不允许的阶段被同一参与者同时修改。 - -### 7.2 publish 条件 - -**已确认:** - -Frame 只有在完成完整 Render DAG 后才能进入可发布状态。 - -也就是至少: - -```text -Prepare -Paint -Composite -``` - -需要满足当前 Frame Strategy 所要求的完成条件。 - -Frame publish / swap 的同步属于 Frame Strategy 层,不应该被“清理 Render DAG 内部锁”的工作改动。 - -## 8. Scene Render DAG - -### 8.1 全局 DAG - -一个 Scene / Plot 的 Renderables 最终组合为一张全局 DAG。 - -Renderable 自己拥有局部子 DAG。 - -全局 DAG 不是严格顺序图,而是 dependency graph。 - -例如: - -```text -A.prepare ─────────→ A.paint ───────┐ - │ -B.prepare ─→ B.work ─→ B.paint ─────┼→ composite - │ -C.prepare ─────────→ C.paint ───────┘ -``` - -只要 dependency 满足,节点就可以执行。 - -### 8.2 没有全局 Prepare barrier - -**已确认:** - -不存在: - -```text -所有 Prepare 全部完成 - ↓ -所有 Paint 才允许开始 -``` - -这种全局 barrier。 - -例如如果 A.paint 只依赖 A.prepare,那么: - -```text -A.prepare 完成 -``` - -以后 A.paint 可以开始,即使: - -```text -B.prepare -``` - -还没有结束。 - -### 8.3 worker 之间的同步 - -Taskflow worker 之间主要通过 DAG dependency 建立先后关系。 - -原则: - -```text -没有 dependency edge - ↓ -允许并发 -``` - -而不是通过一个全局 Render mutex 把所有 Renderable 串起来。 - -## 9. Prepare - -Prepare 是 Renderable 的数据准备阶段。 - -它可以执行: - -```text -geometry 计算 -layout 计算 -clip 计算 -需要的状态读取 -对 Paint 所需数据的准备 -局部任务拆分 -``` - -Prepare 内部也可以是一张 DAG。 - -### 9.1 Prepare 的锁 - -不能简单规定“Prepare 完全不允许锁”。 - -是否需要锁取决于它访问的数据本身。 - -例如: - -```text -RTD -``` - -是动态共享实时数据,本身有同步要求,因此 Prepare 访问 RTD 时允许按 RTD 自身线程模型获取锁。 - -但是: - -```text -viewport -configuration -render graph -``` - -不能因为实现方便就在 Prepare 每次读取时新加一层无意义 mutex。 - -## 10. Paint - -Paint 根据当前 Renderable 已准备好的数据生成像素 / layer / paint output。 - -### 10.1 Paint 并发 - -不同 Renderable 的 Paint 是否能并发,由: - -```text -DAG dependency -paint output ownership -composite dependency -``` - -决定。 - -不能依赖一个全局 mutex 控制 Paint 顺序。 - -### 10.2 Paint 数据访问 - -Paint 不应该为了获取普通固定渲染输入反复进入: - -```text -Plot mutex -configuration mutex -render graph mutex -``` - -### 10.3 跨 Renderable 数据 - -**已确认的设计方向:** - -跨 Renderable 的依赖应该显式体现在 Render DAG / Prepare dependency 中,而不是 Paint 临时从其他 Renderable 拉动态数据。 - -最终允许 Paint 访问哪些 Scene / Renderable API,需要结合回退代码再列出白名单。 - -## 11. Composite - -Composite 负责按照明确的 layer / z-order / dependency 组合各 Paint 输出。 - -Paint 的实际线程完成顺序不能代替 layer order。 - -例如: - -```text -A.paint ─┐ -B.paint ─┼→ composite -C.paint ─┘ -``` - -即使 C.paint 最先结束,也不能因此改变最终覆盖顺序。 - -Composite 顺序是渲染语义,不是 mutex 获得顺序。 - -## 12. Real-Time Data(RTD) - -RTD = Real-Time Data。 - -RTD 是持续被生产线程更新、同时可能被渲染线程读取的动态共享数据。 - -### 12.1 RTD 与普通配置不同 - -RTD 不能套用: - -```text -viewport/config/graph 不加锁 -``` - -这一规则。 - -RTD 本身就是并发共享动态数据,因此它需要自己的同步设计。 - -**已确认:RTD 原有合理锁保留。** - -### 12.2 RTD 可能包含的并发活动 - -例如: - -```text -producer append data -producer update state -discard old data -observer notification -Prepare read RTD -``` - -这些活动之间的同步必须由 RTD 自己保证。 - -不能为了追求“Render DAG 无锁”而直接删掉 RTD mutex。 - -### 12.3 RTD 的具体锁序 - -**待确认:** - -需要根据回退后的实际代码逐项写清: - -```text -RTD mutation mutex -RTD data/state mutex -observer callback 前后锁释放位置 -discard 与 reader 的关系 -render read 是否持锁 -``` - -尤其必须说明 callback 是否在锁外执行。 - -## 13. State Strategy - -普通 Renderable State 和 RTD 不是一个概念。 - -Renderable State Strategy 负责: - -```text -update/cache/render state -publish/swap -revision -``` - -Double State / Triple State 的具体同步规则必须按当前 Strategy 实现描述。 - -### 13.1 已确认原则 - -State 的 publish / swap 本身允许使用锁。 - -不能把: - -```text -发布动作使用 mutex -``` - -误判为: - -```text -Render DAG worker 因读取普通数据而加锁 -``` - -这两种锁语义完全不同。 - -### 13.2 待确认 - -需要根据回退版本最终写明: - -- render state getter 是否直接读 render state。 -- publish/swap 是否加锁。 -- cache/update state 谁能写。 -- observer 是锁内还是锁外调用。 -- Triple State 的 published/render/cache 三者什么时候交换。 - -## 14. Observer / callback - -Callback 是线程同步中最容易出现重入问题的地方。 - -原则: - -> 不应该在持有可能被 callback 重入的内部数据锁时执行未知用户 callback。 - -典型安全模式: - -```text -lock -修改内部状态 -准备 Observation -unlock -调用 observer -``` - -而不是: - -```text -lock -修改内部状态 -调用 observer -unlock -``` - -### 14.1 待确认 - -需要结合实际代码列出: - -```text -State observer -RTD observer -Frame observer -Renderable callback -``` - -分别在哪个线程执行,以及调用时持有哪些锁。 - -## 15. Scene worker 与 Taskflow worker - -**待结合回退代码最终确认线程名称和数量,但概念上需要明确区分两层:** - -### 15.1 Scene render 生命周期线程 - -负责一帧的高层流程,例如: - -```text -开始一帧 -组织/提交 Render Plan -启动 Taskflow -等待 Taskflow -完成 Frame -``` - -### 15.2 Taskflow executor workers - -真正执行 DAG node: - -```text -Prepare node -Paint node -Composite node -``` - -因此某个 mutex 被 Scene caller / Scene worker 持有,不等于 Taskflow nodes 被这把 mutex 串行化。 - -分析锁竞争时必须说明: - -```text -谁拿锁 -谁等待锁 -锁是否进入 Taskflow node -``` - -不能只看到“整帧期间存在一把锁”就判断它影响 DAG 并行度。 - -## 16. 当前锁分类 - -锁应按用途分类,而不是简单分为“好锁/坏锁”。 - -| 类型 | 典型用途 | 是否允许 | -|---|---|---| -| Frame Strategy 生命周期锁 | render 与 publish/swap 互斥 | 允许 | -| Frame/Buffer ownership 锁 | buffer 生命周期、队列、交换 | 允许 | -| RTD 数据锁 | 动态共享实时数据 | 允许 | -| State publish/swap 锁 | 状态发布和交换 | 允许 | -| Scene topology/control 锁 | attach/detach/控制操作 | 允许 | -| Observer 内部管理锁 | observer 注册/状态维护 | 按实际实现 | -| viewport 读取锁 | DAG 内读取 viewport | 不应该有 | -| configuration 读取锁 | DAG 内读取普通配置 | 不应该有 | -| render graph 读取锁 | DAG 内读取当前 graph | 不应该有 | -| Paint 全局锁 | 串行化所有 Paint | 不应该有 | -| Prepare 全局锁 | 串行化所有 Prepare | 不应该有 | - -## 17. “内部渲染无锁”的准确含义 - -项目里的“清理内部渲染锁”不能理解成: - -> 整个 Renderive 在 Frame render 期间任何地方都不能存在 mutex。 - -正确含义应该是: - -> **Render DAG 不因为读取本来已经由现有线程契约保证稳定的普通渲染数据而重复获取 mutex。** - -明确包括: - -```text -viewport -configuration -render graph -``` - -不应该靠新的 mutex 保护 DAG 内读取。 - -但不包括: - -```text -Frame Strategy 生命周期互斥 -Frame publish/swap -RTD 动态共享数据同步 -State publish/swap -Scene 控制操作 -``` - -这些同步有独立语义,不能因为“内部渲染无锁”被删除。 - -## 18. 当前禁止擅自引入的改动 - -在没有重新确认线程模型之前,不进行以下改动: - -```text -不把 Render_Lease 擅自改成短 claim -不把 Frame Strategy 生命周期锁改成 CAS -不重新设计三缓冲 ownership -不删除 RTD 正常数据锁 -不为 viewport 新增 mutex/atomic -不为 configuration 新增 mutex 兜底 -不为 render graph 新增 worker-side mutex -不引入新的“统一快照层”改变原有函数语义 -``` - -后续任何锁优化必须先证明: - -```text -这把锁保护的具体对象是什么 -谁会并发访问 -现有更高层同步是否已经保证安全 -删除后是否改变旧函数语义 -``` - -## 19. 需要项目设计者确认的线程契约 - -下面这些点需要在最终版本中由代码或设计者明确,确认后再更新本文,不应由实现者自行猜测: - -1. `viewport` 修改允许在哪些线程发生。 -2. Frame render 期间是否允许修改 viewport。 -3. Renderable configuration 的所有修改入口及其线程约束。 -4. `rebuild_render_graph()` 的调用线程和真正 rebuild 时机。 -5. 当前 graph 的生命周期如何保证。 -6. Scene 高层 render 生命周期实际在哪个线程执行。 -7. 三种 Frame Strategy 的 `Render_Lease` 分别保护哪些状态。 -8. 三种 Strategy 的 publish/swap 与 render 的锁顺序。 -9. RTD 每把锁分别保护什么。 -10. RTD observer 是否一定在内部数据锁释放以后调用。 -11. State Strategy 的 render getter 是否无锁。 -12. Double/Triple State publish/swap 的精确锁范围。 -13. Scene control lock 与 Frame Strategy lock 是否允许嵌套。 -14. Frame observer/callback 在哪个线程调用。 -15. 是否存在任何公开 API 允许在 Render DAG 执行期间修改当前 Renderable 的 graph/config。 - -## 20. 文档最终需要达到的目标 - -最终 `threading.md` 必须让实现者只看本文就能回答: - -```text -某个字段谁写? -某个字段谁读? -允许哪些线程同时访问? -需要哪把锁? -为什么需要? -锁覆盖多大范围? -callback 时是否还持锁? -Frame Strategy 为什么可以整帧持锁? -RTD 为什么需要锁? -viewport/config/graph 为什么不需要额外读锁? -两个 Renderable 为什么可以并行? -什么时候必须通过 DAG edge 而不是 mutex 建立顺序? -``` - -只有这些问题全部能从文档直接得到确定答案以后,才根据文档做下一轮锁清理。 diff --git a/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.cpp b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.cpp index e7dfe7d..8b930f5 100644 --- a/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.cpp +++ b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.cpp @@ -1,21 +1,38 @@ #include "Real_Time_Data_Base.hpp" #include "renderive/renderable/base/Renderable_Base.hpp" +#include #include Real_Time_Data_Binding::Real_Time_Data_Binding(Renderable_Base& renderable, Real_Time_Data_Base& data) - : renderable_(&renderable), data_(&data) { - renderable_->register_real_time_data(*data_); -} + : renderable_(&renderable), data_(&data) {} Real_Time_Data_Binding::Real_Time_Data_Binding(Real_Time_Data_Binding&& other) noexcept : renderable_(std::exchange(other.renderable_, nullptr)), data_(std::exchange(other.data_, nullptr)) {} Real_Time_Data_Binding::~Real_Time_Data_Binding() { - if (renderable_) { - renderable_->unregister_real_time_data(*data_); - } + if (!renderable_) + return; + renderable_->unregister_real_time_data(*data_); + data_->unbind_renderable(*renderable_); } - Real_Time_Data_Binding Real_Time_Data_Base::bind_renderable(Renderable_Base& renderable) { + { + std::lock_guard lock(binding_mutex_); + if (bound_renderable_) + throw std::logic_error("real-time data is already bound to a renderable"); + bound_renderable_ = &renderable; + } + try { + renderable.register_real_time_data(*this); + } catch (...) { + std::lock_guard lock(binding_mutex_); + bound_renderable_ = nullptr; + throw; + } return {renderable, *this}; } +void Real_Time_Data_Base::unbind_renderable(Renderable_Base& renderable) noexcept { + std::lock_guard lock(binding_mutex_); + if (bound_renderable_ == &renderable) + bound_renderable_ = nullptr; +} diff --git a/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp index dc6727b..56cbccb 100644 --- a/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp +++ b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include "renderive/real_time_data/Observation.hpp" class Renderable_Base; class Real_Time_Data_Base; @@ -26,6 +27,10 @@ public: protected: [[nodiscard]] Real_Time_Data_Binding bind_renderable(Renderable_Base& renderable); private: + friend class Real_Time_Data_Binding; friend class Renderable_Base; + void unbind_renderable(Renderable_Base& renderable) noexcept; virtual void publish_render_state() = 0; + std::mutex binding_mutex_; + Renderable_Base* bound_renderable_{}; }; diff --git a/Kernel/src/renderive/renderable/Renderive_Id_Allocator.cpp b/Kernel/src/renderive/renderable/Renderive_Id_Allocator.cpp new file mode 100644 index 0000000..2f78017 --- /dev/null +++ b/Kernel/src/renderive/renderable/Renderive_Id_Allocator.cpp @@ -0,0 +1,24 @@ +#include "Renderive_Id_Allocator.hpp" +#include +#include +#include +namespace { +template +Id allocate_id(std::atomic& next, const char* message) { + Id value = next.load(std::memory_order_relaxed); + for (;;) { + if (value == 0 || value == std::numeric_limits::max()) + throw std::overflow_error(message); + if (next.compare_exchange_weak(value, value + 1, std::memory_order_relaxed)) + return value; + } +} +std::atomic next_renderable_id{1}; +std::atomic next_node_id{1}; +} +Renderable_Id allocate_renderive_renderable_id() { + return allocate_id(next_renderable_id, "renderable id exhausted"); +} +Render_Node_Id allocate_renderive_node_id() { + return allocate_id(next_node_id, "render node id exhausted"); +} diff --git a/Kernel/src/renderive/renderable/Renderive_Id_Allocator.hpp b/Kernel/src/renderive/renderable/Renderive_Id_Allocator.hpp new file mode 100644 index 0000000..4e618b1 --- /dev/null +++ b/Kernel/src/renderive/renderable/Renderive_Id_Allocator.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "Renderable_Id.hpp" +#include "renderive/render_graph/Render_Plan.hpp" +Renderable_Id allocate_renderive_renderable_id(); +Render_Node_Id allocate_renderive_node_id(); diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp index 4fe6314..daae21d 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp @@ -3,18 +3,16 @@ #include #include #include "renderive/real_time_data/base/Real_Time_Data_Base.hpp" +#include "renderive/renderable/Renderive_Id_Allocator.hpp" #include "renderive/scene/base/Scene_Base.hpp" namespace { void advance_revision(std::atomic& revision) noexcept { std::uint64_t value = revision.load(std::memory_order_relaxed); - while (value != std::numeric_limits::max() && - !revision.compare_exchange_weak(value, value + 1, - std::memory_order_release, - std::memory_order_relaxed)) {} + while (value != std::numeric_limits::max() && !revision.compare_exchange_weak(value, value + 1, std::memory_order_release, std::memory_order_relaxed)) {} } } -Renderable_Base::Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration) - : memory_domain_(scene.memory_domain_), real_time_data_state_(std::allocate_shared(Scene_Memory_Allocator{memory_domain_}, scene.scene_lifetime_)), discard_stale_frame_on_latest_data_update(real_time_data_state_->discard_stale_frame_on_latest_data_update), renderable_id_(scene.allocate_renderable_id()), composite_node_id_(scene.allocate_node_id()), configuration_(configuration) {} +Renderable_Base::Renderable_Base(Renderable_Configuration configuration, std::pmr::memory_resource& memory_resource) + : memory_resource_(&memory_resource), real_time_data_state_(std::make_shared()), discard_stale_frame_on_latest_data_update(real_time_data_state_->discard_stale_frame_on_latest_data_update), renderable_id_(allocate_renderive_renderable_id()), composite_node_id_(allocate_renderive_node_id()), configuration_(configuration) {} Renderable_Base::~Renderable_Base() { real_time_data_state_->attached.store(false, std::memory_order_release); real_time_data_state_->renderable_alive.store(false, std::memory_order_release); @@ -39,17 +37,23 @@ std::uint64_t Renderable_Base::painted_revision() const noexcept { } bool Renderable_Base::prepare_cache_valid() const noexcept { const std::uint64_t revision = prepare_revision(); - return revision != std::numeric_limits::max() && - configuration().cache_enabled && prepared_revision() == revision; + return revision != std::numeric_limits::max() && configuration().cache_enabled && prepared_revision() == revision; } bool Renderable_Base::paint_cache_valid() const noexcept { const std::uint64_t revision = paint_revision(); - return revision != std::numeric_limits::max() && - configuration().cache_enabled && prepare_cache_valid() && - painted_revision() == revision && - painted_prepare_revision_.load(std::memory_order_acquire) == prepared_revision(); + return revision != std::numeric_limits::max() && configuration().cache_enabled && prepare_cache_valid() && painted_revision() == revision && painted_prepare_revision_.load(std::memory_order_acquire) == prepared_revision(); +} +void Renderable_Base::bind_scene(Scene_Base& scene) { + if (!real_time_data_state_->scene_lifetime) + real_time_data_state_->scene_lifetime = scene.scene_lifetime_; + else if (real_time_data_state_->scene_lifetime.get() != scene.scene_lifetime_.get()) + throw std::invalid_argument("renderable belongs to another scene"); } void Renderable_Base::rebuild_render_graph() { + if (!attached()) { + reset_render_graph(); + return; + } scene().request_render_graph_rebuild(*this); } void Renderable_Base::reset_render_graph() noexcept { @@ -63,15 +67,12 @@ std::shared_ptr Renderable_Base::render_graph() { std::lock_guard lock(render_graph_mutex_); if (!render_graph_) { std::unordered_map next_identities; - Renderable_Graph_Builder builder(renderable_id_, [this, &next_identities]( - std::string_view key) { + Renderable_Graph_Builder builder(renderable_id_, [this, &next_identities](std::string_view key) { const std::string logical_key(key); if (next_identities.contains(logical_key)) throw std::invalid_argument("duplicate render node logical key"); const auto current = node_identities_.find(logical_key); - const Render_Node_Id node_id = current == node_identities_.end() - ? scene().allocate_node_id() - : current->second; + const Render_Node_Id node_id = current == node_identities_.end() ? allocate_renderive_node_id() : current->second; next_identities.emplace(logical_key, node_id); return node_id; }); @@ -87,19 +88,40 @@ Renderable_Id Renderable_Base::renderable_id() const noexcept { return renderable_id_; } std::pmr::memory_resource& Renderable_Base::memory_resource() const noexcept { - return memory_domain_->resource(); + return *memory_resource_; } Scene_Base& Renderable_Base::scene() const { - auto lease = real_time_data_state_->scene_lifetime->acquire(); - if (!lease) { + const auto lifetime = real_time_data_state_->scene_lifetime; + if (!lifetime) + throw std::logic_error("renderable is not bound to a scene"); + auto lease = lifetime->acquire(); + if (!lease) throw std::logic_error("renderable scene is no longer alive"); - } return lease.scene(); } +bool Renderable_Base::has_scene() const noexcept { + return static_cast(real_time_data_state_->scene_lifetime); +} +bool Renderable_Base::attached() const noexcept { + return real_time_data_state_->attached.load(std::memory_order_acquire); +} Renderable_Configuration Renderable_Base::configuration() const noexcept { std::lock_guard lock(configuration_mutex_); return configuration_; } +void Renderable_Base::set_configuration(Renderable_Configuration configuration) { + bool changed{}; + { + std::lock_guard lock(configuration_mutex_); + changed = configuration_ != configuration; + if (changed) + configuration_ = configuration; + } + if (!changed) + return; + invalidate_prepare(); + notify_scene_model_dirty(); +} bool Renderable_Base::is_visible() const noexcept { std::lock_guard lock(configuration_mutex_); return visible_; @@ -112,13 +134,22 @@ void Renderable_Base::set_visible(bool visible) { visible_ = visible; invalidate_prepare(); } - scene().notify_model_dirty(); + notify_scene_model_dirty(); +} +void Renderable_Base::notify_scene_model_dirty() noexcept { + if (!attached()) + return; + const auto lifetime = real_time_data_state_->scene_lifetime; + if (!lifetime) + return; + auto lease = lifetime->acquire(); + if (lease) + lease.scene().notify_model_dirty(); } void Renderable_Base::build_prepare_graph(Renderable_Graph_Builder& builder) { - builder.emplace("prepare", "Prepare", - [this](const Prepare_Render_Context& context) { - prepare(context); - }); + builder.emplace("prepare", "Prepare", [this](const Prepare_Render_Context& context) { + prepare(context); + }); } void Renderable_Base::build_paint_graph(Renderable_Graph_Builder&) {} void Renderable_Base::prepare(const Prepare_Render_Context&) {} @@ -131,16 +162,14 @@ Real_Time_Data_Binding Renderable_Base::bind_real_time_data(Real_Time_Data_Base& void Renderable_Base::mark_prepared(std::uint64_t revision) noexcept { prepared_revision_.store(revision, std::memory_order_release); } -void Renderable_Base::mark_painted(std::uint64_t paint_revision, - std::uint64_t prepare_revision) noexcept { +void Renderable_Base::mark_painted(std::uint64_t paint_revision, std::uint64_t prepare_revision) noexcept { painted_prepare_revision_.store(prepare_revision, std::memory_order_release); painted_revision_.store(paint_revision, std::memory_order_release); } void Renderable_Base::register_real_time_data(Real_Time_Data_Base& data) { std::lock_guard lock(real_time_data_mutex_); - if (std::find(real_time_data_.begin(), real_time_data_.end(), &data) == real_time_data_.end()) { + if (std::find(real_time_data_.begin(), real_time_data_.end(), &data) == real_time_data_.end()) real_time_data_.push_back(&data); - } } void Renderable_Base::unregister_real_time_data(Real_Time_Data_Base& data) noexcept { std::lock_guard lock(real_time_data_mutex_); @@ -148,7 +177,6 @@ void Renderable_Base::unregister_real_time_data(Real_Time_Data_Base& data) noexc } void Renderable_Base::publish_real_time_data() { std::lock_guard lock(real_time_data_mutex_); - for (Real_Time_Data_Base* data : real_time_data_) { + for (Real_Time_Data_Base* data : real_time_data_) data->publish_render_state(); - } } diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp index 47464e0..5109f20 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp @@ -8,29 +8,27 @@ #include #include #include -#include "renderive/base/memory/Memory_Resource.hpp" #include "renderive/renderable/Renderable_Configuration.hpp" -#include "renderive/renderable/Renderable_Id.hpp" #include "renderive/renderable/Renderable_Graph_Builder.hpp" +#include "renderive/renderable/Renderable_Id.hpp" #include "renderive/scene/base/Scene_Lifetime.hpp" #include "renderive/state/Render_State_View.hpp" class Frame_Strategy_Real_Time_Data_Observer; class Real_Time_Data_Base; class Real_Time_Data_Binding; class Scene_Base; -class Renderable_Base : public std::enable_shared_from_this { +class Renderable_Base { private: struct Real_Time_Data_State { - explicit Real_Time_Data_State(std::shared_ptr scene_lifetime) : scene_lifetime(std::move(scene_lifetime)) {} std::shared_ptr scene_lifetime; std::atomic renderable_alive{true}; std::atomic attached{}; std::atomic discard_stale_frame_on_latest_data_update{}; }; - std::shared_ptr memory_domain_; + std::pmr::memory_resource* memory_resource_; std::shared_ptr real_time_data_state_; public: - explicit Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration = {}); + explicit Renderable_Base(Renderable_Configuration configuration = {}, std::pmr::memory_resource& memory_resource = *std::pmr::get_default_resource()); virtual ~Renderable_Base(); void invalidate_prepare() noexcept; void invalidate_paint() noexcept; @@ -43,7 +41,10 @@ public: [[nodiscard]] Renderable_Id renderable_id() const noexcept; std::pmr::memory_resource& memory_resource() const noexcept; Scene_Base& scene() const; + [[nodiscard]] bool has_scene() const noexcept; + [[nodiscard]] bool attached() const noexcept; Renderable_Configuration configuration() const noexcept; + void set_configuration(Renderable_Configuration configuration); [[nodiscard]] bool is_visible() const noexcept; void set_visible(bool visible); std::atomic& discard_stale_frame_on_latest_data_update; @@ -55,13 +56,15 @@ protected: [[nodiscard]] std::shared_ptr render_graph(); [[nodiscard]] static Render_State_View render_state_view() noexcept; [[nodiscard]] Real_Time_Data_Binding bind_real_time_data(Real_Time_Data_Base& data); + void notify_scene_model_dirty() noexcept; private: friend class Frame_Strategy_Real_Time_Data_Observer; + friend class Real_Time_Data_Base; friend class Real_Time_Data_Binding; friend class Scene_Base; + void bind_scene(Scene_Base& scene); void mark_prepared(std::uint64_t revision) noexcept; - void mark_painted(std::uint64_t paint_revision, - std::uint64_t prepare_revision) noexcept; + void mark_painted(std::uint64_t paint_revision, std::uint64_t prepare_revision) noexcept; void register_real_time_data(Real_Time_Data_Base& data); void unregister_real_time_data(Real_Time_Data_Base& data) noexcept; void publish_real_time_data(); diff --git a/Kernel/src/renderive/renderable/base/Renderive_Owner.hpp b/Kernel/src/renderive/renderable/base/Renderive_Owner.hpp new file mode 100644 index 0000000..dc65bd6 --- /dev/null +++ b/Kernel/src/renderive/renderable/base/Renderive_Owner.hpp @@ -0,0 +1,64 @@ +#pragma once +#include +#include +#include +#include +template +class renderive_Owner { +public: + using element_type = Value; + renderive_Owner() = default; + renderive_Owner(std::nullptr_t) noexcept {} + explicit renderive_Owner(std::shared_ptr owner) noexcept : owner_(std::move(owner)) {} + template + requires std::convertible_to + renderive_Owner(const renderive_Owner& other) noexcept : owner_(other.owner_) {} + template + requires std::convertible_to + renderive_Owner(renderive_Owner&& other) noexcept : owner_(std::move(other.owner_)) {} + template + static renderive_Owner make(Args&&... args) { + return renderive_Owner(std::make_shared(std::forward(args)...)); + } + [[nodiscard]] Value* get() const noexcept { + return owner_.get(); + } + [[nodiscard]] std::shared_ptr share() const noexcept { + return owner_; + } + Value& operator*() const noexcept { + return *owner_; + } + Value* operator->() const noexcept { + return owner_.get(); + } + explicit operator bool() const noexcept { + return static_cast(owner_); + } + void reset() noexcept { + owner_.reset(); + } + friend bool operator==(const renderive_Owner&, const renderive_Owner&) = default; + friend bool operator==(const renderive_Owner& owner, std::nullptr_t) noexcept { + return !owner; + } + friend bool operator==(std::nullptr_t, const renderive_Owner& owner) noexcept { + return !owner; + } +private: + template + friend class renderive_Owner; + std::shared_ptr owner_; +}; +template +renderive_Owner renderive_dynamic_owner_cast(const renderive_Owner& source) noexcept { + return renderive_Owner(std::dynamic_pointer_cast(source.share())); +} +template +renderive_Owner renderive_static_owner_cast(const renderive_Owner& source) noexcept { + return renderive_Owner(std::static_pointer_cast(source.share())); +} +template +renderive_Owner renderive_const_owner_cast(const renderive_Owner& source) noexcept { + return renderive_Owner(std::const_pointer_cast(source.share())); +} diff --git a/Kernel/src/renderive/scene/base/Frame_Render_Snapshot.hpp b/Kernel/src/renderive/scene/base/Frame_Render_Snapshot.hpp index f02bfaf..c13b300 100644 --- a/Kernel/src/renderive/scene/base/Frame_Render_Snapshot.hpp +++ b/Kernel/src/renderive/scene/base/Frame_Render_Snapshot.hpp @@ -6,6 +6,7 @@ #include "renderive/capture/Capture_Types.hpp" #include "renderive/renderable/Renderable_Configuration.hpp" #include "renderive/renderable/Renderable_Id.hpp" +#include "renderive/renderable/base/Renderive_Owner.hpp" #include "renderive/state/Render_State_View.hpp" #include "Frame_Viewport.hpp" @@ -31,7 +32,7 @@ public: private: friend class Scene_Base; - std::shared_ptr owner_; + renderive_Owner owner_; std::shared_ptr paint_buffer_; }; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index fb0812b..fce7b90 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -1,6 +1,8 @@ #include "Scene_Base.hpp" #include +#include +#include #include #include #include @@ -14,6 +16,7 @@ #include #include "renderive/renderable/Render_Frame_Completion.hpp" +#include "renderive/renderable/Renderive_Id_Allocator.hpp" #include "renderive/renderable/color/Color_Cache.hpp" #include "renderive/state/base/State_Strategy_Base.hpp" @@ -21,19 +24,6 @@ static_assert(TF_VERSION == 400100, "Renderive requires Taskflow 4.1.0"); namespace { -template -Id allocate_id(std::atomic& next, const char* exhausted_message) { - Id value = next.load(std::memory_order_relaxed); - for (;;) { - if (value == 0 || value == std::numeric_limits::max()) - throw std::overflow_error(exhausted_message); - if (next.compare_exchange_weak(value, value + 1, - std::memory_order_relaxed, - std::memory_order_relaxed)) - return value; - } -} - bool contains_id(const std::vector& values, Renderable_Id id) { return std::find(values.begin(), values.end(), id) != values.end(); } @@ -45,217 +35,96 @@ bool contains_id(const std::pmr::vector& values, } // namespace -void Scene_Base::Id_Topology::erase_id( - std::pmr::vector& values, Renderable_Id id) noexcept { +void Scene_Base::Id_Topology::erase_id(std::pmr::vector& values, Renderable_Id id) noexcept { std::erase(values, id); } - -void Scene_Base::Id_Topology::validate_parent(Renderable_Id child, - Renderable_Id parent) const { - if (!nodes_.contains(child) || !nodes_.contains(parent)) +void Scene_Base::Id_Topology::validate_endpoint(Renderable_Id id) const { + if (!nodes_.contains(id)) throw std::invalid_argument("topology endpoint is not attached"); - if (child == parent || reaches(child, parent)) - throw std::invalid_argument("topology relationship would create a cycle"); } - -bool Scene_Base::Id_Topology::reaches(Renderable_Id from, - Renderable_Id target) const { - std::vector stack{from}; - std::unordered_set visited; - visited.reserve(nodes_.size()); - while (!stack.empty()) { - const Renderable_Id current = stack.back(); - stack.pop_back(); - if (current == target) - return true; - if (!visited.insert(current).second) - continue; - const auto iterator = nodes_.find(current); - if (iterator == nodes_.end()) - continue; - stack.insert(stack.end(), iterator->second.children.begin(), - iterator->second.children.end()); - } - return false; -} - -void Scene_Base::Id_Topology::attach( - Renderable_Id id, const std::vector& parents, - const std::vector& children) { +void Scene_Base::Id_Topology::attach(Renderable_Id id) { if (id == 0 || nodes_.contains(id)) throw std::invalid_argument("topology node is already attached"); - std::unordered_set unique_parents; - unique_parents.reserve(parents.size()); - for (const Renderable_Id parent : parents) { - if (parent == id || !nodes_.contains(parent)) - throw std::invalid_argument("topology parent is not attached"); - if (!unique_parents.insert(parent).second) - throw std::invalid_argument("duplicate topology parent"); - } - std::unordered_set unique_children; - unique_children.reserve(children.size()); - for (const Renderable_Id child : children) { - if (child == id || !nodes_.contains(child)) - throw std::invalid_argument("topology child is not attached"); - if (!unique_children.insert(child).second) - throw std::invalid_argument("duplicate topology child"); - for (const Renderable_Id parent : parents) { - if (reaches(child, parent)) - throw std::invalid_argument( - "topology relationships would create a cycle"); - } - } - for (const Renderable_Id parent : parents) { - auto& parent_children = nodes_.at(parent).children; - parent_children.reserve(parent_children.size() + 1); - } - for (const Renderable_Id child : children) { - auto& child_parents = nodes_.at(child).parents; - child_parents.reserve(child_parents.size() + 1); - } - const auto [iterator, inserted] = - nodes_.try_emplace(id, *nodes_.get_allocator().resource()); - if (!inserted) - throw std::invalid_argument("topology node is already attached"); - try { - iterator->second.parents.reserve(parents.size()); - iterator->second.parents.insert(iterator->second.parents.end(), - parents.begin(), parents.end()); - iterator->second.children.reserve(children.size()); - iterator->second.children.insert(iterator->second.children.end(), - children.begin(), children.end()); - } catch (...) { - nodes_.erase(iterator); - throw; - } - for (const Renderable_Id parent : parents) - nodes_.at(parent).children.push_back(id); - for (const Renderable_Id child : children) - nodes_.at(child).parents.push_back(id); + nodes_.try_emplace(id, *nodes_.get_allocator().resource()); } - -void Scene_Base::Id_Topology::erase(Renderable_Id id) noexcept { +void Scene_Base::Id_Topology::erase(Renderable_Id id) { const auto iterator = nodes_.find(id); if (iterator == nodes_.end()) return; - for (const Renderable_Id parent : iterator->second.parents) { - if (const auto node = nodes_.find(parent); node != nodes_.end()) - erase_id(node->second.children, id); - } - for (const Renderable_Id child : iterator->second.children) { - if (const auto node = nodes_.find(child); node != nodes_.end()) - erase_id(node->second.parents, id); - } + if (!iterator->second.parents.empty() || !iterator->second.children.empty()) + throw std::logic_error("topology node still has relationships"); nodes_.erase(iterator); } - -std::vector Scene_Base::Id_Topology::detach_and_promote( - Renderable_Id id) { - const auto iterator = nodes_.find(id); - if (iterator == nodes_.end()) - return {}; - const std::vector parents(iterator->second.parents.begin(), - iterator->second.parents.end()); - const std::vector children(iterator->second.children.begin(), - iterator->second.children.end()); - for (const Renderable_Id parent : parents) { - auto& parent_children = nodes_.at(parent).children; - parent_children.reserve(parent_children.size() + children.size()); - } - for (const Renderable_Id child : children) { - auto& child_parents = nodes_.at(child).parents; - child_parents.reserve(child_parents.size() + parents.size()); - } - for (const Renderable_Id parent : parents) - erase_id(nodes_.at(parent).children, id); - for (const Renderable_Id child : children) - erase_id(nodes_.at(child).parents, id); - for (const Renderable_Id parent : parents) { - auto& parent_children = nodes_.at(parent).children; - for (const Renderable_Id child : children) { - auto& child_parents = nodes_.at(child).parents; - if (!contains_id(parent_children, child)) - parent_children.push_back(child); - if (!contains_id(child_parents, parent)) - child_parents.push_back(parent); - } - } - nodes_.erase(iterator); - return children; -} - -bool Scene_Base::Id_Topology::replace_parents( - Renderable_Id child, const std::vector& parents) { - auto& child_node = nodes_.at(child); - std::unordered_set unique; +bool Scene_Base::Id_Topology::replace_parents(Renderable_Id child, const std::vector& parents) { + validate_endpoint(child); + std::vector unique; unique.reserve(parents.size()); for (const Renderable_Id parent : parents) { - validate_parent(child, parent); - if (!unique.insert(parent).second) - throw std::invalid_argument("duplicate topology parent"); + validate_endpoint(parent); + if (!contains_id(unique, parent)) + unique.push_back(parent); } - if (child_node.parents.size() == parents.size() && - std::all_of(parents.begin(), parents.end(), [&](Renderable_Id parent) { - return contains_id(child_node.parents, parent); - })) + auto& child_node = nodes_.at(child); + if (child_node.parents.size() == unique.size() && std::all_of(unique.begin(), unique.end(), [&](Renderable_Id parent) { + return contains_id(child_node.parents, parent); + })) return false; - child_node.parents.reserve(parents.size()); - for (const Renderable_Id parent : parents) { - auto& children = nodes_.at(parent).children; - if (!contains_id(children, child)) - children.reserve(children.size() + 1); - } - const std::vector previous(child_node.parents.begin(), - child_node.parents.end()); + const std::vector previous(child_node.parents.begin(), child_node.parents.end()); for (const Renderable_Id parent : previous) erase_id(nodes_.at(parent).children, child); child_node.parents.clear(); - child_node.parents.insert(child_node.parents.end(), parents.begin(), - parents.end()); - for (const Renderable_Id parent : parents) { + child_node.parents.insert(child_node.parents.end(), unique.begin(), unique.end()); + for (const Renderable_Id parent : unique) { auto& children = nodes_.at(parent).children; if (!contains_id(children, child)) children.push_back(child); } return true; } - -bool Scene_Base::Id_Topology::add_parent(Renderable_Id child, - Renderable_Id parent) { - validate_parent(child, parent); +bool Scene_Base::Id_Topology::add_parent(Renderable_Id child, Renderable_Id parent) { + validate_endpoint(child); + validate_endpoint(parent); auto& child_node = nodes_.at(child); if (contains_id(child_node.parents, parent)) return false; - auto& parent_node = nodes_.at(parent); - child_node.parents.reserve(child_node.parents.size() + 1); - parent_node.children.reserve(parent_node.children.size() + 1); child_node.parents.push_back(parent); - parent_node.children.push_back(child); + nodes_.at(parent).children.push_back(child); return true; } - bool Scene_Base::Id_Topology::clear_parents(Renderable_Id child) { + validate_endpoint(child); auto& child_node = nodes_.at(child); if (child_node.parents.empty()) return false; - const std::vector parents(child_node.parents.begin(), - child_node.parents.end()); + const std::vector parents(child_node.parents.begin(), child_node.parents.end()); for (const Renderable_Id parent : parents) erase_id(nodes_.at(parent).children, child); child_node.parents.clear(); return true; } - -const Scene_Base::Topology_Node& Scene_Base::Id_Topology::node( - Renderable_Id id) const { +bool Scene_Base::Id_Topology::contains(Renderable_Id id) const noexcept { + return nodes_.contains(id); +} +bool Scene_Base::Id_Topology::isolated(Renderable_Id id) const { + const auto& value = node(id); + return value.parents.empty() && value.children.empty(); +} +const Scene_Base::Topology_Node& Scene_Base::Id_Topology::node(Renderable_Id id) const { return nodes_.at(id); } - +std::vector Scene_Base::Id_Topology::ids() const { + std::vector result; + result.reserve(nodes_.size()); + for (const auto& [id, node] : nodes_) { + static_cast(node); + result.push_back(id); + } + return result; +} std::vector Scene_Base::Id_Topology::order() const { std::unordered_map indegree; indegree.reserve(nodes_.size()); - std::priority_queue, - std::greater<>> ready; + std::priority_queue, std::greater<>> ready; for (const auto& [id, node] : nodes_) { indegree.emplace(id, node.parents.size()); if (node.parents.empty()) @@ -300,79 +169,92 @@ private: }; Scene_Base::Scene_Base() : Scene_Base(*std::pmr::get_default_resource()) {} - Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource) : scene_lifetime_(std::make_shared(*this)), - memory_domain_(std::allocate_shared( - std::pmr::polymorphic_allocator( - &upstream_memory_resource), - upstream_memory_resource)), + memory_domain_(std::allocate_shared(std::pmr::polymorphic_allocator(&upstream_memory_resource), upstream_memory_resource)), renderables_(&memory_domain_->resource()), display_topology_(memory_domain_->resource()), dependency_topology_(memory_domain_->resource()), color_caches_(&memory_domain_->resource()), task_(memory_domain_->resource()), - mutation_queue_(&memory_domain_->resource()), - composite_begin_node_id_(allocate_node_id()) { + renderable_edit_queue_(&memory_domain_->resource()), + composite_begin_node_id_(allocate_renderive_node_id()) { worker_ = std::thread([this] { render_loop(); }); } - Scene_Base::~Scene_Base() { shutdown(); } -void Scene_Base::Mutation::wait() const { - if (!state_) - return; - std::unique_lock lock(state_->mutex); - if (!state_->completed && state_->scene && state_->scene->is_render_worker_thread()) - throw std::logic_error("scene mutation wait is not allowed from render execution"); - state_->condition.wait(lock, [this] { return state_->completed; }); - if (state_->exception) - std::rethrow_exception(state_->exception); -} -bool Scene_Base::Mutation::ready() const { - if (!state_) - return true; - std::lock_guard lock(state_->mutex); - return state_->completed; -} -void Scene_Base::Mutation::then(Mutation::Callback callback) const { - if (!callback) - return; - if (!state_) { - try { - callback({}); - } catch (...) { - } - return; - } - std::exception_ptr exception; - { - std::lock_guard lock(state_->mutex); - if (!state_->completed) { - state_->callbacks.push_back(std::move(callback)); - return; - } - exception = state_->exception; - } - try { - callback(exception); - } catch (...) { - } -} Scene_Base::Attach_Builder::Attach_Builder(Scene_Base& scene) : scene_(&scene), task_lock_(scene.task_mutex_) { if (scene.runtime_started_) throw std::logic_error("scene attach builder is only available before runtime starts"); } -void Scene_Base::Attach_Builder::attach(Renderable renderable, - Attach_Relationships relationships) { +Scene_Base::Attach_Builder::~Attach_Builder() { + try { + std::lock_guard lock(scene_->model_mutex_); + scene_->cleanup_detached_topology_locked(); + scene_->validate_structure_locked(); + } catch (...) { + Scene_Base::structure_fail_fast("initial scene structure", std::current_exception()); + } +} +void Scene_Base::Attach_Builder::attach(Renderable renderable) { std::lock_guard lock(scene_->model_mutex_); - scene_->apply_attach_renderable_locked(std::move(renderable), std::move(relationships)); + scene_->apply_attach_renderable_locked(std::move(renderable)); +} +void Scene_Base::Attach_Builder::set_parent(Relationship relationship, const Renderable& child, const Renderable& parent) { + std::lock_guard lock(scene_->model_mutex_); + if (relationship == Relationship::display) + scene_->apply_set_display_parent_locked(child, parent); + else + scene_->apply_set_dependency_parent_locked(child, parent); +} +void Scene_Base::Attach_Builder::add_parent(Relationship relationship, const Renderable& child, const Renderable& parent) { + std::lock_guard lock(scene_->model_mutex_); + if (relationship == Relationship::display) + scene_->apply_add_display_parent_locked(child, parent); + else + scene_->apply_add_dependency_parent_locked(child, parent); +} +void Scene_Base::Attach_Builder::clear_parents(Relationship relationship, const Renderable& child) { + std::lock_guard lock(scene_->model_mutex_); + if (relationship == Relationship::display) + scene_->apply_clear_display_parent_locked(child); + else + scene_->apply_clear_dependency_parent_locked(child); } Scene_Base::Attach_Builder Scene_Base::attach_builder() { return Attach_Builder(*this); } +void Scene_Base::Renderable_Editor::attach(Renderable renderable) { + scene_->apply_attach_renderable_locked(std::move(renderable)); +} +void Scene_Base::Renderable_Editor::detach(const Renderable& renderable) { + scene_->apply_detach_renderable_locked(renderable); +} +void Scene_Base::Renderable_Editor::set_parent(Relationship relationship, const Renderable& child, const Renderable& parent) { + if (relationship == Relationship::display) + scene_->apply_set_display_parent_locked(child, parent); + else + scene_->apply_set_dependency_parent_locked(child, parent); +} +void Scene_Base::Renderable_Editor::add_parent(Relationship relationship, const Renderable& child, const Renderable& parent) { + if (relationship == Relationship::display) + scene_->apply_add_display_parent_locked(child, parent); + else + scene_->apply_add_dependency_parent_locked(child, parent); +} +void Scene_Base::Renderable_Editor::clear_parents(Relationship relationship, const Renderable& child) { + if (relationship == Relationship::display) + scene_->apply_clear_display_parent_locked(child); + else + scene_->apply_clear_dependency_parent_locked(child); +} +void Scene_Base::edit_renderables(Renderable_Edit edit) { + if (!edit) + throw std::invalid_argument("renderable edit callback is empty"); + enqueue_renderable_edit(std::move(edit)); +} void Scene_Base::render() { submit_render(nullptr); } @@ -464,77 +346,39 @@ void Scene_Base::wait_for_render() { if (exception) std::rethrow_exception(exception); } -std::vector Scene_Base::relationship_ids_locked( - const std::vector& renderables) const { - std::vector ids; - ids.reserve(renderables.size()); - for (const Renderable& renderable : renderables) { - if (!renderable) - throw std::invalid_argument("topology parent is null"); - validate_renderable_scene(*renderable); - validate_renderable_attached_locked(renderable); - ids.push_back(renderable->renderable_id()); - } - return ids; -} -Scene_Base::Mutation Scene_Base::enqueue_mutation(std::function operation) { +void Scene_Base::enqueue_renderable_edit(Renderable_Edit edit) { std::lock_guard lock(task_mutex_); - return enqueue_mutation_locked(std::move(operation)); + enqueue_renderable_edit_locked(std::move(edit)); } -Scene_Base::Mutation Scene_Base::enqueue_mutation_locked(std::function operation) { +void Scene_Base::enqueue_renderable_edit_locked(Renderable_Edit edit) { if (stop_) throw std::logic_error("scene is shutting down"); runtime_started_ = true; - auto completion = std::make_shared(*this); - mutation_queue_.push_back({std::move(operation), completion}); + renderable_edit_queue_.push_back({std::move(edit)}); task_ready_.notify_one(); - return Mutation(std::move(completion)); } -void Scene_Base::complete_mutation(Mutation_Command& command, - std::exception_ptr exception, - std::vector& callbacks) noexcept { - { - std::lock_guard lock(command.completion->mutex); - command.completion->exception = exception; - command.completion->completed = true; - command.completion->scene = nullptr; - callbacks = std::move(command.completion->callbacks); - } - command.completion->condition.notify_all(); -} -void Scene_Base::apply_attach_renderable_locked(Renderable renderable, - Attach_Relationships relationships) { +void Scene_Base::apply_attach_renderable_locked(Renderable renderable) { if (!renderable) throw std::invalid_argument("renderable is null"); - validate_renderable_scene(*renderable); - if (is_renderable_attached_locked(renderable)) - return; - const auto display_parents = relationship_ids_locked(relationships.display_parents); - const auto display_children = relationship_ids_locked(relationships.display_children); - const auto dependency_parents = relationship_ids_locked(relationships.dependency_parents); - auto cache = make_renderable_color_cache(); + renderable->bind_scene(*this); const Renderable_Id id = renderable->renderable_id(); - bool display_attached{}; - bool dependency_attached{}; - bool renderable_inserted{}; - try { - dependency_topology_.attach(id, dependency_parents); - dependency_attached = true; - display_topology_.attach(id, display_parents, display_children); - display_attached = true; - if (!renderables_.try_emplace(id, renderable).second) + if (const auto existing = renderables_.find(id); existing != renderables_.end()) { + if (existing->second != renderable) throw std::logic_error("renderable id is already attached"); - renderable_inserted = true; + return; + } + if (!display_topology_.contains(id)) + display_topology_.attach(id); + if (!dependency_topology_.contains(id)) + dependency_topology_.attach(id); + auto cache = make_renderable_color_cache(); + if (!renderables_.try_emplace(id, renderable).second) + throw std::logic_error("renderable id is already attached"); + try { if (cache && !color_caches_.try_emplace(id, std::move(cache)).second) throw std::logic_error("renderable color cache is already attached"); } catch (...) { - color_caches_.erase(id); - if (renderable_inserted) - renderables_.erase(id); - if (display_attached) - display_topology_.erase(id); - if (dependency_attached) - dependency_topology_.erase(id); + renderables_.erase(id); throw; } renderable->real_time_data_state_->attached.store(true, std::memory_order_release); @@ -549,35 +393,23 @@ void Scene_Base::apply_detach_renderable_locked(const Renderable& renderable) { return; const Renderable_Id id = renderable->renderable_id(); renderable->real_time_data_state_->attached.store(false, std::memory_order_release); - display_topology_.detach_and_promote(id); - const auto dependency_children = dependency_topology_.detach_and_promote(id); - for (const Renderable_Id child : dependency_children) { - if (const auto iterator = renderables_.find(child); iterator != renderables_.end()) - iterator->second->invalidate_prepare(); - } color_caches_.erase(id); renderables_.erase(id); notify_model_dirty(); } -void Scene_Base::apply_set_display_parent_locked(const Renderable& child, - const Renderable& parent) { +void Scene_Base::apply_set_display_parent_locked(const Renderable& child, const Renderable& parent) { if (!child || !parent) throw std::invalid_argument("display relationship endpoint is null"); validate_renderable_scene(*child); validate_renderable_scene(*parent); - validate_renderable_attached_locked(child); - validate_renderable_attached_locked(parent); if (display_topology_.replace_parents(child->renderable_id(), {parent->renderable_id()})) notify_model_dirty(); } -void Scene_Base::apply_add_display_parent_locked(const Renderable& child, - const Renderable& parent) { +void Scene_Base::apply_add_display_parent_locked(const Renderable& child, const Renderable& parent) { if (!child || !parent) throw std::invalid_argument("display relationship endpoint is null"); validate_renderable_scene(*child); validate_renderable_scene(*parent); - validate_renderable_attached_locked(child); - validate_renderable_attached_locked(parent); if (display_topology_.add_parent(child->renderable_id(), parent->renderable_id())) notify_model_dirty(); } @@ -585,31 +417,24 @@ void Scene_Base::apply_clear_display_parent_locked(const Renderable& child) { if (!child) throw std::invalid_argument("display child is null"); validate_renderable_scene(*child); - validate_renderable_attached_locked(child); if (display_topology_.clear_parents(child->renderable_id())) notify_model_dirty(); } -void Scene_Base::apply_set_dependency_parent_locked(const Renderable& child, - const Renderable& parent) { +void Scene_Base::apply_set_dependency_parent_locked(const Renderable& child, const Renderable& parent) { if (!child || !parent) throw std::invalid_argument("dependency relationship endpoint is null"); validate_renderable_scene(*child); validate_renderable_scene(*parent); - validate_renderable_attached_locked(child); - validate_renderable_attached_locked(parent); if (!dependency_topology_.replace_parents(child->renderable_id(), {parent->renderable_id()})) return; child->invalidate_prepare(); notify_model_dirty(); } -void Scene_Base::apply_add_dependency_parent_locked(const Renderable& child, - const Renderable& parent) { +void Scene_Base::apply_add_dependency_parent_locked(const Renderable& child, const Renderable& parent) { if (!child || !parent) throw std::invalid_argument("dependency relationship endpoint is null"); validate_renderable_scene(*child); validate_renderable_scene(*parent); - validate_renderable_attached_locked(child); - validate_renderable_attached_locked(parent); if (!dependency_topology_.add_parent(child->renderable_id(), parent->renderable_id())) return; child->invalidate_prepare(); @@ -619,81 +444,92 @@ void Scene_Base::apply_clear_dependency_parent_locked(const Renderable& child) { if (!child) throw std::invalid_argument("dependency child is null"); validate_renderable_scene(*child); - validate_renderable_attached_locked(child); if (!dependency_topology_.clear_parents(child->renderable_id())) return; child->invalidate_prepare(); notify_model_dirty(); } -Scene_Base::Mutation Scene_Base::attach_renderable(Renderable renderable, - Attach_Relationships relationships) { - if (!renderable) - throw std::invalid_argument("renderable is null"); - validate_renderable_scene(*renderable); - return enqueue_mutation( - [this, renderable = std::move(renderable), relationships = std::move(relationships)]() mutable { - apply_attach_renderable_locked(std::move(renderable), std::move(relationships)); - }); +void Scene_Base::cleanup_detached_topology_locked() { + const auto cleanup = [this](Id_Topology& topology, const char* name) { + for (const Renderable_Id id : topology.ids()) { + if (renderables_.contains(id)) + continue; + if (!topology.isolated(id)) + throw std::logic_error(std::string(name) + " topology still references detached renderable " + std::to_string(id)); + topology.erase(id); + } + }; + cleanup(display_topology_, "display"); + cleanup(dependency_topology_, "dependency"); } -Scene_Base::Mutation Scene_Base::detach_renderable(const Renderable& renderable) { - if (!renderable) - throw std::invalid_argument("renderable is null"); - validate_renderable_scene(*renderable); - return enqueue_mutation([this, renderable] { apply_detach_renderable_locked(renderable); }); +void Scene_Base::validate_structure_locked() { + for (const auto& [id, renderable] : renderables_) { + if (!renderable) + throw std::logic_error("scene contains null renderable owner"); + validate_renderable_scene(*renderable); + if (!renderable->attached()) + throw std::logic_error("scene contains renderable whose attached state is false"); + if (!display_topology_.contains(id)) + throw std::logic_error("display topology is missing attached renderable " + std::to_string(id)); + if (!dependency_topology_.contains(id)) + throw std::logic_error("dependency topology is missing attached renderable " + std::to_string(id)); + } + const auto validate_topology = [this](const Id_Topology& topology, const char* name) { + const auto order = topology.order(); + if (order.size() != renderables_.size()) + throw std::logic_error(std::string(name) + " topology does not match attached renderables"); + for (const Renderable_Id id : order) { + if (!renderables_.contains(id)) + throw std::logic_error(std::string(name) + " topology references detached renderable " + std::to_string(id)); + } + }; + validate_topology(display_topology_, "display"); + validate_topology(dependency_topology_, "dependency"); } -Scene_Base::Mutation Scene_Base::set_parent(Relationship relationship, - const Renderable& child, - const Renderable& parent) { - if (!child || !parent) - throw std::invalid_argument("topology relationship endpoint is null"); - validate_renderable_scene(*child); - validate_renderable_scene(*parent); - return enqueue_mutation([this, relationship, child, parent] { - if (relationship == Relationship::display) - apply_set_display_parent_locked(child, parent); - else - apply_set_dependency_parent_locked(child, parent); - }); -} -Scene_Base::Mutation Scene_Base::add_parent(Relationship relationship, - const Renderable& child, - const Renderable& parent) { - if (!child || !parent) - throw std::invalid_argument("topology relationship endpoint is null"); - validate_renderable_scene(*child); - validate_renderable_scene(*parent); - return enqueue_mutation([this, relationship, child, parent] { - if (relationship == Relationship::display) - apply_add_display_parent_locked(child, parent); - else - apply_add_dependency_parent_locked(child, parent); - }); -} -Scene_Base::Mutation Scene_Base::clear_parents(Relationship relationship, - const Renderable& child) { - if (!child) - throw std::invalid_argument("topology child is null"); - validate_renderable_scene(*child); - return enqueue_mutation([this, relationship, child] { - if (relationship == Relationship::display) - apply_clear_display_parent_locked(child); - else - apply_clear_dependency_parent_locked(child); - }); +[[noreturn]] void Scene_Base::structure_fail_fast(const char* stage, std::exception_ptr exception) noexcept { + std::fprintf(stderr, "Renderive scene structure failure during %s", stage ? stage : "unknown stage"); + if (exception) { + try { + std::rethrow_exception(exception); + } catch (const std::exception& value) { + std::fprintf(stderr, ": %s", value.what()); + } catch (...) { + std::fprintf(stderr, ": unknown exception"); + } + } + std::fprintf(stderr, "\n"); + std::fflush(stderr); + std::abort(); } void Scene_Base::request_render_graph_rebuild(Renderable_Base& renderable) { + if (!renderable.attached()) { + renderable.reset_render_graph(); + return; + } validate_renderable_scene(renderable); - std::unique_lock lock(task_mutex_); - if (!runtime_started_) { + if (active_renderable_edit_scene_ == this) { renderable.reset_render_graph(); notify_model_dirty(); return; } - auto owner = renderable.shared_from_this(); - static_cast(enqueue_mutation_locked([this, owner = std::move(owner)] { + Renderable owner; + { + std::lock_guard lock(model_mutex_); + const auto iterator = renderables_.find(renderable.renderable_id()); + if (iterator == renderables_.end() || iterator->second.get() != &renderable) + return; + owner = iterator->second; + } + std::lock_guard lock(task_mutex_); + if (!runtime_started_) { owner->reset_render_graph(); notify_model_dirty(); - })); + return; + } + enqueue_renderable_edit_locked([owner = std::move(owner)](Renderable_Editor&) { + owner->reset_render_graph(); + owner->notify_scene_model_dirty(); + }); } void Scene_Base::publish_frame_state() { auto task_lock = lock_render_idle(); @@ -719,23 +555,6 @@ void Scene_Base::notify_model_dirty() noexcept { model_dirty_.store(true, std::memory_order_release); } -void Scene_Base::set_renderable_configuration( - const Renderable& renderable, Renderable_Configuration configuration) { - if (!renderable) - throw std::invalid_argument("renderable is null"); - validate_renderable_scene(*renderable); - bool changed{}; - { - std::lock_guard lock(model_mutex_); - std::lock_guard configuration_lock(renderable->configuration_mutex_); - changed = renderable->configuration_ != configuration; - if (changed) - renderable->configuration_ = configuration; - } - if (changed) - notify_model_dirty(); -} - std::size_t Scene_Base::renderable_count() const { std::lock_guard lock(model_mutex_); return renderables_.size(); @@ -960,7 +779,7 @@ std::unique_lock Scene_Base::lock_render_idle() { "render-idle operation is not allowed during render submission observation"); std::unique_lock lock(task_mutex_); render_completed_.wait(lock, [this] { - return !task_pending_ && !rendering_ && mutation_queue_.empty() && !mutating_; + return !task_pending_ && !rendering_ && renderable_edit_queue_.empty() && !editing_renderables_; }); return lock; } @@ -979,7 +798,7 @@ void Scene_Base::shutdown() noexcept { { std::unique_lock lock(task_mutex_); render_completed_.wait(lock, [this] { - return !task_pending_ && !rendering_ && mutation_queue_.empty() && !mutating_; + return !task_pending_ && !rendering_ && renderable_edit_queue_.empty() && !editing_renderables_; }); stop_ = true; } @@ -999,11 +818,11 @@ void Scene_Base::shutdown() noexcept { void Scene_Base::render_loop() { for (;;) { Render_Task task(memory_resource()); - std::vector mutations; + std::vector edits; { std::unique_lock lock(task_mutex_); task_ready_.wait(lock, [this] { - return stop_ || task_pending_ || !mutation_queue_.empty(); + return stop_ || task_pending_ || !renderable_edit_queue_.empty(); }); if (stop_) return; @@ -1012,42 +831,36 @@ void Scene_Base::render_loop() { task_pending_ = false; rendering_ = true; } else { - mutating_ = true; - mutations.reserve(mutation_queue_.size()); - while (!mutation_queue_.empty()) { - mutations.push_back(std::move(mutation_queue_.front())); - mutation_queue_.pop_front(); + editing_renderables_ = true; + edits.reserve(renderable_edit_queue_.size()); + while (!renderable_edit_queue_.empty()) { + edits.push_back(std::move(renderable_edit_queue_.front())); + renderable_edit_queue_.pop_front(); } } } - if (!mutations.empty()) { - std::vector exceptions(mutations.size()); - std::vector> callbacks(mutations.size()); + if (!edits.empty()) { { std::lock_guard lock(model_mutex_); - for (std::size_t index = 0; index < mutations.size(); ++index) { + Scene_Base* previous = std::exchange(active_renderable_edit_scene_, this); + Renderable_Editor editor(*this); + for (auto& command : edits) { try { - mutations[index].operation(); + command.edit(editor); + cleanup_detached_topology_locked(); + validate_structure_locked(); } catch (...) { - exceptions[index] = std::current_exception(); + active_renderable_edit_scene_ = previous; + structure_fail_fast("runtime renderable edit", std::current_exception()); } } + active_renderable_edit_scene_ = previous; } - for (std::size_t index = 0; index < mutations.size(); ++index) - complete_mutation(mutations[index], exceptions[index], callbacks[index]); { std::lock_guard lock(task_mutex_); - mutating_ = false; + editing_renderables_ = false; } render_completed_.notify_all(); - for (std::size_t index = 0; index < callbacks.size(); ++index) { - for (auto& callback : callbacks[index]) { - try { - callback(exceptions[index]); - } catch (...) { - } - } - } continue; } const auto& snapshot = *task.snapshot; @@ -1370,14 +1183,6 @@ void Scene_Base::execute_taskflow(Render_Task& task) { } } -Renderable_Id Scene_Base::allocate_renderable_id() { - return allocate_id(next_renderable_id_, "renderable id exhausted"); -} - -Render_Node_Id Scene_Base::allocate_node_id() { - return allocate_id(next_node_id_, "render node id exhausted"); -} - void Scene_Base::validate_renderable_scene( const Renderable_Base& renderable) const { if (renderable.real_time_data_state_->scene_lifetime.get() != @@ -1394,9 +1199,4 @@ bool Scene_Base::is_renderable_attached_locked( iterator->second.get() == renderable.get(); } -void Scene_Base::validate_renderable_attached_locked( - const Renderable& renderable) const { - if (!is_renderable_attached_locked(renderable)) - throw std::invalid_argument( - "renderable is not attached to this scene"); -} + diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index e8d8f8e..69041e4 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -21,6 +21,7 @@ #include "renderive/capture/Capture.hpp" #include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" #include "renderive/renderable/base/Renderable_Base.hpp" +#include "renderive/renderable/base/Renderive_Owner.hpp" #include "renderive/render_graph/Render_Plan.hpp" #include "Abstract_Frame.hpp" #include "Frame_Render_Snapshot.hpp" @@ -42,49 +43,25 @@ private: struct Render_Completion; public: - using Renderable = std::shared_ptr; - using Const_Renderable = std::shared_ptr; + using Renderable = renderive_Owner; + using Const_Renderable = renderive_Owner; enum class Relationship { display, dependency }; - struct Attach_Relationships { - std::vector display_parents; - std::vector display_children; - std::vector dependency_parents; - }; - - class Mutation { - public: - using Callback = std::function; - Mutation() = default; - void wait() const; - [[nodiscard]] bool ready() const; - void then(Callback callback) const; - private: - struct State { - explicit State(Scene_Base& scene) : scene(&scene) {} - Scene_Base* scene{}; - mutable std::mutex mutex; - std::condition_variable condition; - std::vector callbacks; - std::exception_ptr exception; - bool completed{}; - }; - explicit Mutation(std::shared_ptr state) : state_(std::move(state)) {} - std::shared_ptr state_; - friend class Scene_Base; - }; - class Attach_Builder { public: Attach_Builder(const Attach_Builder&) = delete; Attach_Builder& operator=(const Attach_Builder&) = delete; Attach_Builder(Attach_Builder&&) = delete; Attach_Builder& operator=(Attach_Builder&&) = delete; - void attach(Renderable renderable, Attach_Relationships relationships = {}); + ~Attach_Builder(); + void attach(Renderable renderable); + void set_parent(Relationship relationship, const Renderable& child, const Renderable& parent); + void add_parent(Relationship relationship, const Renderable& child, const Renderable& parent); + void clear_parents(Relationship relationship, const Renderable& child); private: explicit Attach_Builder(Scene_Base& scene); Scene_Base* scene_{}; @@ -92,6 +69,21 @@ public: friend class Scene_Base; }; + class Renderable_Editor { + public: + void attach(Renderable renderable); + void detach(const Renderable& renderable); + void set_parent(Relationship relationship, const Renderable& child, const Renderable& parent); + void add_parent(Relationship relationship, const Renderable& child, const Renderable& parent); + void clear_parents(Relationship relationship, const Renderable& child); + private: + explicit Renderable_Editor(Scene_Base& scene) : scene_(&scene) {} + Scene_Base* scene_{}; + friend class Scene_Base; + }; + + using Renderable_Edit = std::function; + enum class Observation_Event { render_submitted, render_started, @@ -133,16 +125,7 @@ public: void notify_model_dirty() noexcept; Attach_Builder attach_builder(); - Mutation attach_renderable(Renderable renderable, - Attach_Relationships relationships = {}); - Mutation detach_renderable(const Renderable& renderable); - Mutation set_parent(Relationship relationship, const Renderable& child, - const Renderable& parent); - Mutation add_parent(Relationship relationship, const Renderable& child, - const Renderable& parent); - Mutation clear_parents(Relationship relationship, const Renderable& child); - void set_renderable_configuration(const Renderable& renderable, - Renderable_Configuration configuration); + void edit_renderables(Renderable_Edit edit); [[nodiscard]] std::size_t renderable_count() const; [[nodiscard]] Topology_Snapshot topology_snapshot() const; @@ -168,14 +151,6 @@ public: Frame_Control_Strategy_Base& frame_control_strategy(); const Frame_Control_Strategy_Base& frame_control_strategy() const; - template - requires std::derived_from && - std::constructible_from - std::shared_ptr make_renderable(Args&&... args) { - return std::allocate_shared( - Scene_Memory_Allocator{memory_domain_}, *this, - std::forward(args)...); - } protected: virtual Frame_Control_Strategy_Base& frame_control_strategy_impl(); @@ -207,22 +182,20 @@ private: explicit Id_Topology(std::pmr::memory_resource& memory_resource) : nodes_(&memory_resource) {} - void attach(Renderable_Id id, const std::vector& parents, - const std::vector& children = {}); - void erase(Renderable_Id id) noexcept; - std::vector detach_and_promote(Renderable_Id id); - bool replace_parents(Renderable_Id child, - const std::vector& parents); + void attach(Renderable_Id id); + void erase(Renderable_Id id); + bool replace_parents(Renderable_Id child, const std::vector& parents); bool add_parent(Renderable_Id child, Renderable_Id parent); bool clear_parents(Renderable_Id child); + [[nodiscard]] bool contains(Renderable_Id id) const noexcept; + [[nodiscard]] bool isolated(Renderable_Id id) const; [[nodiscard]] const Topology_Node& node(Renderable_Id id) const; + [[nodiscard]] std::vector ids() const; [[nodiscard]] std::vector order() const; private: - [[nodiscard]] bool reaches(Renderable_Id from, Renderable_Id target) const; - void validate_parent(Renderable_Id child, Renderable_Id parent) const; - static void erase_id(std::pmr::vector& values, - Renderable_Id id) noexcept; + void validate_endpoint(Renderable_Id id) const; + static void erase_id(std::pmr::vector& values, Renderable_Id id) noexcept; std::pmr::unordered_map nodes_; }; @@ -253,9 +226,8 @@ private: std::shared_ptr completion; }; - struct Mutation_Command { - std::function operation; - std::shared_ptr completion; + struct Renderable_Edit_Command { + Renderable_Edit edit; }; class Execution_Context; @@ -268,12 +240,9 @@ private: }; void submit_render(Abstract_Frame* frame); - [[nodiscard]] Mutation enqueue_mutation(std::function operation); - [[nodiscard]] Mutation enqueue_mutation_locked(std::function operation); - void complete_mutation(Mutation_Command& command, std::exception_ptr exception, - std::vector& callbacks) noexcept; - void apply_attach_renderable_locked(Renderable renderable, - Attach_Relationships relationships); + void enqueue_renderable_edit(Renderable_Edit edit); + void enqueue_renderable_edit_locked(Renderable_Edit edit); + void apply_attach_renderable_locked(Renderable renderable); void apply_detach_renderable_locked(const Renderable& renderable); void apply_set_display_parent_locked(const Renderable& child, const Renderable& parent); @@ -291,16 +260,15 @@ private: const Frame_Render_Snapshot& snapshot, Render_Task& task); void render_loop(); void execute_taskflow(Render_Task& task); - Renderable_Id allocate_renderable_id(); - Render_Node_Id allocate_node_id(); void validate_renderable_scene(const Renderable_Base& renderable) const; [[nodiscard]] bool is_renderable_attached_locked( const Renderable& renderable) const; - void validate_renderable_attached_locked(const Renderable& renderable) const; - [[nodiscard]] std::vector relationship_ids_locked( - const std::vector& renderables) const; + void validate_structure_locked(); + void cleanup_detached_topology_locked(); + [[noreturn]] static void structure_fail_fast(const char* stage, std::exception_ptr exception = {}) noexcept; inline static thread_local Scene_Base* active_execution_scene_{}; + inline static thread_local Scene_Base* active_renderable_edit_scene_{}; inline static thread_local Scene_Base* active_submitted_observer_scene_{}; std::shared_ptr scene_lifetime_; @@ -317,7 +285,7 @@ private: std::condition_variable_any render_completed_; std::thread worker_; Render_Task task_; - std::pmr::deque mutation_queue_; + std::pmr::deque renderable_edit_queue_; std::shared_ptr current_completion_; std::exception_ptr pending_exception_; bool pending_exception_observed_{}; @@ -325,12 +293,10 @@ private: std::uint64_t render_sequence_{}; bool task_pending_{}; bool rendering_{}; - bool mutating_{}; + bool editing_renderables_{}; bool runtime_started_{}; bool stop_{}; - std::atomic next_renderable_id_{1}; - std::atomic next_node_id_{1}; const Render_Node_Id composite_begin_node_id_; Render_Plan_History render_plan_history_; Capture_Controller capture_controller_; diff --git a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp index 3af1e53..a4a7582 100644 --- a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp +++ b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp @@ -9,6 +9,7 @@ #include "renderive/real_time_data/Real_Time_Data.hpp" #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" +#include "renderive/scene/Scene_Test_Helpers.hpp" struct Real_Time_Data_Test_Time_Source { std::shared_ptr> time_ns{std::make_shared>()}; std::uint64_t now_ns() const noexcept { @@ -16,7 +17,7 @@ struct Real_Time_Data_Test_Time_Source { } }; struct Real_Time_Data_Test_Renderable : Renderable_Base { - explicit Real_Time_Data_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + explicit Real_Time_Data_Test_Renderable() : Renderable_Base() {} void prepare(const Prepare_Render_Context&) override { ++render_count; } @@ -35,7 +36,7 @@ class Real_Time_Data_Render_Probe : public Renderable_Base { Real_Time_Data_Binding binding_; }; public: - explicit Real_Time_Data_Render_Probe(Scene_Base& scene) : Renderable_Base(scene), data(*this) {} + explicit Real_Time_Data_Render_Probe() : Renderable_Base(), data(*this) {} void prepare(const Prepare_Render_Context& context) override { const auto& value = context.frame.render_state.get(data); rendered_value = value.value_or(-1); @@ -45,8 +46,11 @@ public: }; TEST(real_time_data_render_state_test, publishes_once_at_frame_boundary_and_reads_without_snapshot) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } renderable->data.update(1); scene.publish_frame_state(); renderable->data.update(2); @@ -93,9 +97,12 @@ TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy) Scene2D_Context<> scene; auto latest = std::make_shared(); auto history = std::make_shared(); - auto renderable = std::make_shared(With_Real_Time_Data(latest, history), scene); + auto renderable = renderive_Owner::make(With_Real_Time_Data(latest, history)); renderable->discard_stale_frame_on_latest_data_update = true; - scene.attach_renderable(renderable).wait(); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } { auto frame = scene.frame_control.acquire_painter(); ASSERT_TRUE(frame); @@ -110,12 +117,15 @@ TEST(real_time_data_attachment_test, discards_history_older_than_one_frame_inter Scene2D_Context scene(Observer_State<>{}, Strategy::Configuration{100'000'000.0}); auto latest = std::make_shared(); auto history = std::make_shared(); - auto renderable = std::make_shared(With_Real_Time_Data(latest, history), scene); + auto renderable = renderive_Owner::make(With_Real_Time_Data(latest, history)); renderable->real_time_data_discard_mode = Real_Time_Data_Discard_Mode::retain_frame_interval; history->update(1); history->update(2); history->update(3); - scene.attach_renderable(renderable); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } scene.render(); scene.wait_for_render(); EXPECT_EQ(history->snapshot(), (std::vector{2, 3})); @@ -126,50 +136,31 @@ TEST(real_time_data_attachment_test, rejects_null_real_time_data_source) { EXPECT_THROW((With_Real_Time_Data(latest)), std::invalid_argument); } using Real_Time_Data_Test_Latest_Attachment = Attach_Real_Time_Data; -TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_destroyed_renderable) { - Scene2D_Context<> first_scene; - Scene2D_Context<> second_scene; +TEST(real_time_data_attachment_test, rejects_binding_one_source_to_multiple_renderables) { auto latest = std::make_shared(); - auto first = std::make_shared(With_Real_Time_Data(latest), first_scene); - auto second = std::make_shared(With_Real_Time_Data(latest), second_scene); - first->discard_stale_frame_on_latest_data_update = true; - second->discard_stale_frame_on_latest_data_update = true; - first_scene.attach_renderable(first).wait(); - second_scene.attach_renderable(second).wait(); - { - auto frame = first_scene.frame_control.acquire_painter(); - ASSERT_TRUE(frame); - } - { - auto frame = second_scene.frame_control.acquire_painter(); - ASSERT_TRUE(frame); - } - latest->update(1); - EXPECT_EQ(first_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); - EXPECT_EQ(second_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); - first_scene.detach_renderable(first).wait(); + auto first = renderive_Owner::make(With_Real_Time_Data(latest)); + EXPECT_THROW((renderive_Owner::make(With_Real_Time_Data(latest))), std::logic_error); first.reset(); - { - auto frame = second_scene.frame_control.acquire_painter(); - ASSERT_TRUE(frame); - } - latest->update(2); - EXPECT_EQ(first_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); - EXPECT_EQ(second_scene.frame_control.counter_statistics().manually_discarded_frame_count, 2); + EXPECT_NO_THROW((renderive_Owner::make(With_Real_Time_Data(latest)))); } TEST(real_time_data_attachment_test, detached_renderable_no_longer_drives_frame_strategy) { Scene2D_Context<> scene; auto latest = std::make_shared(); - auto renderable = std::make_shared(With_Real_Time_Data(latest), scene); + auto renderable = renderive_Owner::make(With_Real_Time_Data(latest)); renderable->discard_stale_frame_on_latest_data_update = true; - scene.attach_renderable(renderable).wait(); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } { auto frame = scene.frame_control.acquire_painter(); ASSERT_TRUE(frame); } latest->update(1); EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); - scene.detach_renderable(renderable).wait(); + wait_renderable_edit(scene, [renderable](auto& editor) { + editor.detach(renderable); + }); { auto frame = scene.frame_control.acquire_painter(); ASSERT_TRUE(frame); @@ -180,7 +171,7 @@ TEST(real_time_data_attachment_test, detached_renderable_no_longer_drives_frame_ TEST(real_time_data_attachment_test, unbind_is_synchronized_with_concurrent_updates) { Scene2D_Context<> scene; auto latest = std::make_shared(); - auto renderable = std::make_shared(With_Real_Time_Data(latest), scene); + auto renderable = renderive_Owner::make(With_Real_Time_Data(latest)); std::atomic running{true}; std::atomic updated{}; std::thread updater([&] { @@ -202,8 +193,11 @@ TEST(real_time_data_attachment_test, runtime_discard_configuration_is_safe_durin Scene2D_Context<> scene; auto latest = std::make_shared(); auto history = std::make_shared(); - auto renderable = std::make_shared(With_Real_Time_Data(latest, history), scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(With_Real_Time_Data(latest, history)); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } std::thread updater([&] { for (int value = 0; value < 1000; ++value) { latest->update(value); @@ -225,11 +219,14 @@ TEST(real_time_data_attachment_test, runtime_discard_configuration_is_safe_durin } TEST(real_time_data_attachment_test, ignores_updates_after_bound_scene_is_destroyed) { auto latest = std::make_shared(); - std::shared_ptr renderable; + renderive_Owner renderable; { Scene2D_Context<> scene; - renderable = std::make_shared(With_Real_Time_Data(latest), scene); - scene.attach_renderable(renderable).wait(); + renderable = renderive_Owner::make(With_Real_Time_Data(latest)); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } latest->update(1); EXPECT_EQ(scene.frame_control.state().real_time_data_update_sequence, 1); } @@ -237,28 +234,18 @@ TEST(real_time_data_attachment_test, ignores_updates_after_bound_scene_is_destro EXPECT_THROW(renderable->scene(), std::logic_error); renderable.reset(); } -TEST(real_time_data_attachment_test, notifies_each_scene_once_for_one_source_update) { - Scene2D_Context<> scene; +TEST(real_time_data_attachment_test, source_binding_is_owned_by_renderable_lifetime) { auto latest = std::make_shared(); - auto first = std::make_shared(With_Real_Time_Data(latest), scene); - auto second = std::make_shared(With_Real_Time_Data(latest), scene); - first->discard_stale_frame_on_latest_data_update = true; - second->discard_stale_frame_on_latest_data_update = true; - scene.attach_renderable(first).wait(); - scene.attach_renderable(second).wait(); - { - auto frame = scene.frame_control.acquire_painter(); - ASSERT_TRUE(frame); - } - latest->update(1); - EXPECT_EQ(scene.frame_control.state().real_time_data_update_sequence, 1); - EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); + auto renderable = renderive_Owner::make(With_Real_Time_Data(latest)); + EXPECT_THROW((renderive_Owner::make(With_Real_Time_Data(latest))), std::logic_error); + renderable.reset(); + EXPECT_NO_THROW((renderive_Owner::make(With_Real_Time_Data(latest)))); } TEST(real_time_data_attachment_test, attachment_owns_real_time_data_sources) { Scene2D_Context<> scene; auto latest = std::make_shared(); std::weak_ptr weak = latest; - auto renderable = std::make_shared(With_Real_Time_Data(latest), scene); + auto renderable = renderive_Owner::make(With_Real_Time_Data(latest)); latest.reset(); EXPECT_FALSE(weak.expired()); renderable->real_time_data().update(1); @@ -287,8 +274,11 @@ TEST(real_time_data_attachment_test, frame_strategy_observer_can_reacquire_scene Real_Time_Data_Reentrant_Strategy::Configuration configuration{60.0}; Scene2D_Context scene(std::move(observer_state), configuration); auto latest = std::make_shared(); - auto renderable = std::make_shared(With_Real_Time_Data(latest), scene); - scene.attach_renderable(renderable).wait(); + auto renderable = renderive_Owner::make(With_Real_Time_Data(latest)); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } std::atomic reacquired{}; observer_data->callback = [&] { reacquired.store(&renderable->scene() == &scene, std::memory_order_release); diff --git a/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp b/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp index 06c62ea..6c86c58 100644 --- a/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp +++ b/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp @@ -7,7 +7,7 @@ #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" struct Renderable_Base_Test_Renderable : Renderable_Base { - Renderable_Base_Test_Renderable(Scene_Base& scene, bool cache_enabled) : Renderable_Base(scene, {.cache_enabled = cache_enabled}) {} + Renderable_Base_Test_Renderable(bool cache_enabled) : Renderable_Base({.cache_enabled = cache_enabled}) {} void prepare(const Prepare_Render_Context&) override { ++render_count; } @@ -16,19 +16,30 @@ struct Renderable_Base_Test_Renderable : Renderable_Base { TEST(renderable_base_test, exposes_scene_and_strategy_through_accessors) { Scene2D_Context<> low_latency_scene; Scene2D_Context> manual_scene; - Renderable_Base_Test_Renderable low_latency_renderable(low_latency_scene, true); - Renderable_Base_Test_Renderable manual_renderable(manual_scene, true); - EXPECT_EQ(&low_latency_renderable.scene(), &low_latency_scene); - EXPECT_EQ(&low_latency_renderable.scene().frame_control_strategy(), &low_latency_scene.frame_control); - EXPECT_DOUBLE_EQ(low_latency_renderable.scene().frame_control_strategy().frequency_hz(), 60.0); - EXPECT_TRUE(std::isnan(manual_renderable.scene().frame_control_strategy().frequency_hz())); + auto low_latency_renderable = renderive_Owner::make(true); + auto manual_renderable = renderive_Owner::make(true); + { + auto attach = low_latency_scene.attach_builder(); + attach.attach(low_latency_renderable); + } + { + auto attach = manual_scene.attach_builder(); + attach.attach(manual_renderable); + } + EXPECT_EQ(&low_latency_renderable->scene(), &low_latency_scene); + EXPECT_EQ(&low_latency_renderable->scene().frame_control_strategy(), &low_latency_scene.frame_control); + EXPECT_DOUBLE_EQ(low_latency_renderable->scene().frame_control_strategy().frequency_hz(), 60.0); + EXPECT_TRUE(std::isnan(manual_renderable->scene().frame_control_strategy().frequency_hz())); } TEST(renderable_base_test, controls_cache_with_configuration) { Scene2D_Context<> scene; - auto cached = std::make_shared(scene, true); - auto uncached = std::make_shared(scene, false); - scene.attach_renderable(cached); - scene.attach_renderable(uncached); + auto cached = renderive_Owner::make(true); + auto uncached = renderive_Owner::make(false); + { + auto attach = scene.attach_builder(); + attach.attach(cached); + attach.attach(uncached); + } scene.render(); scene.wait_for_render(); scene.render(); @@ -43,18 +54,21 @@ TEST(renderable_base_test, controls_cache_with_configuration) { } TEST(renderable_base_test, changes_configuration_through_scene) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene, false); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(false); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } scene.render(); scene.wait_for_render(); - scene.set_renderable_configuration(renderable, {.cache_enabled = true}); + renderable->set_configuration({.cache_enabled = true}); scene.render(); scene.wait_for_render(); EXPECT_TRUE(renderable->configuration().cache_enabled); EXPECT_EQ(renderable->render_count, 1); } struct Renderable_Base_Cache_Invalidation_Test_Renderable : Renderable_Base { - explicit Renderable_Base_Cache_Invalidation_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = true}) {} + explicit Renderable_Base_Cache_Invalidation_Test_Renderable() : Renderable_Base({.cache_enabled = true}) {} void prepare(const Prepare_Render_Context&) override { const int count = render_count.fetch_add(1, std::memory_order_acq_rel) + 1; if (count != 1) { @@ -86,8 +100,11 @@ struct Renderable_Base_Cache_Invalidation_Test_Renderable : Renderable_Base { }; TEST(renderable_base_test, preserves_cache_invalidation_that_happens_during_render) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } scene.render(); renderable->wait_started(); renderable->invalidate_prepare(); @@ -100,7 +117,7 @@ TEST(renderable_base_test, preserves_cache_invalidation_that_happens_during_rend EXPECT_TRUE(renderable->prepare_cache_valid()); } struct Renderable_Base_Render_Graph_Rebuild_Test_Renderable : Renderable_Base { - explicit Renderable_Base_Render_Graph_Rebuild_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + explicit Renderable_Base_Render_Graph_Rebuild_Test_Renderable() : Renderable_Base() {} void build_prepare_graph(Renderable_Graph_Builder& graph) override { const int count = build_count.fetch_add(1, std::memory_order_acq_rel) + 1; if (count == 1) { @@ -138,8 +155,7 @@ struct Renderable_Base_Render_Graph_Rebuild_Test_Renderable : Renderable_Base { bool release_build{}; }; TEST(renderable_base_test, preserves_rebuild_request_that_arrives_during_render_graph_build) { - Scene2D_Context<> scene; - Renderable_Base_Render_Graph_Rebuild_Test_Renderable renderable(scene); + Renderable_Base_Render_Graph_Rebuild_Test_Renderable renderable; std::shared_ptr first_graph; std::thread builder([&] { first_graph = renderable.graph_snapshot(); @@ -156,8 +172,7 @@ TEST(renderable_base_test, preserves_rebuild_request_that_arrives_during_render_ EXPECT_NE(first_graph.get(), second_graph.get()); } TEST(renderable_base_test, render_graph_snapshots_remain_valid_during_concurrent_rebuilds) { - Scene2D_Context<> scene; - Renderable_Base_Render_Graph_Rebuild_Test_Renderable renderable(scene); + Renderable_Base_Render_Graph_Rebuild_Test_Renderable renderable; renderable.release(); std::atomic running{true}; std::thread rebuilder([&] { @@ -178,12 +193,15 @@ TEST(renderable_base_test, render_graph_snapshots_remain_valid_during_concurrent } TEST(renderable_base_test, configuration_snapshot_is_safe_during_scene_updates) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene, false); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(false); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } std::atomic running{true}; std::thread writer([&] { for (int index = 0; index < 1000; ++index) { - scene.set_renderable_configuration(renderable, {.cache_enabled = index % 2 != 0}); + renderable->set_configuration({.cache_enabled = index % 2 != 0}); } running.store(false, std::memory_order_release); }); diff --git a/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp b/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp index c45c5e4..c534159 100644 --- a/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp +++ b/Kernel/tests/renderive/renderable/concept/Renderable_Test.cpp @@ -3,13 +3,17 @@ #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" struct Renderable_Concept_Test : Renderable_Base { - explicit Renderable_Concept_Test(Scene_Base& scene) : Renderable_Base(scene) {} + explicit Renderable_Concept_Test() : Renderable_Base() {} void prepare(const Prepare_Render_Context&) override {} }; static_assert(Renderable); TEST(renderable_concept_test, exposes_scene_and_configuration_state) { Scene2D_Context<> scene; - Renderable_Concept_Test renderable(scene); - EXPECT_EQ(&renderable.scene(), &scene); - EXPECT_FALSE(renderable.configuration().cache_enabled); + auto renderable = renderive_Owner::make(); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } + EXPECT_EQ(&renderable->scene(), &scene); + EXPECT_FALSE(renderable->configuration().cache_enabled); } diff --git a/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp b/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp index 2fedf2b..fd2f537 100644 --- a/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp +++ b/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp @@ -1,5 +1,4 @@ #include - #include #include #include @@ -9,14 +8,11 @@ #include #include #include - #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" - +#include "Scene_Test_Helpers.hpp" namespace { - using namespace std::chrono_literals; - class Gate { public: void arrive_and_wait() { @@ -25,25 +21,21 @@ public: condition_.notify_all(); condition_.wait(lock, [this] { return open_; }); } - void wait_until_arrived() { std::unique_lock lock(mutex_); ASSERT_TRUE(condition_.wait_for(lock, 2s, [this] { return arrived_; })); } - void open() { std::lock_guard lock(mutex_); open_ = true; condition_.notify_all(); } - private: std::mutex mutex_; std::condition_variable condition_; bool arrived_{}; bool open_{}; }; - template bool wait_until(Predicate&& predicate) { const auto deadline = std::chrono::steady_clock::now() + 2s; @@ -54,119 +46,104 @@ bool wait_until(Predicate&& predicate) { } return true; } - class Counting_Renderable : public Renderable_Base { public: - explicit Counting_Renderable(Scene_Base& scene) - : Renderable_Base(scene, {.cache_enabled = false}) {} - + Counting_Renderable() : Renderable_Base({.cache_enabled = false}) {} std::atomic prepare_count{}; - protected: void prepare(const Prepare_Render_Context&) override { prepare_count.fetch_add(1, std::memory_order_release); } }; - class Blocking_Renderable : public Counting_Renderable { public: - Blocking_Renderable(Scene_Base& scene, std::shared_ptr gate, - int block_on_call = 1, - std::atomic* destroyed = nullptr) - : Counting_Renderable(scene), gate_(std::move(gate)), - block_on_call_(block_on_call), destroyed_(destroyed) {} - + Blocking_Renderable(std::shared_ptr gate, int block_on_call = 1, std::atomic* destroyed = nullptr) + : gate_(std::move(gate)), block_on_call_(block_on_call), destroyed_(destroyed) {} ~Blocking_Renderable() override { if (destroyed_) destroyed_->store(true, std::memory_order_release); } - protected: void prepare(const Prepare_Render_Context&) override { const int call = prepare_count.fetch_add(1, std::memory_order_acq_rel) + 1; if (call == block_on_call_) gate_->arrive_and_wait(); } - private: std::shared_ptr gate_; int block_on_call_{}; std::atomic* destroyed_{}; }; - -TEST(dynamic_renderable_lifecycle_test, - attach_during_render_is_queued_until_the_current_frame_finishes) { +TEST(dynamic_renderable_lifecycle_test, edit_waits_until_current_frame_finishes) { Scene2D_Context<> scene; auto gate = std::make_shared(); - auto current = std::make_shared(scene, gate); - auto attached = std::make_shared(scene); - scene.attach_renderable(current).wait(); + auto current = renderive_Owner::make(gate); + auto added = renderive_Owner::make(); + attach_initial(scene, current); scene.render(); gate->wait_until_arrived(); - std::atomic callback_called{}; - auto mutation = scene.attach_renderable(attached); - mutation.then([&](std::exception_ptr exception) { - EXPECT_FALSE(exception); - callback_called.store(true, std::memory_order_release); + std::atomic edited{}; + scene.edit_renderables([added, &edited](auto& editor) { + editor.attach(added); + edited.store(true, std::memory_order_release); }); - EXPECT_FALSE(mutation.ready()); - EXPECT_FALSE(callback_called.load(std::memory_order_acquire)); - EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 0); + EXPECT_FALSE(edited.load(std::memory_order_acquire)); gate->open(); scene.wait_for_render(); - mutation.wait(); - EXPECT_TRUE(wait_until([&] { return callback_called.load(std::memory_order_acquire); })); - EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 0); + EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); })); + EXPECT_EQ(added->prepare_count.load(std::memory_order_acquire), 0); scene.render(); scene.wait_for_render(); - EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 1); + EXPECT_EQ(added->prepare_count.load(std::memory_order_acquire), 1); } - -TEST(dynamic_renderable_lifecycle_test, - detach_during_render_waits_for_the_frame_before_releasing_scene_ownership) { +TEST(dynamic_renderable_lifecycle_test, detach_waits_until_current_frame_finishes_before_releasing_scene_ownership) { Scene2D_Context<> scene; auto gate = std::make_shared(); std::atomic destroyed{}; - auto renderable = std::make_shared(scene, gate, 1, &destroyed); - std::weak_ptr weak = renderable; - scene.attach_renderable(renderable).wait(); + auto renderable = renderive_Owner::make(gate, 1, &destroyed); + std::weak_ptr weak = renderable.share(); + attach_initial(scene, renderable); scene.render(); gate->wait_until_arrived(); - auto mutation = scene.detach_renderable(renderable); - EXPECT_FALSE(mutation.ready()); + std::atomic edited{}; + scene.edit_renderables([renderable, &edited](auto& editor) { + editor.detach(renderable); + edited.store(true, std::memory_order_release); + }); renderable.reset(); EXPECT_FALSE(destroyed.load(std::memory_order_acquire)); - EXPECT_FALSE(weak.expired()); gate->open(); scene.wait_for_render(); - mutation.wait(); + EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); })); EXPECT_TRUE(wait_until([&] { return destroyed.load(std::memory_order_acquire); })); EXPECT_TRUE(weak.expired()); EXPECT_EQ(scene.renderable_count(), 0u); } - -TEST(dynamic_renderable_lifecycle_test, - reparent_during_render_changes_only_the_next_frame_dependency_plan) { +TEST(dynamic_renderable_lifecycle_test, reparent_edit_changes_only_the_next_frame_dependency_plan) { Scene2D_Context<> scene; auto first_gate = std::make_shared(); auto second_gate = std::make_shared(); - auto first_parent = std::make_shared(scene, first_gate, 1); - auto second_parent = std::make_shared(scene, second_gate, 2); - auto child = std::make_shared(scene); - scene.attach_renderable(first_parent); - scene.attach_renderable(second_parent); - auto attach = scene.attach_renderable(child, { - .dependency_parents = {first_parent} - }); - attach.wait(); + auto first_parent = renderive_Owner::make(first_gate, 1); + auto second_parent = renderive_Owner::make(second_gate, 2); + auto child = renderive_Owner::make(); + { + auto attach = scene.attach_builder(); + attach.attach(first_parent); + attach.attach(second_parent); + attach.attach(child); + attach.set_parent(Scene_Base::Relationship::dependency, child, first_parent); + } scene.render(); first_gate->wait_until_arrived(); - auto reparent = scene.set_parent(Scene_Base::Relationship::dependency, child, second_parent); - EXPECT_FALSE(reparent.ready()); + std::atomic edited{}; + scene.edit_renderables([child, second_parent, &edited](auto& editor) { + editor.set_parent(Scene_Base::Relationship::dependency, child, second_parent); + edited.store(true, std::memory_order_release); + }); EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 0); first_gate->open(); scene.wait_for_render(); - reparent.wait(); + EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); })); EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 1); scene.render(); second_gate->wait_until_arrived(); @@ -175,297 +152,133 @@ TEST(dynamic_renderable_lifecycle_test, scene.wait_for_render(); EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 2); } - class Rebuildable_Renderable final : public Renderable_Base { public: - Rebuildable_Renderable(Scene_Base& scene, std::shared_ptr first_graph_gate) - : Renderable_Base(scene, {.cache_enabled = false}), - first_graph_gate_(std::move(first_graph_gate)) {} - + explicit Rebuildable_Renderable(std::shared_ptr first_graph_gate) + : Renderable_Base({.cache_enabled = false}), first_graph_gate_(std::move(first_graph_gate)) {} void publish_second_graph() { graph_version_.store(2, std::memory_order_release); rebuild_render_graph(); } - std::vector executions() const { std::lock_guard lock(executions_mutex_); return executions_; } - protected: void build_prepare_graph(Renderable_Graph_Builder& builder) override { const int version = graph_version_.load(std::memory_order_acquire); - builder.emplace("versioned_prepare", version == 1 ? "Graph A" : "Graph B", - [this, version](const Prepare_Render_Context&) { + builder.emplace("versioned_prepare", version == 1 ? "Graph A" : "Graph B", [this, version](const Prepare_Render_Context&) { if (version == 1) first_graph_gate_->arrive_and_wait(); std::lock_guard lock(executions_mutex_); executions_.push_back(version); }); } - private: std::shared_ptr first_graph_gate_; std::atomic graph_version_{1}; mutable std::mutex executions_mutex_; std::vector executions_; }; - -TEST(dynamic_renderable_lifecycle_test, - graph_rebuild_during_render_publishes_an_immutable_graph_for_the_next_frame) { +TEST(dynamic_renderable_lifecycle_test, graph_rebuild_uses_the_same_no_inflight_frame_barrier) { Scene2D_Context<> scene; auto gate = std::make_shared(); - auto renderable = std::make_shared(scene, gate); - scene.attach_renderable(renderable); - + auto renderable = renderive_Owner::make(gate); + attach_initial(scene, renderable); scene.render(); gate->wait_until_arrived(); renderable->publish_second_graph(); gate->open(); scene.wait_for_render(); EXPECT_EQ(renderable->executions(), std::vector({1})); - scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->executions(), std::vector({1, 2})); } - -class Self_Detaching_Renderable final : public Counting_Renderable { -public: - using Counting_Renderable::Counting_Renderable; - - std::weak_ptr self; - -protected: - void prepare(const Prepare_Render_Context&) override { - prepare_count.fetch_add(1, std::memory_order_release); - if (auto owner = self.lock()) - scene().detach_renderable(owner); - } -}; - -TEST(dynamic_renderable_lifecycle_test, - renderable_can_detach_itself_without_deadlock_and_finishes_the_current_frame) { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - renderable->self = renderable; - scene.attach_renderable(renderable); - - scene.render(); - scene.wait_for_render(); - EXPECT_TRUE(wait_until([&] { return scene.renderable_count() == 0; })); - EXPECT_EQ(renderable->prepare_count.load(std::memory_order_acquire), 1); - EXPECT_EQ(scene.renderable_count(), 0u); - scene.render(); - scene.wait_for_render(); - EXPECT_EQ(renderable->prepare_count.load(std::memory_order_acquire), 1); -} - -class Tracking_Color_Cache final : public Color_Cache { -public: - Tracking_Color_Cache() : instance_id(next_instance_id.fetch_add(1) + 1) {} - explicit Tracking_Color_Cache(std::pmr::memory_resource&) : Tracking_Color_Cache() {} - ~Tracking_Color_Cache() override { - if (instance_id == watched_instance.load(std::memory_order_acquire)) - watched_destroyed.store(true, std::memory_order_release); - } - - void clear() override { - samples.clear(); - } - - void composite(const Color_Cache& source) override { - const auto& cache = dynamic_cast(source); - samples.insert(samples.end(), cache.samples.begin(), cache.samples.end()); - } - - static void reset() { - next_instance_id.store(0, std::memory_order_release); - watched_instance.store(0, std::memory_order_release); - watched_destroyed.store(false, std::memory_order_release); - } - - int instance_id{}; - std::vector samples; - inline static std::atomic next_instance_id{}; - inline static std::atomic watched_instance{}; - inline static std::atomic watched_destroyed{}; -}; - -class Blocking_Painted_Renderable final : public Renderable_Base { -public: - Blocking_Painted_Renderable(Scene_Base& scene, std::shared_ptr gate) - : Renderable_Base(scene, {.cache_enabled = false}), gate_(std::move(gate)) {} - -protected: - void prepare(const Prepare_Render_Context&) override { - if (prepare_count_.fetch_add(1, std::memory_order_acq_rel) == 0) - gate_->arrive_and_wait(); - } - - void build_paint_graph(Renderable_Graph_Builder& builder) override { - builder.emplace("paint", "Paint", - [](const Paint_Render_Context& context) { - auto& cache = dynamic_cast(context.color_cache); - cache.samples.push_back(cache.instance_id); - }); - } - -private: - std::shared_ptr gate_; - std::atomic prepare_count_{}; -}; - -TEST(dynamic_renderable_lifecycle_test, - detached_color_cache_lives_through_the_frame_and_reattach_uses_a_new_cache) { - Tracking_Color_Cache::reset(); - Scene2D_Context, Tracking_Color_Cache> scene; - auto gate = std::make_shared(); - auto renderable = std::make_shared(scene, gate); - scene.attach_renderable(renderable).wait(); - ASSERT_EQ(Tracking_Color_Cache::next_instance_id.load(), 2); - Tracking_Color_Cache::watched_instance.store(2, std::memory_order_release); - scene.render(); - gate->wait_until_arrived(); - auto detach = scene.detach_renderable(renderable); - auto attach = scene.attach_renderable(renderable); - EXPECT_FALSE(detach.ready()); - EXPECT_FALSE(attach.ready()); - EXPECT_FALSE(Tracking_Color_Cache::watched_destroyed.load(std::memory_order_acquire)); - EXPECT_EQ(Tracking_Color_Cache::next_instance_id.load(), 2); - gate->open(); - scene.wait_for_render(); - detach.wait(); - attach.wait(); - EXPECT_TRUE(wait_until([] { - return Tracking_Color_Cache::watched_destroyed.load(std::memory_order_acquire); - })); - ASSERT_EQ(Tracking_Color_Cache::next_instance_id.load(), 3); - scene.with_final_color_cache([](const Tracking_Color_Cache& cache) { - EXPECT_EQ(cache.samples, std::vector({2})); - }); - - scene.render(); - scene.wait_for_render(); - scene.with_final_color_cache([](const Tracking_Color_Cache& cache) { - EXPECT_EQ(cache.samples, std::vector({3})); - }); -} - class Sequence_Parent final : public Renderable_Base { public: - Sequence_Parent(Scene_Base& scene, std::atomic& completed_sequence) - : Renderable_Base(scene, {.cache_enabled = false}), - completed_sequence_(completed_sequence) {} - + explicit Sequence_Parent(std::atomic& completed_sequence) + : Renderable_Base({.cache_enabled = false}), completed_sequence_(completed_sequence) {} protected: void prepare(const Prepare_Render_Context& context) override { completed_sequence_.store(context.frame.render_sequence, std::memory_order_release); } - private: std::atomic& completed_sequence_; }; - class Sequence_Child final : public Renderable_Base { public: - Sequence_Child(Scene_Base& scene, std::atomic& parent_sequence, - std::atomic& violations) - : Renderable_Base(scene, {.cache_enabled = false}), - parent_sequence_(parent_sequence), violations_(violations) {} - - void invalidate_graph() { - rebuild_render_graph(); - } - + Sequence_Child(std::atomic& parent_sequence, std::atomic& violations) + : Renderable_Base({.cache_enabled = false}), parent_sequence_(parent_sequence), violations_(violations) {} protected: void prepare(const Prepare_Render_Context& context) override { if (parent_sequence_.load(std::memory_order_acquire) != context.frame.render_sequence) violations_.fetch_add(1, std::memory_order_relaxed); } - private: std::atomic& parent_sequence_; std::atomic& violations_; }; - -TEST(dynamic_renderable_lifecycle_test, - atomic_attach_and_concurrent_membership_updates_never_publish_partial_topology) { +TEST(dynamic_renderable_lifecycle_test, one_runtime_callback_can_apply_a_complete_relationship_change) { Scene2D_Context<> scene; std::atomic parent_sequence{}; std::atomic violations{}; - std::atomic failures{}; - auto parent = std::make_shared(scene, parent_sequence); - auto alternate_parent = - std::make_shared(scene, parent_sequence); - scene.attach_renderable(parent); - scene.attach_renderable(alternate_parent, { - .display_parents = {parent}, - .dependency_parents = {parent} + auto parent = renderive_Owner::make(parent_sequence); + auto child = renderive_Owner::make(parent_sequence, violations); + attach_initial(scene, parent); + wait_renderable_edit(scene, [parent, child](auto& editor) { + editor.attach(child); + editor.set_parent(Scene_Base::Relationship::dependency, child, parent); + editor.set_parent(Scene_Base::Relationship::display, child, parent); }); - - std::atomic controls_done{}; - std::thread renderer([&] { - while (!controls_done.load(std::memory_order_acquire)) { - try { - scene.render(); - scene.wait_for_render(); - } catch (...) { - failures.fetch_add(1, std::memory_order_relaxed); - } - } + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(violations.load(std::memory_order_acquire), 0); + wait_renderable_edit(scene, [child](auto& editor) { + editor.clear_parents(Scene_Base::Relationship::dependency, child); + editor.clear_parents(Scene_Base::Relationship::display, child); + editor.detach(child); }); - - constexpr int controller_count = 2; - constexpr int iterations = 1000; - std::vector controllers; - for (int controller = 0; controller < controller_count; ++controller) { - controllers.emplace_back([&] { - for (int index = 0; index < iterations; ++index) { - try { - auto child = std::make_shared( - scene, parent_sequence, violations); - scene.attach_renderable(child, { - .display_parents = {parent}, - .dependency_parents = {parent} - }); - scene.set_parent(Scene_Base::Relationship::display, child, alternate_parent); - scene.set_parent(Scene_Base::Relationship::dependency, child, alternate_parent); - child->invalidate_graph(); - scene.set_parent(Scene_Base::Relationship::display, child, parent); - scene.set_parent(Scene_Base::Relationship::dependency, child, parent); - scene.detach_renderable(child); - } catch (...) { - failures.fetch_add(1, std::memory_order_relaxed); - } - } + EXPECT_EQ(scene.renderable_count(), 1u); +} +TEST(dynamic_renderable_lifecycle_test, queued_edits_and_render_submission_preserve_complete_topologies) { + Scene2D_Context<> scene; + std::atomic parent_sequence{}; + std::atomic violations{}; + auto parent = renderive_Owner::make(parent_sequence); + attach_initial(scene, parent); + constexpr int iterations = 200; + for (int index = 0; index < iterations; ++index) { + auto child = renderive_Owner::make(parent_sequence, violations); + scene.edit_renderables([parent, child](auto& editor) { + editor.attach(child); + editor.set_parent(Scene_Base::Relationship::dependency, child, parent); + editor.set_parent(Scene_Base::Relationship::display, child, parent); + }); + scene.edit_renderables([child](auto& editor) { + editor.clear_parents(Scene_Base::Relationship::dependency, child); + editor.clear_parents(Scene_Base::Relationship::display, child); + editor.detach(child); }); } - for (auto& controller : controllers) - controller.join(); - controls_done.store(true, std::memory_order_release); - renderer.join(); scene.render(); scene.wait_for_render(); - EXPECT_EQ(failures.load(std::memory_order_acquire), 0); EXPECT_EQ(violations.load(std::memory_order_acquire), 0); - EXPECT_EQ(scene.renderable_count(), 2u); + EXPECT_EQ(scene.renderable_count(), 1u); } - -TEST(dynamic_renderable_lifecycle_test, - captures_before_and_after_dynamic_membership_keep_both_plan_versions_resolvable) { +TEST(dynamic_renderable_lifecycle_test, captures_before_and_after_runtime_attach_keep_both_plan_versions_resolvable) { Scene2D_Context<> scene; - auto first = std::make_shared(scene); - auto second = std::make_shared(scene); - scene.attach_renderable(first); + auto first = renderive_Owner::make(); + auto second = renderive_Owner::make(); + attach_initial(scene, first); const Capture_Session_Id session_id = scene.capture_frames(2); - scene.render(); scene.wait_for_render(); - scene.attach_renderable(second); + wait_renderable_edit(scene, [second](auto& editor) { + editor.attach(second); + }); scene.render(); scene.wait_for_render(); - const auto session = scene.capture_session(session_id); ASSERT_TRUE(session); ASSERT_EQ(session->frames.size(), 2u); @@ -483,5 +296,4 @@ TEST(dynamic_renderable_lifecycle_test, return node.owner_id == second->renderable_id(); })); } - -} // namespace +} diff --git a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp index 9a9ad64..2646a26 100644 --- a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp +++ b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp @@ -10,13 +10,14 @@ #include #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" +#include "Scene_Test_Helpers.hpp" namespace { class Pipeline_Renderable : public Renderable_Base { public: - explicit Pipeline_Renderable(Scene_Base& scene, bool cached = false) - : Renderable_Base(scene, {.cache_enabled = cached}) {} + explicit Pipeline_Renderable(bool cached = false) + : Renderable_Base({.cache_enabled = cached}) {} std::function prepare_action; std::function paint_action; std::atomic prepare_count{}; @@ -44,7 +45,7 @@ protected: class Dynamic_Renderable : public Renderable_Base { public: - explicit Dynamic_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + explicit Dynamic_Renderable() : Renderable_Base() {} void set_chunk_count(std::size_t count) { chunk_count_ = count; rebuild_render_graph(); @@ -91,12 +92,14 @@ bool has_edge(const Render_Plan& plan, Render_Node_Id from, Render_Node_Id to) { TEST(render_plan_execution_test, dependency_edges_connect_prepare_only_and_layer_edges_connect_composite) { Scene2D_Context<> scene; - auto parent = std::make_shared(scene); - auto child = std::make_shared(scene); - scene.attach_renderable(parent); - scene.attach_renderable(child); - scene.set_parent(Scene_Base::Relationship::dependency, child, parent); - scene.set_parent(Scene_Base::Relationship::display, child, parent); + auto parent = renderive_Owner::make(); + auto child = renderive_Owner::make(); + with_attach_builder(scene, [&](auto& attach) { + attach.attach(parent); + attach.attach(child); + attach.set_parent(Scene_Base::Relationship::dependency, child, parent); + attach.set_parent(Scene_Base::Relationship::display, child, parent); + }); scene.render(); scene.wait_for_render(); @@ -115,8 +118,8 @@ TEST(render_plan_execution_test, dependency_edges_connect_prepare_only_and_layer #ifndef RENDERIVE_TASKFLOW_MOCK TEST(render_plan_execution_test, local_paint_can_run_while_an_unrelated_prepare_is_blocked) { Scene2D_Context<> scene; - auto fast = std::make_shared(scene); - auto slow = std::make_shared(scene); + auto fast = renderive_Owner::make(); + auto slow = renderive_Owner::make(); std::mutex mutex; std::condition_variable condition; bool slow_prepare_started{}; @@ -129,8 +132,7 @@ TEST(render_plan_execution_test, local_paint_can_run_while_an_unrelated_prepare_ condition.wait(lock, [&] { return release_slow; }); }; fast->paint_action = [&] { fast_painted.store(true, std::memory_order_release); }; - scene.attach_renderable(fast); - scene.attach_renderable(slow); + attach_initial(scene, fast, slow); scene.render(); { std::unique_lock lock(mutex); @@ -150,8 +152,8 @@ TEST(render_plan_execution_test, local_paint_can_run_while_an_unrelated_prepare_ TEST(render_plan_execution_test, dependent_prepare_does_not_wait_for_dependency_paint) { Scene2D_Context<> scene; - auto parent = std::make_shared(scene); - auto child = std::make_shared(scene); + auto parent = renderive_Owner::make(); + auto child = renderive_Owner::make(); std::mutex mutex; std::condition_variable condition; bool parent_paint_started{}; @@ -164,9 +166,11 @@ TEST(render_plan_execution_test, dependent_prepare_does_not_wait_for_dependency_ condition.wait(lock, [&] { return release_parent_paint; }); }; child->prepare_action = [&] { child_prepared.store(true, std::memory_order_release); }; - scene.attach_renderable(parent); - scene.attach_renderable(child); - scene.set_parent(Scene_Base::Relationship::dependency, child, parent); + with_attach_builder(scene, [&](auto& attach) { + attach.attach(parent); + attach.attach(child); + attach.set_parent(Scene_Base::Relationship::dependency, child, parent); + }); scene.render(); { std::unique_lock lock(mutex); @@ -187,8 +191,8 @@ TEST(render_plan_execution_test, dependent_prepare_does_not_wait_for_dependency_ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independent_and_prune_nodes) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene, true); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(true); + attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); EXPECT_TRUE(renderable->prepare_cache_valid()); @@ -239,8 +243,8 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen TEST(render_plan_execution_test, logical_node_ids_are_stable_and_retired_ids_are_never_reused) { Scene_Base scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); const auto first = renderable->graph_snapshot(); ASSERT_EQ(first->graph.nodes.size(), 3u); const Render_Node_Id root = first->graph.nodes[0].node_id; @@ -276,8 +280,8 @@ TEST(render_plan_execution_test, logical_node_ids_are_stable_and_retired_ids_are TEST(render_plan_execution_test, capture_off_keeps_abstract_frame_without_execution_timing) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); Abstract_Frame frame; scene.render(frame); scene.wait_for_render(); @@ -286,8 +290,8 @@ TEST(render_plan_execution_test, capture_off_keeps_abstract_frame_without_execut TEST(render_plan_execution_test, measures_capture_off_and_on_cost_separately) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); constexpr std::size_t warmup_count = 32; constexpr std::size_t frame_count = 512; const auto render_frames = [&scene](std::size_t count) { @@ -316,8 +320,8 @@ TEST(render_plan_execution_test, measures_capture_off_and_on_cost_separately) { TEST(render_plan_execution_test, capture_requests_publish_exact_immutable_abstract_frame_snapshots) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene, true); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(true); + attach_initial(scene, renderable); constexpr std::size_t capture_count = 20; const Capture_Session_Id session_id = scene.capture_frames(capture_count); Alternate_Frame alternate_frame; @@ -348,13 +352,13 @@ TEST(render_plan_execution_test, capture_requests_publish_exact_immutable_abstra TEST(render_plan_execution_test, failed_render_discards_abstract_frame_capture_and_retries_ticket) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); + auto renderable = renderive_Owner::make(); std::atomic fail_once{true}; renderable->paint_action = [&] { if (fail_once.exchange(false)) throw std::runtime_error("paint failed"); }; - scene.attach_renderable(renderable); + attach_initial(scene, renderable); const Capture_Session_Id session_id = scene.capture_next_frame(); Alternate_Frame failed_frame; scene.render(failed_frame); diff --git a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp index 2028f66..8068cfd 100644 --- a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp @@ -5,9 +5,10 @@ #include #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" +#include "Scene_Test_Helpers.hpp" static_assert(std::same_as&>().topology_snapshot()), Scene_Base::Topology_Snapshot>); struct Scene2D_Context_Test_Renderable : Renderable_Base { - explicit Scene2D_Context_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} + explicit Scene2D_Context_Test_Renderable() : Renderable_Base({.cache_enabled = false}) {} void prepare(const Prepare_Render_Context&) override { ++render_count; scene_is_2d = dynamic_cast(&scene()) != nullptr; @@ -20,16 +21,18 @@ struct Scene2D_Context_Test_Renderable : Renderable_Base { TEST(scene2d_context_test, swaps_renderable_cache_and_renders_in_background) { Scene2D_Context<> scene; const auto caller_thread = std::this_thread::get_id(); - auto first = std::make_shared(scene); - auto second = std::make_shared(scene); - scene.attach_renderable(first); + auto first = renderive_Owner::make(); + auto second = renderive_Owner::make(); + attach_initial(scene, first); scene.render(); scene.wait_for_render(); EXPECT_EQ(first->render_count.load(), 1); EXPECT_TRUE(first->scene_is_2d); EXPECT_NE(first->render_thread, caller_thread); - scene.detach_renderable(first); - scene.attach_renderable(second); + wait_renderable_edit(scene, [first, second](auto& editor) { + editor.detach(first); + editor.attach(second); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(first->render_count.load(), 1); @@ -37,7 +40,7 @@ TEST(scene2d_context_test, swaps_renderable_cache_and_renders_in_background) { EXPECT_EQ(scene.frame_control_strategy().frame_control_state().publish_revision, 2); } struct Scene2D_Final_Cache_Blocking_Renderable : Renderable_Base { - explicit Scene2D_Final_Cache_Blocking_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} + explicit Scene2D_Final_Cache_Blocking_Renderable() : Renderable_Base({.cache_enabled = false}) {} void prepare(const Prepare_Render_Context&) override { while (!release.load(std::memory_order_acquire)) { started.store(true, std::memory_order_release); @@ -49,8 +52,8 @@ struct Scene2D_Final_Cache_Blocking_Renderable : Renderable_Base { }; TEST(scene2d_context_test, final_color_cache_access_waits_for_render_completion) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); scene.render(); while (!renderable->started.load(std::memory_order_acquire)) { std::this_thread::yield(); @@ -72,7 +75,7 @@ TEST(scene2d_context_test, final_color_cache_access_waits_for_render_completion) EXPECT_TRUE(cache_accessed.load(std::memory_order_acquire)); } struct Scene2D_Cached_Renderable : Renderable_Base { - Scene2D_Cached_Renderable(Scene_Base& scene, std::uint64_t value) : Renderable_Base(scene, {.cache_enabled = true}), value(value) {} + Scene2D_Cached_Renderable(std::uint64_t value) : Renderable_Base({.cache_enabled = true}), value(value) {} void prepare(const Prepare_Render_Context&) override { ++render_count; } @@ -87,13 +90,15 @@ struct Scene2D_Cached_Renderable : Renderable_Base { }; TEST(scene2d_context_test, reattached_cached_renderable_rebuilds_removed_color_cache) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene, 7); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(7); + attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->render_count, 1); - scene.detach_renderable(renderable); - scene.attach_renderable(renderable); + wait_renderable_edit(scene, [renderable](auto& editor) { + editor.detach(renderable); + editor.attach(renderable); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->render_count, 2); @@ -104,19 +109,23 @@ TEST(scene2d_context_test, reattached_cached_renderable_rebuilds_removed_color_c } TEST(scene2d_context_test, dependency_reparent_invalidates_cached_child) { Scene2D_Context<> scene; - auto first_parent = std::make_shared(scene, 1); - auto second_parent = std::make_shared(scene, 2); - auto child = std::make_shared(scene, 3); - scene.attach_renderable(first_parent); - scene.attach_renderable(second_parent); - scene.attach_renderable(child); - scene.set_parent(Scene_Base::Relationship::dependency, child, first_parent); + auto first_parent = renderive_Owner::make(1); + auto second_parent = renderive_Owner::make(2); + auto child = renderive_Owner::make(3); + with_attach_builder(scene, [&](auto& attach) { + attach.attach(first_parent); + attach.attach(second_parent); + attach.attach(child); + attach.set_parent(Scene_Base::Relationship::dependency, child, first_parent); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(first_parent->render_count, 1); EXPECT_EQ(second_parent->render_count, 1); EXPECT_EQ(child->render_count, 1); - scene.set_parent(Scene_Base::Relationship::dependency, child, second_parent); + wait_renderable_edit(scene, [child, second_parent](auto& editor) { + editor.set_parent(Scene_Base::Relationship::dependency, child, second_parent); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(first_parent->render_count, 1); @@ -125,14 +134,16 @@ TEST(scene2d_context_test, dependency_reparent_invalidates_cached_child) { } TEST(scene2d_context_test, multiple_dependency_parents_invalidate_cached_child) { Scene2D_Context<> scene; - auto first_parent = std::make_shared(scene, 1); - auto second_parent = std::make_shared(scene, 2); - auto child = std::make_shared(scene, 3); - scene.attach_renderable(first_parent); - scene.attach_renderable(second_parent); - scene.attach_renderable(child); - scene.set_parent(Scene_Base::Relationship::dependency, child, first_parent); - scene.add_parent(Scene_Base::Relationship::dependency, child, second_parent); + auto first_parent = renderive_Owner::make(1); + auto second_parent = renderive_Owner::make(2); + auto child = renderive_Owner::make(3); + with_attach_builder(scene, [&](auto& attach) { + attach.attach(first_parent); + attach.attach(second_parent); + attach.attach(child); + attach.set_parent(Scene_Base::Relationship::dependency, child, first_parent); + attach.add_parent(Scene_Base::Relationship::dependency, child, second_parent); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(child->render_count, 1); @@ -154,31 +165,38 @@ TEST(scene2d_context_test, multiple_dependency_parents_invalidate_cached_child) } TEST(scene2d_context_test, final_color_cache_callback_can_reenter_scene_control_api) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene, 9); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(9); + attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); scene.with_final_color_cache([&](const Recording_Color_Cache&) { - scene.set_renderable_configuration(renderable, {.cache_enabled = false}); + renderable->set_configuration({.cache_enabled = false}); }); EXPECT_FALSE(renderable->configuration().cache_enabled); } TEST(scene2d_context_test, detach_dependency_parent_invalidates_promoted_cached_child) { Scene2D_Context<> scene; - auto grandparent = std::make_shared(scene, 1); - auto parent = std::make_shared(scene, 2); - auto child = std::make_shared(scene, 3); - scene.attach_renderable(grandparent); - scene.attach_renderable(parent); - scene.attach_renderable(child); - scene.set_parent(Scene_Base::Relationship::dependency, parent, grandparent); - scene.set_parent(Scene_Base::Relationship::dependency, child, parent); + auto grandparent = renderive_Owner::make(1); + auto parent = renderive_Owner::make(2); + auto child = renderive_Owner::make(3); + with_attach_builder(scene, [&](auto& attach) { + attach.attach(grandparent); + attach.attach(parent); + attach.attach(child); + attach.set_parent(Scene_Base::Relationship::dependency, parent, grandparent); + attach.set_parent(Scene_Base::Relationship::dependency, child, parent); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(grandparent->render_count, 1); EXPECT_EQ(parent->render_count, 1); EXPECT_EQ(child->render_count, 1); - scene.detach_renderable(parent); + wait_renderable_edit(scene, [grandparent, parent, child](auto& editor) { + editor.clear_parents(Scene_Base::Relationship::dependency, child); + editor.clear_parents(Scene_Base::Relationship::dependency, parent); + editor.set_parent(Scene_Base::Relationship::dependency, child, grandparent); + editor.detach(parent); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(grandparent->render_count, 1); @@ -206,16 +224,16 @@ TEST(scene2d_context_test, failed_attach_leaves_renderable_fully_detached) { Scene2D_Attach_Throwing_Cache::construction_count = 0; Scene2D_Attach_Throwing_Cache::throw_on_construction = 2; Scene2D_Context, Scene2D_Attach_Throwing_Cache> scene; - auto renderable = std::make_shared(scene); - EXPECT_THROW(scene.attach_renderable(renderable).wait(), std::runtime_error); + auto renderable = renderive_Owner::make(); + EXPECT_THROW(attach_initial(scene, renderable), std::runtime_error); EXPECT_EQ(scene.renderable_count(), 0); EXPECT_TRUE(scene.topology_snapshot().renderables.empty()); Scene2D_Attach_Throwing_Cache::throw_on_construction = 0; - EXPECT_NO_THROW(scene.attach_renderable(renderable).wait()); + EXPECT_NO_THROW(attach_initial(scene, renderable)); EXPECT_EQ(scene.renderable_count(), 1); } struct Scene2D_Viewport_Snapshot_Renderable : Renderable_Base { - explicit Scene2D_Viewport_Snapshot_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + explicit Scene2D_Viewport_Snapshot_Renderable() : Renderable_Base() {} void prepare(const Prepare_Render_Context& context) override { viewport = context.frame.viewport; } @@ -223,8 +241,8 @@ struct Scene2D_Viewport_Snapshot_Renderable : Renderable_Base { }; TEST(scene2d_context_test, published_scene_viewport_is_frozen_for_render) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); scene.Scene_State_Strategy::set<&Scene2D_State::viewport>(Frame_Viewport{320, 180}); scene.publish_frame_state(); scene.Scene_State_Strategy::set<&Scene2D_State::viewport>(Frame_Viewport{640, 360}); diff --git a/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp index 068c922..ee37472 100644 --- a/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp @@ -18,8 +18,8 @@ struct Scene2D_Render_Order_State { std::vector render_order; }; struct Scene2D_Render_Order_Renderable : Renderable_Base { - Scene2D_Render_Order_Renderable(Scene_Base& scene, Scene2D_Render_Order_State& state, std::uint64_t id) - : Renderable_Base(scene, {.cache_enabled = false}), state(&state), id(id) {} + Scene2D_Render_Order_Renderable(Scene2D_Render_Order_State& state, std::uint64_t id) + : Renderable_Base({.cache_enabled = false}), state(&state), id(id) {} void prepare(const Prepare_Render_Context&) override { state->append(id); } @@ -35,12 +35,15 @@ struct Scene2D_Render_Order_Renderable : Renderable_Base { TEST(scene2d_render_order_test, separates_dependency_order_from_display_order) { Scene2D_Context<> scene; Scene2D_Render_Order_State state; - auto spectrum = std::make_shared(scene, state, 2); - auto axis = std::make_shared(scene, state, 1); - scene.attach_renderable(spectrum); - scene.attach_renderable(axis); - scene.set_parent(Scene_Base::Relationship::dependency, spectrum, axis); - scene.set_parent(Scene_Base::Relationship::display, axis, spectrum); + auto spectrum = renderive_Owner::make(state, 2); + auto axis = renderive_Owner::make(state, 1); + { + auto attach = scene.attach_builder(); + attach.attach(spectrum); + attach.attach(axis); + attach.set_parent(Scene_Base::Relationship::dependency, spectrum, axis); + attach.set_parent(Scene_Base::Relationship::display, axis, spectrum); + } scene.render(); scene.wait_for_render(); EXPECT_EQ(state.snapshot(), (std::vector{1, 2})); @@ -55,16 +58,19 @@ TEST(scene2d_render_order_test, separates_dependency_order_from_display_order) { TEST(scene2d_render_order_test, shared_overlay_waits_for_every_display_parent) { Scene2D_Context<> scene; Scene2D_Render_Order_State state; - auto first_plot = std::make_shared(scene, state, 2); - auto axis = std::make_shared(scene, state, 1); - auto second_plot = std::make_shared(scene, state, 3); - scene.attach_renderable(first_plot); - scene.attach_renderable(axis); - scene.attach_renderable(second_plot); - scene.add_parent(Scene_Base::Relationship::dependency, first_plot, axis); - scene.add_parent(Scene_Base::Relationship::dependency, second_plot, axis); - scene.add_parent(Scene_Base::Relationship::display, axis, first_plot); - scene.add_parent(Scene_Base::Relationship::display, axis, second_plot); + auto first_plot = renderive_Owner::make(state, 2); + auto axis = renderive_Owner::make(state, 1); + auto second_plot = renderive_Owner::make(state, 3); + { + auto attach = scene.attach_builder(); + attach.attach(first_plot); + attach.attach(axis); + attach.attach(second_plot); + attach.add_parent(Scene_Base::Relationship::dependency, first_plot, axis); + attach.add_parent(Scene_Base::Relationship::dependency, second_plot, axis); + attach.add_parent(Scene_Base::Relationship::display, axis, first_plot); + attach.add_parent(Scene_Base::Relationship::display, axis, second_plot); + } scene.render(); scene.wait_for_render(); const auto render_order = state.snapshot(); diff --git a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp index 56560bf..af51252 100644 --- a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp @@ -3,8 +3,9 @@ #include #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" +#include "Scene_Test_Helpers.hpp" struct Scene3D_Context_Test_Renderable : Renderable_Base { - explicit Scene3D_Context_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + explicit Scene3D_Context_Test_Renderable() : Renderable_Base() {} void prepare(const Prepare_Render_Context&) override { scene_is_3d = dynamic_cast(&scene()) != nullptr; ++render_count; @@ -14,15 +15,15 @@ struct Scene3D_Context_Test_Renderable : Renderable_Base { }; TEST(scene3d_context_test, uses_scene_3d_base_contract) { Scene3D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->render_count.load(), 1); EXPECT_TRUE(renderable->scene_is_3d); } struct Scene3D_Dependency_Cache_Test_Renderable : Renderable_Base { - explicit Scene3D_Dependency_Cache_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = true}) {} + explicit Scene3D_Dependency_Cache_Test_Renderable() : Renderable_Base({.cache_enabled = true}) {} void prepare(const Prepare_Render_Context&) override { ++render_count; } @@ -30,14 +31,16 @@ struct Scene3D_Dependency_Cache_Test_Renderable : Renderable_Base { }; TEST(scene3d_context_test, propagates_cache_invalidation_through_unsorted_dependency_chain) { Scene3D_Context<> scene; - auto leaf = std::make_shared(scene); - auto middle = std::make_shared(scene); - auto root = std::make_shared(scene); - scene.attach_renderable(leaf); - scene.attach_renderable(middle); - scene.attach_renderable(root); - scene.set_parent(Scene_Base::Relationship::dependency, leaf, middle); - scene.set_parent(Scene_Base::Relationship::dependency, middle, root); + auto leaf = renderive_Owner::make(); + auto middle = renderive_Owner::make(); + auto root = renderive_Owner::make(); + with_attach_builder(scene, [&](auto& attach) { + attach.attach(leaf); + attach.attach(middle); + attach.attach(root); + attach.set_parent(Scene_Base::Relationship::dependency, leaf, middle); + attach.set_parent(Scene_Base::Relationship::dependency, middle, root); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(root->render_count, 1); @@ -50,22 +53,30 @@ TEST(scene3d_context_test, propagates_cache_invalidation_through_unsorted_depend EXPECT_EQ(middle->render_count, 2); EXPECT_EQ(leaf->render_count, 2); } -TEST(scene3d_context_test, detach_parent_promotes_children_and_invalidates_dependency_cache) { +TEST(scene3d_context_test, runtime_edit_explicitly_reparents_children_before_detach) { Scene3D_Context<> scene; - auto grandparent = std::make_shared(scene); - auto parent = std::make_shared(scene); - auto child = std::make_shared(scene); - scene.attach_renderable(grandparent); - scene.attach_renderable(parent); - scene.attach_renderable(child); - scene.set_parent(Scene_Base::Relationship::display, parent, grandparent); - scene.set_parent(Scene_Base::Relationship::display, child, parent); - scene.set_parent(Scene_Base::Relationship::dependency, parent, grandparent); - scene.set_parent(Scene_Base::Relationship::dependency, child, parent); + auto grandparent = renderive_Owner::make(); + auto parent = renderive_Owner::make(); + auto child = renderive_Owner::make(); + with_attach_builder(scene, [&](auto& attach) { + attach.attach(grandparent); + attach.attach(parent); + attach.attach(child); + attach.set_parent(Scene_Base::Relationship::display, parent, grandparent); + attach.set_parent(Scene_Base::Relationship::display, child, parent); + attach.set_parent(Scene_Base::Relationship::dependency, parent, grandparent); + attach.set_parent(Scene_Base::Relationship::dependency, child, parent); + }); scene.render(); scene.wait_for_render(); EXPECT_EQ(child->render_count, 1); - scene.detach_renderable(parent).wait(); + wait_renderable_edit(scene, [grandparent, parent, child](auto& editor) { + editor.set_parent(Scene_Base::Relationship::display, child, grandparent); + editor.set_parent(Scene_Base::Relationship::dependency, child, grandparent); + editor.clear_parents(Scene_Base::Relationship::display, parent); + editor.clear_parents(Scene_Base::Relationship::dependency, parent); + editor.detach(parent); + }); const auto topology = scene.topology_snapshot(); bool display_checked{}; bool dependency_checked{}; diff --git a/Kernel/tests/renderive/scene/Scene_Memory_Resource_Test.cpp b/Kernel/tests/renderive/scene/Scene_Memory_Resource_Test.cpp index 35c37c2..be49d7f 100644 --- a/Kernel/tests/renderive/scene/Scene_Memory_Resource_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene_Memory_Resource_Test.cpp @@ -8,6 +8,7 @@ #include "renderive/real_time_data/Real_Time_Data.hpp" #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" +#include "Scene_Test_Helpers.hpp" class Scene_Memory_Resource_Test_Resource : public std::pmr::memory_resource { public: std::size_t allocation_count() const noexcept { @@ -32,7 +33,8 @@ private: std::atomic deallocation_count_{}; }; struct Scene_Memory_Resource_Test_Renderable : Renderable_Base { - explicit Scene_Memory_Resource_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} + explicit Scene_Memory_Resource_Test_Renderable(std::pmr::memory_resource& memory_resource = *std::pmr::get_default_resource()) + : Renderable_Base({.cache_enabled = false}, memory_resource) {} void build_prepare_graph(Renderable_Graph_Builder& graph) override { auto first = graph.emplace("first", "prepare_memory_resource_test", [](const Prepare_Render_Context&) {}); @@ -55,31 +57,24 @@ struct Scene_Memory_Resource_Test_Property_Product { Scene_Memory_Resource_Test_Property_Product(const Scene_Memory_Resource_Test_Properties& properties, int offset) : value(properties.value + offset) {} int value; }; -TEST(scene_memory_resource_test, propagates_thread_safe_scene_memory_domain) { +TEST(scene_memory_resource_test, renderable_builder_can_use_scene_memory_domain_explicitly) { Scene_Memory_Resource_Test_Resource upstream; Scene2D_Context<> scene(upstream); EXPECT_EQ(&scene.upstream_memory_resource(), &upstream); EXPECT_NE(&scene.memory_resource(), &upstream); - auto factory_renderable = scene.make_renderable(); - auto independent_renderable = std::make_shared(scene); - EXPECT_EQ(&factory_renderable->memory_resource(), &scene.memory_resource()); - EXPECT_EQ(&independent_renderable->memory_resource(), &scene.memory_resource()); - scene.attach_renderable(factory_renderable); - scene.attach_renderable(independent_renderable); + auto renderable = renderive_Owner::make(scene.memory_resource()); + EXPECT_EQ(&renderable->memory_resource(), &scene.memory_resource()); + attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); EXPECT_GT(upstream.allocation_count(), 0); } -TEST(scene_memory_resource_test, keeps_factory_memory_domain_alive_until_renderable_release) { +TEST(scene_memory_resource_test, explicit_scene_memory_domain_remains_valid_while_scene_is_alive) { Scene_Memory_Resource_Test_Resource upstream; - std::shared_ptr renderable; - { - Scene2D_Context<> scene(upstream); - renderable = scene.make_renderable(); - renderable->build_graph_for_test(); - } - renderable.reset(); - EXPECT_GT(upstream.deallocation_count(), 0); + Scene2D_Context<> scene(upstream); + auto renderable = renderive_Owner::make(scene.memory_resource()); + renderable->build_graph_for_test(); + EXPECT_EQ(&renderable->memory_resource(), &scene.memory_resource()); } TEST(scene_memory_resource_test, supports_independent_resource_aware_components) { Scene_Memory_Resource_Test_Resource memory_resource; @@ -101,17 +96,10 @@ TEST(scene_memory_resource_test, supports_independent_resource_aware_components) EXPECT_EQ(unique_product->value, 8); EXPECT_GT(memory_resource.allocation_count(), 0); } -TEST(scene_memory_resource_test, keeps_independent_renderable_memory_domain_alive_until_renderable_release) { - Scene_Memory_Resource_Test_Resource upstream; - std::shared_ptr renderable; - { - Scene2D_Context<> scene(upstream); - renderable = std::make_shared(scene); - renderable->build_graph_for_test(); - } - auto& memory_resource = renderable->memory_resource(); - void* storage = memory_resource.allocate(32, alignof(std::max_align_t)); - memory_resource.deallocate(storage, 32, alignof(std::max_align_t)); - renderable.reset(); - EXPECT_GT(upstream.deallocation_count(), 0); +TEST(scene_memory_resource_test, supports_explicit_independent_renderable_memory_resource) { + Scene_Memory_Resource_Test_Resource memory_resource; + auto renderable = renderive_Owner::make(memory_resource); + renderable->build_graph_for_test(); + EXPECT_EQ(&renderable->memory_resource(), &memory_resource); + EXPECT_GT(memory_resource.allocation_count(), 0); } diff --git a/Kernel/tests/renderive/scene/Scene_Test_Helpers.hpp b/Kernel/tests/renderive/scene/Scene_Test_Helpers.hpp new file mode 100644 index 0000000..434e4f6 --- /dev/null +++ b/Kernel/tests/renderive/scene/Scene_Test_Helpers.hpp @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include "renderive/scene/base/Scene_Base.hpp" +template +void attach_initial(Scene_Base& scene, const Owners&... owners) { + auto builder = scene.attach_builder(); + (builder.attach(owners), ...); +} +template +void with_attach_builder(Scene_Base& scene, Edit&& edit) { + auto builder = scene.attach_builder(); + edit(builder); +} +template +void wait_renderable_edit(Scene_Base& scene, Edit&& edit) { + std::promise completed; + auto future = completed.get_future(); + scene.edit_renderables([edit = std::forward(edit), &completed](Scene_Base::Renderable_Editor& editor) mutable { + edit(editor); + completed.set_value(); + }); + future.wait(); +} diff --git a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp index 52b77b4..687aec8 100644 --- a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp +++ b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp @@ -9,11 +9,11 @@ #include #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.hpp" +#include "renderive/scene/Scene_Test_Helpers.hpp" namespace { template bool wait_until(Predicate&& predicate) { - const auto deadline = std::chrono::steady_clock::now() + - std::chrono::seconds(2); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (!predicate()) { if (std::chrono::steady_clock::now() >= deadline) return false; @@ -23,7 +23,7 @@ bool wait_until(Predicate&& predicate) { } } struct Scene_Base_Test_Renderable : Renderable_Base { - explicit Scene_Base_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + Scene_Base_Test_Renderable() = default; void prepare(const Prepare_Render_Context& context) override { render_sequence = context.frame.render_sequence; ++render_count; @@ -31,53 +31,23 @@ struct Scene_Base_Test_Renderable : Renderable_Base { std::uint64_t render_sequence{}; int render_count{}; }; -TEST(scene_base_test, publishes_renderable_collection_before_background_render) { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); - scene.render(); - scene.wait_for_render(); - EXPECT_EQ(renderable->render_count, 1); - EXPECT_EQ(renderable->render_sequence, 1); - EXPECT_EQ(scene.renderable_count(), 1); -} struct Scene_Base_Throwing_Renderable : Renderable_Base { - explicit Scene_Base_Throwing_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} + Scene_Base_Throwing_Renderable() : Renderable_Base({.cache_enabled = false}) {} void prepare(const Prepare_Render_Context&) override { throw std::runtime_error("render failed"); } }; -TEST(scene_base_test, wait_for_render_propagates_background_render_failure) { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); - scene.render(); - EXPECT_THROW(scene.wait_for_render(), std::runtime_error); -} -TEST(scene_base_test, scene_destruction_does_not_throw_after_background_render_failure) { - { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); - scene.render(); - } - EXPECT_TRUE(true); -} struct Scene_Base_Blocking_Renderable : Renderable_Base { - explicit Scene_Base_Blocking_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} + Scene_Base_Blocking_Renderable() : Renderable_Base({.cache_enabled = false}) {} void prepare(const Prepare_Render_Context&) override { std::unique_lock lock(mutex); rendering = true; condition.notify_all(); - condition.wait(lock, [this] { - return released; - }); + condition.wait(lock, [this] { return released; }); } void wait_rendering() { std::unique_lock lock(mutex); - condition.wait(lock, [this] { - return rendering; - }); + condition.wait(lock, [this] { return rendering; }); } void release() { std::lock_guard lock(mutex); @@ -89,217 +59,189 @@ struct Scene_Base_Blocking_Renderable : Renderable_Base { bool rendering{}; bool released{}; }; -TEST(scene_base_test, scene_model_mutation_does_not_wait_for_background_render) { +TEST(scene_base_test, attach_builder_is_synchronous_before_runtime) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + { + auto builder = scene.attach_builder(); + builder.attach(renderable); + EXPECT_EQ(scene.renderable_count(), 1u); + } + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->render_count, 1); + EXPECT_EQ(renderable->render_sequence, 1); +} +TEST(scene_base_test, attach_builder_can_build_complete_initial_topology) { + Scene2D_Context<> scene; + auto parent = renderive_Owner::make(); + auto child = renderive_Owner::make(); + { + auto builder = scene.attach_builder(); + builder.attach(parent); + builder.attach(child); + builder.set_parent(Scene_Base::Relationship::display, child, parent); + builder.set_parent(Scene_Base::Relationship::dependency, child, parent); + } + const auto topology = scene.topology_snapshot(); + EXPECT_EQ(topology.renderables.size(), 2u); + EXPECT_TRUE(std::ranges::any_of(topology.display, [&](const auto& relation) { + return relation.child.get() == child.get() && relation.parent.get() == parent.get(); + })); + EXPECT_TRUE(std::ranges::any_of(topology.dependency, [&](const auto& relation) { + return relation.child.get() == child.get() && relation.parent.get() == parent.get(); + })); +} +TEST(scene_base_test, attach_builder_rejects_null_renderable) { + Scene2D_Context<> scene; + auto builder = scene.attach_builder(); + EXPECT_THROW(builder.attach({}), std::invalid_argument); +} +TEST(scene_base_test, attach_builder_is_rejected_after_runtime_starts) { + Scene2D_Context<> scene; + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); + scene.render(); + scene.wait_for_render(); + EXPECT_THROW(scene.attach_builder(), std::logic_error); +} +TEST(scene_base_test, edit_renderables_waits_for_inflight_frame) { + Scene2D_Context<> scene; + auto blocking = renderive_Owner::make(); + auto added = renderive_Owner::make(); + attach_initial(scene, blocking); + scene.render(); + blocking->wait_rendering(); + std::atomic edited{}; + scene.edit_renderables([added, &edited](auto& editor) { + editor.attach(added); + edited.store(true, std::memory_order_release); + }); + EXPECT_FALSE(edited.load(std::memory_order_acquire)); + blocking->release(); + scene.wait_for_render(); + EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); })); + EXPECT_EQ(added->render_count, 0); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(added->render_count, 1); +} +TEST(scene_base_test, one_runtime_edit_can_rewire_and_detach_without_internal_repair) { + Scene2D_Context<> scene; + auto first_parent = renderive_Owner::make(); + auto second_parent = renderive_Owner::make(); + auto child = renderive_Owner::make(); + with_attach_builder(scene, [&](auto& builder) { + builder.attach(first_parent); + builder.attach(second_parent); + builder.attach(child); + builder.set_parent(Scene_Base::Relationship::display, child, first_parent); + builder.set_parent(Scene_Base::Relationship::dependency, child, first_parent); + }); + wait_renderable_edit(scene, [first_parent, second_parent, child](auto& editor) { + editor.set_parent(Scene_Base::Relationship::display, child, second_parent); + editor.set_parent(Scene_Base::Relationship::dependency, child, second_parent); + editor.detach(first_parent); + }); + const auto topology = scene.topology_snapshot(); + EXPECT_EQ(topology.renderables.size(), 2u); + EXPECT_TRUE(std::ranges::any_of(topology.display, [&](const auto& relation) { + return relation.child.get() == child.get() && relation.parent.get() == second_parent.get(); + })); +} +TEST(scene_base_test, detach_releases_scene_ownership_after_edit_applies) { + Scene2D_Context<> scene; + auto renderable = renderive_Owner::make(); + std::weak_ptr weak = renderable.share(); + attach_initial(scene, renderable); + wait_renderable_edit(scene, [renderable](auto& editor) { + editor.detach(renderable); + }); + renderable.reset(); + EXPECT_TRUE(weak.expired()); + EXPECT_EQ(scene.renderable_count(), 0u); +} +TEST(scene_base_test, topology_snapshot_retains_renderable_lifetime) { + Scene2D_Context<> scene; + auto renderable = renderive_Owner::make(); + std::weak_ptr weak = renderable.share(); + attach_initial(scene, renderable); + const auto snapshot = scene.topology_snapshot(); + wait_renderable_edit(scene, [renderable](auto& editor) { + editor.detach(renderable); + }); + renderable.reset(); + EXPECT_FALSE(weak.expired()); + EXPECT_EQ(snapshot.renderables.size(), 1u); + EXPECT_EQ(snapshot.renderables.front().get(), weak.lock().get()); +} +TEST(scene_base_test, non_structural_configuration_update_does_not_wait_for_render) { + Scene2D_Context<> scene; + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); scene.render(); renderable->wait_rendering(); - std::atomic mutation_finished{}; - std::thread mutation([&] { - scene.set_renderable_configuration(renderable, {.cache_enabled = true}); - mutation_finished.store(true, std::memory_order_release); + std::atomic updated{}; + std::thread writer([&] { + renderable->set_configuration({.cache_enabled = true}); + updated.store(true, std::memory_order_release); }); - std::this_thread::yield(); - EXPECT_TRUE(wait_until([&] { - return mutation_finished.load(std::memory_order_acquire); - })); + EXPECT_TRUE(wait_until([&] { return updated.load(std::memory_order_acquire); })); renderable->release(); - mutation.join(); - EXPECT_TRUE(mutation_finished.load(std::memory_order_acquire)); + writer.join(); + scene.wait_for_render(); EXPECT_TRUE(renderable->configuration().cache_enabled); } -TEST(scene_base_test, concurrent_render_and_topology_updates_remain_serialized) { +TEST(scene_base_test, concurrent_render_and_runtime_structure_edits_are_serialized_by_worker) { Scene2D_Context<> scene; - auto parent = std::make_shared(scene); - auto child = std::make_shared(scene); - scene.attach_renderable(parent); - scene.attach_renderable(child); + auto parent = renderive_Owner::make(); + auto child = renderive_Owner::make(); + attach_initial(scene, parent, child); std::thread renderer([&] { - for (int index = 0; index < 100; ++index) { + for (int index = 0; index < 50; ++index) { scene.render(); scene.wait_for_render(); } }); - std::thread mutator([&] { - for (int index = 0; index < 100; ++index) { - if (index % 2 == 0) { - scene.set_parent(Scene_Base::Relationship::dependency, child, parent); - scene.set_parent(Scene_Base::Relationship::display, child, parent); - } else { - scene.clear_parents(Scene_Base::Relationship::dependency, child); - scene.clear_parents(Scene_Base::Relationship::display, child); - } - scene.set_renderable_configuration(child, {.cache_enabled = index % 2 == 0}); + std::thread editor([&] { + for (int index = 0; index < 50; ++index) { + scene.edit_renderables([parent, child, index](auto& edit) { + if (index % 2 == 0) { + edit.set_parent(Scene_Base::Relationship::dependency, child, parent); + edit.set_parent(Scene_Base::Relationship::display, child, parent); + } else { + edit.clear_parents(Scene_Base::Relationship::dependency, child); + edit.clear_parents(Scene_Base::Relationship::display, child); + } + }); } }); renderer.join(); - mutator.join(); - EXPECT_EQ(scene.renderable_count(), 2); -} -TEST(scene_base_test, detach_releases_renderable_from_the_live_model) { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - std::weak_ptr weak = renderable; - scene.attach_renderable(renderable); + editor.join(); scene.render(); scene.wait_for_render(); - scene.detach_renderable(renderable).wait(); - renderable.reset(); - EXPECT_TRUE(weak.expired()); - EXPECT_EQ(scene.renderable_count(), 0); -} -TEST(scene_base_test, attach_rejects_null_renderable) { - Scene2D_Context<> scene; - EXPECT_THROW(scene.attach_renderable({}), std::invalid_argument); -} -TEST(scene_base_test, topology_rejects_unattached_renderables) { - Scene2D_Context<> scene; - auto parent = std::make_shared(scene); - auto child = std::make_shared(scene); - scene.attach_renderable(child); - EXPECT_THROW(scene.set_parent(Scene_Base::Relationship::dependency, child, parent).wait(), std::invalid_argument); - EXPECT_THROW(scene.set_parent(Scene_Base::Relationship::display, child, parent).wait(), std::invalid_argument); - scene.attach_renderable(parent); - auto detached_child = std::make_shared(scene); - EXPECT_THROW(scene.set_parent(Scene_Base::Relationship::dependency, detached_child, parent).wait(), std::invalid_argument); -} -TEST(scene_base_test, setting_same_topology_parent_is_a_noop) { - Scene2D_Context<> scene; - auto parent = std::make_shared(scene); - auto child = std::make_shared(scene); - scene.attach_renderable(parent); - scene.attach_renderable(child); - scene.set_parent(Scene_Base::Relationship::dependency, child, parent).wait(); - const auto revision = child->prepare_revision(); - EXPECT_NO_THROW(scene.set_parent(Scene_Base::Relationship::dependency, child, parent).wait()); - EXPECT_EQ(child->prepare_revision(), revision); - const auto topology = scene.topology_snapshot(); - EXPECT_EQ(topology.dependency.size(), 2); - bool found{}; - for (const auto& relation : topology.dependency) { - if (relation.child.get() == child.get()) { - EXPECT_EQ(relation.parent.get(), parent.get()); - found = true; - } - } - EXPECT_TRUE(found); -} -TEST(scene_base_test, display_graph_rejects_cycles) { - Scene2D_Context<> scene; - auto first = std::make_shared(scene); - auto second = std::make_shared(scene); - auto third = std::make_shared(scene); - scene.attach_renderable(first); - scene.attach_renderable(second); - scene.attach_renderable(third); - scene.set_parent(Scene_Base::Relationship::display, second, first); - scene.add_parent(Scene_Base::Relationship::display, third, second); - EXPECT_THROW(scene.add_parent(Scene_Base::Relationship::display, first, third).wait(), std::invalid_argument); -} -TEST(scene_base_test, mutation_callback_receives_apply_failure) { - Scene2D_Context<> scene; - auto first = std::make_shared(scene); - auto second = std::make_shared(scene); - scene.attach_renderable(first).wait(); - scene.attach_renderable(second).wait(); - scene.set_parent(Scene_Base::Relationship::display, second, first).wait(); - std::atomic callback_called{}; - std::atomic callback_failed{}; - auto mutation = scene.set_parent(Scene_Base::Relationship::display, first, second); - mutation.then([&](std::exception_ptr exception) { - callback_failed.store(static_cast(exception), std::memory_order_release); - callback_called.store(true, std::memory_order_release); - }); - EXPECT_THROW(mutation.wait(), std::invalid_argument); - EXPECT_TRUE(wait_until([&] { return callback_called.load(std::memory_order_acquire); })); - EXPECT_TRUE(callback_failed.load(std::memory_order_acquire)); -} -TEST(scene_base_test, atomic_attach_can_place_existing_display_children) { - Scene2D_Context<> scene; - auto parent = std::make_shared(scene); - auto axis = std::make_shared(scene); - auto renderable = std::make_shared(scene); - scene.attach_renderable(parent); - scene.attach_renderable(axis); - auto mutation = scene.attach_renderable(renderable, { - .display_parents = {parent}, - .display_children = {axis}, - .dependency_parents = {parent, axis} - }); - mutation.wait(); - const auto topology = scene.topology_snapshot(); - const auto contains = [](const auto& relationships, const auto& child, - const auto& parent_value) { - return std::ranges::any_of(relationships, [&](const auto& relationship) { - return relationship.child.get() == child.get() && - relationship.parent.get() == parent_value.get(); - }); - }; - EXPECT_TRUE(contains(topology.display, renderable, parent)); - EXPECT_TRUE(contains(topology.display, axis, renderable)); - EXPECT_TRUE(contains(topology.dependency, renderable, parent)); - EXPECT_TRUE(contains(topology.dependency, renderable, axis)); -} -TEST(scene_base_test, failed_atomic_attach_rolls_back_every_topology) { - Scene2D_Context<> scene; - auto parent = std::make_shared(scene); - auto renderable = std::make_shared(scene); - scene.attach_renderable(parent); - - EXPECT_THROW(scene.attach_renderable(renderable, { - .display_parents = {parent}, - .display_children = {parent}, - .dependency_parents = {parent} - }).wait(), std::invalid_argument); - EXPECT_EQ(scene.renderable_count(), 1u); - EXPECT_NO_THROW(scene.attach_renderable(renderable, { - .display_parents = {parent}, - .dependency_parents = {parent} - }).wait()); EXPECT_EQ(scene.renderable_count(), 2u); } -TEST(scene_base_test, topology_snapshot_retains_renderable_lifetime) { +TEST(scene_base_test, wait_for_render_propagates_background_render_failure) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - std::weak_ptr weak = renderable; - scene.attach_renderable(renderable).wait(); - const auto snapshot = scene.topology_snapshot(); - scene.detach_renderable(renderable).wait(); - renderable.reset(); - EXPECT_FALSE(weak.expired()); - EXPECT_EQ(snapshot.renderables.size(), 1); - EXPECT_EQ(snapshot.renderables.front().get(), weak.lock().get()); + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); + scene.render(); + EXPECT_THROW(scene.wait_for_render(), std::runtime_error); } -TEST(scene_base_test, topology_snapshot_is_safe_during_topology_updates) { - Scene2D_Context<> scene; - auto parent = std::make_shared(scene); - auto child = std::make_shared(scene); - scene.attach_renderable(parent); - scene.attach_renderable(child).wait(); - std::thread writer([&] { - for (int index = 0; index < 500; ++index) { - if (index % 2 == 0) { - scene.set_parent(Scene_Base::Relationship::dependency, child, parent); - scene.set_parent(Scene_Base::Relationship::display, child, parent); - } else { - scene.clear_parents(Scene_Base::Relationship::dependency, child); - scene.clear_parents(Scene_Base::Relationship::display, child); - } - } - }); - for (int index = 0; index < 500; ++index) { - const auto snapshot = scene.topology_snapshot(); - EXPECT_EQ(snapshot.renderables.size(), 2); - EXPECT_EQ(snapshot.display.size(), 2); - EXPECT_EQ(snapshot.dependency.size(), 2); +TEST(scene_base_test, scene_destruction_does_not_throw_after_background_render_failure) { + { + Scene2D_Context<> scene; + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); + scene.render(); } - writer.join(); + EXPECT_TRUE(true); } TEST(scene_base_test, all_waiters_receive_the_same_render_failure) { Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(); + attach_initial(scene, renderable); scene.render(); std::atomic failures{}; auto waiter = [&] { @@ -315,74 +257,3 @@ TEST(scene_base_test, all_waiters_receive_the_same_render_failure) { second.join(); EXPECT_EQ(failures.load(), 2); } -struct Scene_Base_Throw_Once_Renderable : Renderable_Base { - explicit Scene_Base_Throw_Once_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} - void prepare(const Prepare_Render_Context&) override { - if (render_count.fetch_add(1, std::memory_order_acq_rel) == 0) { - throw std::runtime_error("first render failed"); - } - } - std::atomic render_count{}; -}; -TEST(scene_base_test, unobserved_render_failure_survives_next_successful_submission) { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); - scene.render(); - scene.render(); - EXPECT_THROW(scene.wait_for_render(), std::runtime_error); - EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 2); - scene.render(); - EXPECT_NO_THROW(scene.wait_for_render()); - EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 3); -} -struct Scene_Base_Render_Execution_Renderable : Renderable_Base { - explicit Scene_Base_Render_Execution_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} - void prepare(const Prepare_Render_Context&) override { - scene().wait_for_render(); - wait_returned.store(true, std::memory_order_release); - try { - scene().render(); - } catch (const std::logic_error&) { - nested_render_rejected.store(true, std::memory_order_release); - } - try { - scene().set_renderable_configuration(shared_from_this(), - {.cache_enabled = true}); - mutation_completed.store(true, std::memory_order_release); - } catch (...) { - } - } - std::atomic wait_returned{}; - std::atomic nested_render_rejected{}; - std::atomic mutation_completed{}; -}; - -TEST(scene_base_test, attach_builder_is_synchronous_before_runtime) { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - { - auto builder = scene.attach_builder(); - builder.attach(renderable); - EXPECT_EQ(scene.renderable_count(), 1u); - } - scene.render(); - scene.wait_for_render(); - EXPECT_EQ(renderable->render_count, 1); -} -TEST(scene_base_test, attach_builder_is_rejected_after_runtime_starts) { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable).wait(); - EXPECT_THROW(scene.attach_builder(), std::logic_error); -} -TEST(scene_base_test, taskflow_execution_can_mutate_live_model_but_cannot_submit_nested_render) { - Scene2D_Context<> scene; - auto renderable = std::make_shared(scene); - scene.attach_renderable(renderable); - scene.render(); - scene.wait_for_render(); - EXPECT_TRUE(renderable->wait_returned.load(std::memory_order_acquire)); - EXPECT_TRUE(renderable->nested_render_rejected.load(std::memory_order_acquire)); - EXPECT_TRUE(renderable->mutation_completed.load(std::memory_order_acquire)); -} diff --git a/Kernel/tests/renderive/state/State_Test_Types.hpp b/Kernel/tests/renderive/state/State_Test_Types.hpp index c2176b0..2627718 100644 --- a/Kernel/tests/renderive/state/State_Test_Types.hpp +++ b/Kernel/tests/renderive/state/State_Test_Types.hpp @@ -7,20 +7,19 @@ class State_Render_State_Reader { class Reader : public Renderable_Base { public: - explicit Reader(Scene_Base& scene) : Renderable_Base(scene) {} + explicit Reader() : Renderable_Base() {} template auto read(const Source& source) const { return render_state_view().get(source); } }; public: - State_Render_State_Reader() : reader_(scene_) {} + State_Render_State_Reader() = default; template auto read(const Source& source) const { return reader_.read(source); } private: - Scene2D_Context<> scene_; Reader reader_; }; struct State_Plain_Value { diff --git a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp index b8c1127..af988fd 100644 --- a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp +++ b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp @@ -72,8 +72,8 @@ struct Threading_Test_State_Value { using Threading_Test_Double_State = Double_State_Strategy; using Threading_Test_Triple_State = Triple_State_Strategy; struct Threading_Test_Scene_Renderable : Renderable_Base { - Threading_Test_Scene_Renderable(Scene_Base& scene, std::atomic& render_count, std::atomic& maximum_sequence) - : Renderable_Base(scene, {.cache_enabled = false}), render_count(&render_count), maximum_sequence(&maximum_sequence) {} + Threading_Test_Scene_Renderable(std::atomic& render_count, std::atomic& maximum_sequence) + : Renderable_Base({.cache_enabled = false}), render_count(&render_count), maximum_sequence(&maximum_sequence) {} void prepare(const Prepare_Render_Context& context) override { render_count->fetch_add(1, std::memory_order_relaxed); std::uint64_t maximum = maximum_sequence->load(std::memory_order_acquire); @@ -83,7 +83,7 @@ struct Threading_Test_Scene_Renderable : Renderable_Base { std::atomic* maximum_sequence; }; struct Threading_Test_Render_Graph_Renderable : Renderable_Base { - Threading_Test_Render_Graph_Renderable(Scene_Base& scene, std::atomic& executed) : Renderable_Base(scene, {.cache_enabled = false}), executed(&executed) {} + Threading_Test_Render_Graph_Renderable(std::atomic& executed) : Renderable_Base({.cache_enabled = false}), executed(&executed) {} void build_prepare_graph(Renderable_Graph_Builder& graph) override { graph.emplace("prepare", "Prepare", [this](const Prepare_Render_Context&) { @@ -513,8 +513,11 @@ TEST(threading_contract_test, scene_serializes_multiple_render_submitters_withou constexpr int render_count = submitter_count * renders_per_submitter; std::atomic executed{}; std::atomic maximum_sequence{}; - auto renderable = std::make_shared(scene, executed, maximum_sequence); - scene.attach_renderable(renderable); + auto renderable = renderive_Owner::make(executed, maximum_sequence); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } std::atomic start{}; std::vector submitters; submitters.reserve(submitter_count); @@ -538,8 +541,11 @@ TEST(threading_contract_test, scene_render_and_render_graph_rebuild_are_serializ Scene2D_Context<> scene; constexpr int render_count = 300; std::atomic executed{}; - auto renderable = std::make_shared(scene, executed); - scene.attach_renderable(renderable).wait(); + auto renderable = renderive_Owner::make(executed); + { + auto attach = scene.attach_builder(); + attach.attach(renderable); + } std::atomic start{}; std::thread renderer([&] { wait_start(start); @@ -563,12 +569,16 @@ TEST(threading_contract_test, scene_render_and_render_graph_rebuild_are_serializ TEST(threading_contract_test, real_time_data_updates_can_race_with_scene_destruction) { using Data = Latest_Real_Time_Data>; struct Renderable : Renderable_Base { - explicit Renderable(Scene_Base& scene) : Renderable_Base(scene) {} + explicit Renderable() : Renderable_Base() {} }; using Attachment = Attach_Real_Time_Data; auto data = std::make_shared(); auto scene = std::make_unique>(); - auto renderable = std::make_shared(With_Real_Time_Data(data), *scene); + auto renderable = renderive_Owner::make(With_Real_Time_Data(data)); + { + auto attach = scene->attach_builder(); + attach.attach(renderable); + } std::atomic start{}; std::atomic stop{}; std::atomic updates{}; diff --git a/render_2D/axis/Abs_Axis.cpp b/render_2D/axis/Abs_Axis.cpp index 262c221..9328488 100644 --- a/render_2D/axis/Abs_Axis.cpp +++ b/render_2D/axis/Abs_Axis.cpp @@ -5,7 +5,7 @@ namespace renderive { -Abs_Axis::Abs_Axis(::Scene_Base& scene) : Renderable(scene, true) {} +Abs_Axis::Abs_Axis() : Renderable(true) {} Abs_Axis::~Abs_Axis() = default; diff --git a/render_2D/axis/Abs_Axis.h b/render_2D/axis/Abs_Axis.h index 4ae3eca..3314312 100644 --- a/render_2D/axis/Abs_Axis.h +++ b/render_2D/axis/Abs_Axis.h @@ -9,7 +9,7 @@ namespace renderive { class LIB_DECL Abs_Axis : public Renderable { public: - explicit Abs_Axis(::Scene_Base& scene); + Abs_Axis(); ~Abs_Axis() override; [[nodiscard]] virtual Axis_Transform transform() const = 0; diff --git a/render_2D/axis/Axis_Builder.h b/render_2D/axis/Axis_Builder.h index ff7124e..c992d32 100644 --- a/render_2D/axis/Axis_Builder.h +++ b/render_2D/axis/Axis_Builder.h @@ -1,24 +1,17 @@ #pragma once - #include "../renderable/Renderable.h" #include "Axis_Types.h" #include - -#include #include - namespace renderive::detail { - template class Axis_Renderable_Builder { public: using Self = Axis_Renderable_Builder; - - Axis_Renderable_Builder(std::shared_ptr parent, Orientation orientation) - : parent_(std::move(parent)) { + Axis_Renderable_Builder(renderive_Owner parent, Orientation orientation, ::Scene_Base::Attach_Builder* attach_builder = nullptr) + : parent_(std::move(parent)), attach_builder_(attach_builder) { properties_.orientation = orientation; } - Self& set_x(int value) { properties_.x = value; return *this; } Self& set_y(int value) { properties_.y = value; return *this; } Self& set_orientation(Orientation value) { properties_.orientation = value; return *this; } @@ -32,7 +25,6 @@ public: Self& set_unit_text_pen(Pen value) { properties_.unit_text_pen = value; return *this; } Self& set_unit_text_background_brush(Brush value) { properties_.unit_text_background_brush = value; return *this; } Self& set_label_rotation_degrees(int value) { properties_.label_rotation_degrees = value; return *this; } - Self& set_coord_range(Range value) requires requires(Properties properties) { properties.coordinates = value; } { properties_.coordinates = value; return *this; @@ -69,21 +61,20 @@ public: properties_.newest_at_start = value; return *this; } - - std::shared_ptr build() const { + renderive_Owner build() const { if (!parent_) return {}; - auto result = parent_->scene().template make_renderable(properties_); - parent_->scene().attach_renderable(result, { - .display_parents = {parent_}, - .dependency_parents = {parent_} - }); + auto result = renderive_Owner::make(properties_); + if (attach_builder_) { + attach_builder_->attach(result); + attach_builder_->add_parent(::Scene_Base::Relationship::display, result, parent_); + attach_builder_->add_parent(::Scene_Base::Relationship::dependency, result, parent_); + } return result; } - private: - std::shared_ptr parent_; + renderive_Owner parent_; Properties properties_; + ::Scene_Base::Attach_Builder* attach_builder_{}; }; - -} // namespace renderive::detail +} diff --git a/render_2D/axis/Axis_State_Strategy.hpp b/render_2D/axis/Axis_State_Strategy.hpp index f107983..d201c34 100644 --- a/render_2D/axis/Axis_State_Strategy.hpp +++ b/render_2D/axis/Axis_State_Strategy.hpp @@ -25,10 +25,9 @@ class Axis_State_Strategy using Base = Double_State_Strategy; public: - Axis_State_Strategy(::Scene_Base& scene, State state) + explicit Axis_State_Strategy(State state) : Base(state, - With_Observer{State_Observer(Renderable_State_Observer(this))}, - scene) {} + With_Observer{State_Observer(Renderable_State_Observer(this))}) {} template Value> Axis_State_Strategy& set(Value&& value) { diff --git a/render_2D/axis/Frequency_Axis.cpp b/render_2D/axis/Frequency_Axis.cpp index e6bd5d2..0f285af 100644 --- a/render_2D/axis/Frequency_Axis.cpp +++ b/render_2D/axis/Frequency_Axis.cpp @@ -6,8 +6,8 @@ namespace renderive::detail { -Frequency_Axis_Control::Frequency_Axis_Control(::Scene_Base& scene, const Axis_Properties& properties) - : Axis(scene, properties) {} +Frequency_Axis_Control::Frequency_Axis_Control(const Axis_Properties& properties) + : Axis(properties) {} std::string Frequency_Axis_Control::format_tick_label(double tick, const Axis_Properties& state) const { diff --git a/render_2D/axis/Frequency_Axis.h b/render_2D/axis/Frequency_Axis.h index d10e155..8b09e78 100644 --- a/render_2D/axis/Frequency_Axis.h +++ b/render_2D/axis/Frequency_Axis.h @@ -8,7 +8,7 @@ namespace detail { class LIB_DECL Frequency_Axis_Control : public Axis { public: - Frequency_Axis_Control(::Scene_Base& scene, const Axis_Properties& properties); + explicit Frequency_Axis_Control(const Axis_Properties& properties); protected: [[nodiscard]] std::string format_tick_label(double tick, const Axis_Properties& state) const override; }; diff --git a/render_2D/axis/Numeric_Axis.cpp b/render_2D/axis/Numeric_Axis.cpp index 34b930f..380405d 100644 --- a/render_2D/axis/Numeric_Axis.cpp +++ b/render_2D/axis/Numeric_Axis.cpp @@ -2,8 +2,8 @@ namespace renderive::detail { -Axis_Control::Axis_Control(::Scene_Base& scene, const Axis_Properties& properties) - : Axis_State_Strategy(scene, properties) {} +Axis_Control::Axis_Control(const Axis_Properties& properties) + : Axis_State_Strategy(properties) {} void Axis_Control::handle_event(const Event& event) { if (event.type == Event_Type::Wheel && get<&Axis_Properties::wheel>()) { diff --git a/render_2D/axis/Numeric_Axis.h b/render_2D/axis/Numeric_Axis.h index fcfbe44..873a27f 100644 --- a/render_2D/axis/Numeric_Axis.h +++ b/render_2D/axis/Numeric_Axis.h @@ -34,7 +34,7 @@ struct Axis_Interaction_State { class LIB_DECL Axis_Control : public Axis_State_Strategy, public Event_Handler { public: - Axis_Control(::Scene_Base& scene, const Axis_Properties& properties); + explicit Axis_Control(const Axis_Properties& properties); void handle_event(const Event& event) override; void publish() override; diff --git a/render_2D/axis/Time_Axis.cpp b/render_2D/axis/Time_Axis.cpp index 0388d68..88a0125 100644 --- a/render_2D/axis/Time_Axis.cpp +++ b/render_2D/axis/Time_Axis.cpp @@ -16,8 +16,8 @@ Time_Axis_State time_state_from(const Time_Axis_Properties& properties) { } // namespace -Time_Axis_Control::Time_Axis_Control(::Scene_Base& scene, const Time_Axis_Properties& properties) - : Axis_State_Strategy(scene, time_state_from(properties)) {} +Time_Axis_Control::Time_Axis_Control(const Time_Axis_Properties& properties) + : Axis_State_Strategy(time_state_from(properties)) {} std::size_t Time_Axis_Control::time_point_count() const { return read([](const Time_Axis_State& state) { return state.samples.size(); }); diff --git a/render_2D/axis/Time_Axis.h b/render_2D/axis/Time_Axis.h index db24129..919c50f 100644 --- a/render_2D/axis/Time_Axis.h +++ b/render_2D/axis/Time_Axis.h @@ -27,7 +27,7 @@ struct Time_Axis_State : Time_Axis_Properties { class LIB_DECL Time_Axis_Control : public Axis_State_Strategy { public: - Time_Axis_Control(::Scene_Base& scene, const Time_Axis_Properties& properties); + explicit Time_Axis_Control(const Time_Axis_Properties& properties); [[nodiscard]] std::size_t time_point_count() const; int append_time(Time_Of_Day time); [[nodiscard]] Time_Of_Day tick_to_time(int tick) const; diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp index 60a50d9..1a5ccdd 100644 --- a/render_2D/plottable/Afterglow.cpp +++ b/render_2D/plottable/Afterglow.cpp @@ -23,16 +23,16 @@ struct Afterglow_Prepare_Buffer { }; } struct Afterglow_Control::Impl { - Impl(Afterglow_Control& owner, std::shared_ptr frequency, std::shared_ptr power) + Impl(Afterglow_Control& owner, renderive_Owner frequency, renderive_Owner power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)), history(owner) {} - std::shared_ptr frequency_axis; - std::shared_ptr power_axis; + renderive_Owner frequency_axis; + renderive_Owner power_axis; Afterglow_History history; Adaptive_Render_Partitioner partitioner; Afterglow_Prepare_Buffer prepare_buffer; }; -Afterglow_Control::Afterglow_Control(::Scene_Base& scene, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) - : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} +Afterglow_Control::Afterglow_Control(const Afterglow_Properties& properties, renderive_Owner frequency_axis, renderive_Owner power_axis) + : Plottable_State(properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} Afterglow_Control::~Afterglow_Control() = default; std::size_t Afterglow_Control::history_count() const { return impl_->history.size(); diff --git a/render_2D/plottable/Afterglow.h b/render_2D/plottable/Afterglow.h index 16a0b0e..c4de2bf 100644 --- a/render_2D/plottable/Afterglow.h +++ b/render_2D/plottable/Afterglow.h @@ -21,7 +21,7 @@ namespace detail { class LIB_DECL Afterglow_Control : public Plottable_State, public ::Render_Frame_Completion { public: - Afterglow_Control(::Scene_Base& scene, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + Afterglow_Control(const Afterglow_Properties& properties, renderive_Owner frequency_axis, renderive_Owner power_axis); ~Afterglow_Control() override; [[nodiscard]] std::size_t history_count() const; [[nodiscard]] std::size_t latest_spectrum_point_count() const; diff --git a/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/plottable/Constellation_Diagram.cpp index e67b173..a9e3a2a 100644 --- a/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/plottable/Constellation_Diagram.cpp @@ -21,15 +21,15 @@ struct Constellation_Diagram_Control::Impl { std::vector anchors; std::vector points; }; - Impl(Constellation_Diagram_Control& owner, std::shared_ptr i, std::shared_ptr q) + Impl(Constellation_Diagram_Control& owner, renderive_Owner i, renderive_Owner q) : i_axis(std::move(i)), q_axis(std::move(q)), points(owner) {} - std::shared_ptr i_axis; - std::shared_ptr q_axis; + renderive_Owner i_axis; + renderive_Owner q_axis; Constellation_History points; Prepare_Buffer prepare_buffer; }; -Constellation_Diagram_Control::Constellation_Diagram_Control(::Scene_Base& scene, const Constellation_Diagram_Properties& properties, std::shared_ptr i_axis, std::shared_ptr q_axis) - : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(i_axis), std::move(q_axis))) {} +Constellation_Diagram_Control::Constellation_Diagram_Control(const Constellation_Diagram_Properties& properties, renderive_Owner i_axis, renderive_Owner q_axis) + : Plottable_State(properties), impl_(std::make_unique(*this, std::move(i_axis), std::move(q_axis))) {} Constellation_Diagram_Control::~Constellation_Diagram_Control() = default; void Constellation_Diagram_Control::append_point(PointF point) { const auto now = std::chrono::steady_clock::now(); diff --git a/render_2D/plottable/Constellation_Diagram.h b/render_2D/plottable/Constellation_Diagram.h index 56bbded..51eb466 100644 --- a/render_2D/plottable/Constellation_Diagram.h +++ b/render_2D/plottable/Constellation_Diagram.h @@ -19,7 +19,7 @@ struct Constellation_Diagram_Properties { namespace detail { class LIB_DECL Constellation_Diagram_Control : public Plottable_State { public: - Constellation_Diagram_Control(::Scene_Base& scene, const Constellation_Diagram_Properties& properties, std::shared_ptr i_axis, std::shared_ptr q_axis); + Constellation_Diagram_Control(const Constellation_Diagram_Properties& properties, renderive_Owner i_axis, renderive_Owner q_axis); ~Constellation_Diagram_Control() override; void append_point(PointF point); [[nodiscard]] std::size_t point_count() const; diff --git a/render_2D/plottable/Frequency_Trace.cpp b/render_2D/plottable/Frequency_Trace.cpp index b969fb7..c11d565 100644 --- a/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/plottable/Frequency_Trace.cpp @@ -14,15 +14,15 @@ struct Frequency_Trace_Control::Impl { struct Prepare_Buffer { std::vector points; }; - Impl(Frequency_Trace_Control& owner, std::shared_ptr time, std::shared_ptr value) + Impl(Frequency_Trace_Control& owner, renderive_Owner time, renderive_Owner value) : time_axis(std::move(time)), value_axis(std::move(value)), samples(owner) {} - std::shared_ptr time_axis; - std::shared_ptr value_axis; + renderive_Owner time_axis; + renderive_Owner value_axis; Frequency_Trace_History samples; Prepare_Buffer prepare_buffer; }; -Frequency_Trace_Control::Frequency_Trace_Control(::Scene_Base& scene, const Frequency_Trace_Properties& properties, std::shared_ptr time_axis, std::shared_ptr value_axis) - : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(time_axis), std::move(value_axis))) {} +Frequency_Trace_Control::Frequency_Trace_Control(const Frequency_Trace_Properties& properties, renderive_Owner time_axis, renderive_Owner value_axis) + : Plottable_State(properties), impl_(std::make_unique(*this, std::move(time_axis), std::move(value_axis))) {} Frequency_Trace_Control::~Frequency_Trace_Control() = default; void Frequency_Trace_Control::append_sample(int tick, double value) { const int limit = std::max(2, impl_->time_axis->get<&Time_Axis_Properties::visible_count>()); diff --git a/render_2D/plottable/Frequency_Trace.h b/render_2D/plottable/Frequency_Trace.h index 4add745..2f745b6 100644 --- a/render_2D/plottable/Frequency_Trace.h +++ b/render_2D/plottable/Frequency_Trace.h @@ -8,7 +8,7 @@ struct Frequency_Trace_Properties { namespace detail { class LIB_DECL Frequency_Trace_Control : public Plottable_State { public: - Frequency_Trace_Control(::Scene_Base& scene, const Frequency_Trace_Properties& properties, std::shared_ptr time_axis, std::shared_ptr value_axis); + Frequency_Trace_Control(const Frequency_Trace_Properties& properties, renderive_Owner time_axis, renderive_Owner value_axis); ~Frequency_Trace_Control() override; void append_sample(int tick, double value); void append_sample(Time_Of_Day time, double value); diff --git a/render_2D/plottable/Plottable.h b/render_2D/plottable/Plottable.h index 7620b1c..0150a14 100644 --- a/render_2D/plottable/Plottable.h +++ b/render_2D/plottable/Plottable.h @@ -60,11 +60,10 @@ public: using State_Observer = Observer_State; using Base = Double_State_Strategy; - Plottable_State(::Scene_Base& scene, const Properties& properties) + explicit Plottable_State(const Properties& properties) : Base(properties, With_Observer{ - State_Observer(Renderable_State_Observer(this))}, - scene) {} + State_Observer(Renderable_State_Observer(this))}) {} template Value> Plottable_State& set(Value&& value) { Base::template set(std::forward(value)); diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/plottable/Selection_Rectangle_Overlay.cpp index 1076fa5..7a83828 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -29,16 +29,16 @@ struct Selection_Rectangle_Overlay_Control::Impl { RectF active_selection; bool selecting{}; }; - Impl(Selection_Rectangle_Overlay_Control& owner, std::shared_ptr horizontal, std::shared_ptr vertical) + Impl(Selection_Rectangle_Overlay_Control& owner, renderive_Owner horizontal, renderive_Owner vertical) : horizontal_axis(std::move(horizontal)), vertical_axis(std::move(vertical)), regions(owner) {} - std::shared_ptr horizontal_axis; - std::shared_ptr vertical_axis; + renderive_Owner horizontal_axis; + renderive_Owner vertical_axis; Plottable_History_Real_Time_Data> regions; Selection_Interaction_State interaction; Prepare_Buffer prepare_buffer; }; -Selection_Rectangle_Overlay_Control::Selection_Rectangle_Overlay_Control(::Scene_Base& scene, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis) - : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(horizontal_axis), std::move(vertical_axis))) {} +Selection_Rectangle_Overlay_Control::Selection_Rectangle_Overlay_Control(const Selection_Rectangle_Overlay_Properties& properties, renderive_Owner horizontal_axis, renderive_Owner vertical_axis) + : Plottable_State(properties), impl_(std::make_unique(*this, std::move(horizontal_axis), std::move(vertical_axis))) {} Selection_Rectangle_Overlay_Control::~Selection_Rectangle_Overlay_Control() = default; std::vector Selection_Rectangle_Overlay_Control::selected_regions() const { return impl_->regions.snapshot(); diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.h b/render_2D/plottable/Selection_Rectangle_Overlay.h index 2575478..93675b2 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.h +++ b/render_2D/plottable/Selection_Rectangle_Overlay.h @@ -11,7 +11,7 @@ struct Selection_Rectangle_Overlay_Properties { namespace detail { class LIB_DECL Selection_Rectangle_Overlay_Control : public Plottable_State, public Paint_Overlay, public Event_Handler { public: - Selection_Rectangle_Overlay_Control(::Scene_Base& scene, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis); + Selection_Rectangle_Overlay_Control(const Selection_Rectangle_Overlay_Properties& properties, renderive_Owner horizontal_axis, renderive_Owner vertical_axis); ~Selection_Rectangle_Overlay_Control() override; [[nodiscard]] std::vector selected_regions() const; void clear_selected_regions(); diff --git a/render_2D/plottable/Spectrum.cpp b/render_2D/plottable/Spectrum.cpp index b9899cd..eccdbfb 100644 --- a/render_2D/plottable/Spectrum.cpp +++ b/render_2D/plottable/Spectrum.cpp @@ -137,18 +137,18 @@ double spectrum_power_at(const Spectrum_Properties& properties, const Spectrum_F } } struct Spectrum_Control::Impl { - Impl(Spectrum_Control& owner, std::shared_ptr frequency, std::shared_ptr power) + Impl(Spectrum_Control& owner, renderive_Owner frequency, renderive_Owner power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)), frame(owner) {} - std::shared_ptr frequency_axis; - std::shared_ptr power_axis; + renderive_Owner frequency_axis; + renderive_Owner power_axis; Plottable_Latest_Real_Time_Data frame; Spectrum_Interaction_State interaction; std::mutex frame_update_mutex; Adaptive_Render_Partitioner partitioner; Spectrum_Prepare_Buffer prepare_buffer; }; -Spectrum_Control::Spectrum_Control(::Scene_Base& scene, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) - : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} +Spectrum_Control::Spectrum_Control(const Spectrum_Properties& properties, renderive_Owner frequency_axis, renderive_Owner power_axis) + : Plottable_State(properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} Spectrum_Control::~Spectrum_Control() = default; void Spectrum_Control::update_samples(std::span values) { if(get<&Spectrum_Properties::frequency_point_size>() <= 0) diff --git a/render_2D/plottable/Spectrum.h b/render_2D/plottable/Spectrum.h index 75636f7..236f9ff 100644 --- a/render_2D/plottable/Spectrum.h +++ b/render_2D/plottable/Spectrum.h @@ -38,7 +38,7 @@ class LIB_DECL Spectrum_Control : public Plottable_State, public Event_Handler, public ::Render_Frame_Completion { public: - Spectrum_Control(::Scene_Base& scene, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + Spectrum_Control(const Spectrum_Properties& properties, renderive_Owner frequency_axis, renderive_Owner power_axis); ~Spectrum_Control() override; void update_samples(std::span values); void update_samples(std::pmr::vector&& values); diff --git a/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/plottable/Sweep_Spectrum.cpp index 8712511..1cc108c 100644 --- a/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/plottable/Sweep_Spectrum.cpp @@ -23,15 +23,15 @@ struct Sweep_Spectrum_Control::Impl { PointF current_second; bool valid{}; }; - Impl(Sweep_Spectrum_Control& owner, std::shared_ptr frequency, std::shared_ptr power) + Impl(Sweep_Spectrum_Control& owner, renderive_Owner frequency, renderive_Owner power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)), blocks(owner) {} - std::shared_ptr frequency_axis; - std::shared_ptr power_axis; + renderive_Owner frequency_axis; + renderive_Owner power_axis; Sweep_Spectrum_History blocks; Prepare_Buffer prepare_buffer; }; -Sweep_Spectrum_Control::Sweep_Spectrum_Control(::Scene_Base& scene, const Sweep_Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) - : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} +Sweep_Spectrum_Control::Sweep_Spectrum_Control(const Sweep_Spectrum_Properties& properties, renderive_Owner frequency_axis, renderive_Owner power_axis) + : Plottable_State(properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} Sweep_Spectrum_Control::~Sweep_Spectrum_Control() = default; void Sweep_Spectrum_Control::append_block(std::span values) { if(get<&Sweep_Spectrum_Properties::bins_per_block>() <= 0) diff --git a/render_2D/plottable/Sweep_Spectrum.h b/render_2D/plottable/Sweep_Spectrum.h index fcc6c2b..e8c6341 100644 --- a/render_2D/plottable/Sweep_Spectrum.h +++ b/render_2D/plottable/Sweep_Spectrum.h @@ -16,7 +16,7 @@ struct Sweep_Spectrum_Properties { namespace detail { class LIB_DECL Sweep_Spectrum_Control : public Plottable_State { public: - Sweep_Spectrum_Control(::Scene_Base& scene, const Sweep_Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + Sweep_Spectrum_Control(const Sweep_Spectrum_Properties& properties, renderive_Owner frequency_axis, renderive_Owner power_axis); ~Sweep_Spectrum_Control() override; void append_block(std::span values); void append_block(std::pmr::vector&& values); diff --git a/render_2D/plottable/Waterfall.cpp b/render_2D/plottable/Waterfall.cpp index adfbee8..ad2340e 100644 --- a/render_2D/plottable/Waterfall.cpp +++ b/render_2D/plottable/Waterfall.cpp @@ -57,17 +57,17 @@ std::size_t waterfall_work_size(const Waterfall_Properties& state, } } struct Waterfall_Control::Impl { - Impl(Waterfall_Control& owner, std::shared_ptr frequency, std::shared_ptr time) + Impl(Waterfall_Control& owner, renderive_Owner frequency, renderive_Owner time) : frequency_axis(std::move(frequency)), time_axis(std::move(time)), rows(owner) {} - std::shared_ptr frequency_axis; - std::shared_ptr time_axis; + renderive_Owner frequency_axis; + renderive_Owner time_axis; Waterfall_History rows; Waterfall_Interaction_State interaction; Adaptive_Render_Partitioner partitioner; Waterfall_Prepare_Buffer prepare_buffer; }; -Waterfall_Control::Waterfall_Control(::Scene_Base& scene, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis) - : Plottable_State(scene, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(time_axis))) {} +Waterfall_Control::Waterfall_Control(const Waterfall_Properties& properties, renderive_Owner frequency_axis, renderive_Owner time_axis) + : Plottable_State(properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(time_axis))) {} Waterfall_Control::~Waterfall_Control() = default; void Waterfall_Control::append_row(int tick, std::span values) { const std::size_t limit = static_cast( diff --git a/render_2D/plottable/Waterfall.h b/render_2D/plottable/Waterfall.h index a1fe9c3..e45e9c5 100644 --- a/render_2D/plottable/Waterfall.h +++ b/render_2D/plottable/Waterfall.h @@ -22,7 +22,7 @@ class LIB_DECL Waterfall_Control : public Plottable_State, public Event_Handler, public ::Render_Frame_Completion { public: - Waterfall_Control(::Scene_Base& scene, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis); + Waterfall_Control(const Waterfall_Properties& properties, renderive_Owner frequency_axis, renderive_Owner time_axis); ~Waterfall_Control() override; void append_row(int tick, std::span values); void append_row(int tick, std::pmr::vector&& values); diff --git a/render_2D/renderable/Renderable.cpp b/render_2D/renderable/Renderable.cpp index 627991d..55dfd99 100644 --- a/render_2D/renderable/Renderable.cpp +++ b/render_2D/renderable/Renderable.cpp @@ -23,8 +23,8 @@ void Renderable_Observer::observe_state(Renderable_Observer_Event event, observation_.publish_count = publish_count; } -Renderable::Renderable(::Scene_Base& scene, bool cache_enabled) - : ::Renderable_Base(scene, {.cache_enabled = cache_enabled}) {} +Renderable::Renderable(bool cache_enabled) + : ::Renderable_Base({.cache_enabled = cache_enabled}) {} Renderable::~Renderable() = default; @@ -35,9 +35,7 @@ Renderable_Cache_Mode Renderable::get_cache_mode() const noexcept { } void Renderable::set_cache_mode(Renderable_Cache_Mode mode) { - scene().set_renderable_configuration( - shared_from_this(), - {.cache_enabled = mode == Renderable_Cache_Mode::Local_Pixel}); + set_configuration({.cache_enabled = mode == Renderable_Cache_Mode::Local_Pixel}); } std::string Renderable::object_name() const { @@ -106,12 +104,12 @@ Renderable_Graph_Builder::Task Renderable::add_paint_task( void Renderable::changed() noexcept { invalidate_prepare(); - scene().notify_model_dirty(); + notify_scene_model_dirty(); } void Renderable::paint_changed() noexcept { invalidate_paint(); - scene().notify_model_dirty(); + notify_scene_model_dirty(); } void Renderable::render_graph_changed() { diff --git a/render_2D/renderable/Renderable.h b/render_2D/renderable/Renderable.h index 1fe4c5d..cceee80 100644 --- a/render_2D/renderable/Renderable.h +++ b/render_2D/renderable/Renderable.h @@ -4,6 +4,7 @@ #include "../event/Event.h" #include +#include #include #include @@ -53,7 +54,7 @@ private: class LIB_DECL Renderable : public ::Renderable_Base { public: - explicit Renderable(::Scene_Base& scene, bool cache_enabled = true); + explicit Renderable(bool cache_enabled = true); ~Renderable() override; [[nodiscard]] Renderable_Cache_Mode get_cache_mode() const noexcept; diff --git a/render_2D/renderable/Renderable_Builder.h b/render_2D/renderable/Renderable_Builder.h index bfa2f1e..f5cf72d 100644 --- a/render_2D/renderable/Renderable_Builder.h +++ b/render_2D/renderable/Renderable_Builder.h @@ -6,55 +6,33 @@ #include #include #include -#include #include -#include namespace renderive { class Abs_Axis; namespace detail { class Paint_Overlay; - -template -void append_unique(std::vector<::Scene_Base::Renderable>& renderables, - const std::shared_ptr& renderable) { - ::Scene_Base::Renderable base = renderable; - if (base && std::find(renderables.begin(), renderables.end(), base) == - renderables.end()) - renderables.push_back(std::move(base)); -} - template -requires std::derived_from && - std::derived_from -void append_renderable_dependency( - ::Scene_Base::Attach_Relationships& relationships, - const std::shared_ptr& axis) { +requires std::derived_from && std::derived_from +void attach_renderable_dependency(::Scene_Base::Attach_Builder& builder, const renderive_Owner& renderable, const renderive_Owner& axis) { if (!axis) return; - append_unique(relationships.dependency_parents, axis); + builder.add_parent(::Scene_Base::Relationship::dependency, renderable, axis); if constexpr (std::derived_from) - append_unique(relationships.display_parents, axis); + builder.add_parent(::Scene_Base::Relationship::display, renderable, axis); else - append_unique(relationships.display_children, axis); + builder.add_parent(::Scene_Base::Relationship::display, axis, renderable); } - template -void append_renderable_dependency( - ::Scene_Base::Attach_Relationships&, const Value&) {} - +void attach_renderable_dependency(::Scene_Base::Attach_Builder&, const renderive_Owner&, const Value&) {} template requires std::derived_from -auto renderable_attach_relationships( - const std::shared_ptr& parent, const Args&... args) - -> ::Scene_Base::Attach_Relationships { - ::Scene_Base::Attach_Relationships relationships; - append_unique(relationships.display_parents, parent); - append_unique(relationships.dependency_parents, parent); - (append_renderable_dependency(relationships, args), ...); - return relationships; +void attach_renderable(::Scene_Base::Attach_Builder& builder, const renderive_Owner& renderable, const renderive_Owner& parent, const Args&... args) { + builder.attach(renderable); + builder.add_parent(::Scene_Base::Relationship::display, renderable, parent); + builder.add_parent(::Scene_Base::Relationship::dependency, renderable, parent); + (attach_renderable_dependency(builder, renderable, args), ...); } } - template Validator_Type = No_Property_Validator> class Renderable_Builder { public: @@ -62,9 +40,10 @@ public: using Properties = Properties_Type; using Validator = Validator_Type; using Self = Renderable_Builder; - Renderable_Builder() requires std::default_initializable && std::default_initializable : properties{}, validator{} {} - explicit Renderable_Builder(Properties properties) requires std::default_initializable : properties(std::move(properties)), validator{} {} - Renderable_Builder(Properties properties, Validator validator) : properties(std::move(properties)), validator(std::move(validator)) {} + Renderable_Builder() requires std::default_initializable && std::default_initializable = default; + explicit Renderable_Builder(::Scene_Base::Attach_Builder* attach_builder) requires std::default_initializable && std::default_initializable : attach_builder_(attach_builder) {} + explicit Renderable_Builder(Properties properties, ::Scene_Base::Attach_Builder* attach_builder = nullptr) requires std::default_initializable : properties(std::move(properties)), attach_builder_(attach_builder) {} + Renderable_Builder(Properties properties, Validator validator, ::Scene_Base::Attach_Builder* attach_builder = nullptr) : properties(std::move(properties)), validator(std::move(validator)), attach_builder_(attach_builder) {} template Value> Self& set(Value&& value) { properties.*Member = std::forward(value); @@ -79,27 +58,27 @@ public: return properties; } template - requires std::derived_from && std::constructible_from - std::shared_ptr build(const std::shared_ptr& parent, Args&&... args) const { + requires std::derived_from && std::constructible_from + renderive_Owner build(const renderive_Owner& parent, Args&&... args) const { if (!parent || !(valid_argument(args) && ...)) return {}; validator(properties); - auto relationships = - detail::renderable_attach_relationships(parent, args...); - auto result = parent->scene().template make_renderable(properties, std::forward(args)...); - parent->scene().attach_renderable(result, std::move(relationships)); + auto result = renderive_Owner::make(properties, args...); + if (attach_builder_) + detail::attach_renderable(*attach_builder_, result, parent, args...); return result; } private: template - static bool valid_argument(const std::shared_ptr& value) { + static bool valid_argument(const renderive_Owner& value) { return static_cast(value); } template static bool valid_argument(const T&) { return true; } - Properties properties; - [[no_unique_address]] Validator validator; + Properties properties{}; + [[no_unique_address]] Validator validator{}; + ::Scene_Base::Attach_Builder* attach_builder_{}; }; } diff --git a/render_2D/scene/Scene.cpp b/render_2D/scene/Scene.cpp index e5212fc..dd449a0 100644 --- a/render_2D/scene/Scene.cpp +++ b/render_2D/scene/Scene.cpp @@ -11,8 +11,8 @@ private: void paint(Painter&, const Paint_Render_Context&) override {} }; } -std::shared_ptr make_renderable_group(::Scene_Base& scene, bool cache_enabled) { - return scene.make_renderable(cache_enabled); +renderive_Owner make_renderable_group(bool cache_enabled) { + return renderive_Owner::make(cache_enabled); } void record_performance_frame(Performance_Overlay& overlay, double duration_ms, const Low_Latency_Diagnostics& diagnostics, std::size_t renderable_count, Size viewport) { overlay.record_frame(duration_ms, diagnostics, renderable_count, viewport); diff --git a/render_2D/scene/Scene.h b/render_2D/scene/Scene.h index 8f51b7b..7518558 100644 --- a/render_2D/scene/Scene.h +++ b/render_2D/scene/Scene.h @@ -171,7 +171,7 @@ constexpr Frame_Control_Mode frame_control_mode() noexcept { return Frame_Control_Mode::Low_Latency; return Frame_Control_Mode::Playback; } -LIB_DECL std::shared_ptr make_renderable_group(::Scene_Base& scene, bool cache_enabled); +LIB_DECL renderive_Owner make_renderable_group(bool cache_enabled); LIB_DECL void record_performance_frame(Performance_Overlay& overlay, double duration_ms, const Low_Latency_Diagnostics& diagnostics, std::size_t renderable_count, Size viewport); inline std::string observer_event_name(Frame_Control_Mode mode, std::uint32_t event) { if (mode == Frame_Control_Mode::Manual) { @@ -207,40 +207,22 @@ public: std::lock_guard lock(initialization_mutex_); if (root_renderable()) return; - auto root = detail::make_renderable_group(*this, true); + auto root = detail::make_renderable_group(true); root->set_object_name("root"); auto builder = this->attach_builder(); builder.attach(root); } - [[nodiscard]] std::shared_ptr root_renderable() const { + [[nodiscard]] renderive_Owner root_renderable() const { const auto topology = this->topology_snapshot(); for (const auto& relationship : topology.display) { if (relationship.parent) continue; - auto base = std::const_pointer_cast<::Renderable_Base>(relationship.child); - if (auto renderable = std::dynamic_pointer_cast(base)) + auto base = renderive_const_owner_cast<::Renderable_Base>(relationship.child); + if (auto renderable = renderive_dynamic_owner_cast(base)) return renderable; } return {}; } - [[nodiscard]] std::shared_ptr create_renderable_node(const std::shared_ptr& parent, std::string object_name = {}) { - auto renderable = detail::make_renderable_group(*this, true); - renderable->set_object_name(std::move(object_name)); - attach_renderable_node(renderable, parent); - return renderable; - } - template - requires std::derived_from && std::constructible_from - std::shared_ptr make_renderable(const std::shared_ptr& parent, Args&&... args) { - auto renderable = Kernel_Scene::template make_renderable(std::forward(args)...); - attach_renderable_node(renderable, parent); - return renderable; - } - ::Scene_Base::Mutation remove_renderable(const std::shared_ptr& renderable) { - if (!renderable || renderable == root_renderable()) - return {}; - return this->detach_renderable(renderable); - } void set_background_color(Color color) { if (background_color() == color) return; @@ -269,10 +251,10 @@ public: void dispatch_event(const Event& event) { const auto paint_order = this->paint_order_snapshot(); for (auto iterator = paint_order.rbegin(); iterator != paint_order.rend(); ++iterator) { - auto base = std::const_pointer_cast<::Renderable_Base>(*iterator); - if (auto renderable = std::dynamic_pointer_cast(base)) { + auto base = renderive_const_owner_cast<::Renderable_Base>(*iterator); + if (auto renderable = renderive_dynamic_owner_cast(base)) { if (renderable->is_visible()) { - if (auto handler = std::dynamic_pointer_cast(base)) + if (auto handler = renderive_dynamic_owner_cast(base)) handler->handle_event(event); } if (event.is_accepted()) @@ -475,20 +457,6 @@ public: private: explicit Basic_Scene2D(std::shared_ptr observer) : Kernel_Scene(*memory_resource(Memory_Domain::Plot_Frame), detail::Frame_Observer_State(detail::Frame_Observer{observer})), observer_(std::move(observer)) {} - void attach_renderable_node(const std::shared_ptr& renderable, const std::shared_ptr& parent) { - if (!renderable) - throw std::invalid_argument("renderable is null"); - auto actual_parent = parent; - if (!actual_parent) { - actual_parent = root_renderable(); - if (!actual_parent) - throw std::logic_error("Scene2D::init must be called before adding renderables"); - } - this->attach_renderable(renderable, { - .display_parents = {actual_parent}, - .dependency_parents = {actual_parent} - }); - } std::shared_ptr observer_; mutable std::mutex control_mutex_; std::mutex initialization_mutex_; diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 789e0aa..e8519b5 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -6,12 +6,20 @@ #include #include #include +#include #include #include #include +#include +#include #include namespace renderive { namespace { +template +decltype(auto) build_initial(Scene& scene, Build&& build) { + auto attach = scene.attach_builder(); + return std::invoke(std::forward(build), attach); +} template concept Legacy_Axis_Property_Api = requires(Axis_Type& axis) { axis.x(); @@ -143,28 +151,31 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { plot.activate_view(); const auto root = plot.root_renderable(); ASSERT_TRUE(root); - auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal) - .set_x(32) - .set_y(150) - .set_pixel_length(270) - .set_coord_range({88.0, 108.0}) - .build(); + auto [frequency_axis, power_axis, spectrum] = build_initial(plot, [&](auto& attach) { + auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach) + .set_x(32) + .set_y(150) + .set_pixel_length(270) + .set_coord_range({88.0, 108.0}) + .build(); + auto power = Axis::Builder(root, Orientation::Vertical, &attach) + .set_x(32) + .set_y(8) + .set_pixel_length(142) + .set_coord_range({-120.0, 0.0}) + .build(); + auto value = Spectrum::Builder{&attach} + .set<&Spectrum::Properties::frequency_range>(Range{88.0, 108.0}) + .set<&Spectrum::Properties::frequency_point_size>(64) + .set<&Spectrum::Properties::max_hold_visible>(true) + .build(root, frequency, power); + return std::tuple{frequency, power, value}; + }); frequency_axis->set<&Axis_Base_Properties::locale>(Number_Locale{','}); frequency_axis->set<&Axis_Base_Properties::label_rotation_degrees>(15); EXPECT_EQ(frequency_axis->tick_label(88.5), "88,5 Hz"); frequency_axis->set<&Axis_Properties::precision>(4); EXPECT_EQ(frequency_axis->tick_label(1'234'567.0), "1,2346 MHz"); - auto power_axis = Axis::Builder(root, Orientation::Vertical) - .set_x(32) - .set_y(8) - .set_pixel_length(142) - .set_coord_range({-120.0, 0.0}) - .build(); - auto spectrum = Spectrum::Builder{} - .set<&Spectrum::Properties::frequency_range>(Range{88.0, 108.0}) - .set<&Spectrum::Properties::frequency_point_size>(64) - .set<&Spectrum::Properties::max_hold_visible>(true) - .build(root, frequency_axis, power_axis); std::vector samples(64); for (std::size_t index = 0; index < samples.size(); ++index) samples[index] = -100.0 + static_cast(index % 24) * 3.0; @@ -201,19 +212,52 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { EXPECT_TRUE(wheel.is_accepted()); EXPECT_LT(frequency_axis->get<&Axis_Properties::coordinates>().size(), before_zoom.size()); EXPECT_TRUE(plot.render_frame()); - plot.remove_renderable(spectrum); + plot.edit_renderables([root, frequency_axis, power_axis, spectrum](auto& editor) { + editor.set_parent(::Scene_Base::Relationship::display, frequency_axis, root); + editor.set_parent(::Scene_Base::Relationship::display, power_axis, root); + editor.clear_parents(::Scene_Base::Relationship::display, spectrum); + editor.clear_parents(::Scene_Base::Relationship::dependency, spectrum); + editor.detach(spectrum); + }); EXPECT_TRUE(plot.render_frame(true)); EXPECT_FALSE(plot.render_frame()); } +TEST(Renderive_Core2, StandaloneRenderableBuilderJoinsSceneOnlyInsideRuntimeEditor) { + Scene2D plot; + plot.init(); + const auto root = plot.root_renderable(); + const auto [frequency, power] = build_initial(plot, [&](auto& attach) { + return std::pair{ + Frequency_Axis::Builder(root, Orientation::Horizontal, &attach).build(), + Axis::Builder(root, Orientation::Vertical, &attach).build() + }; + }); + const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); + ASSERT_TRUE(spectrum); + EXPECT_FALSE(spectrum->attached()); + plot.edit_renderables([root, frequency, power, spectrum](auto& editor) { + editor.attach(spectrum); + editor.add_parent(::Scene_Base::Relationship::display, spectrum, root); + editor.add_parent(::Scene_Base::Relationship::dependency, spectrum, root); + editor.add_parent(::Scene_Base::Relationship::dependency, spectrum, frequency); + editor.add_parent(::Scene_Base::Relationship::dependency, spectrum, power); + editor.add_parent(::Scene_Base::Relationship::display, frequency, spectrum); + editor.add_parent(::Scene_Base::Relationship::display, power, spectrum); + }); + EXPECT_TRUE(plot.render_frame(true)); + EXPECT_TRUE(spectrum->attached()); +} TEST(Renderive_Core2, TimeAxisUsesOneFontStateAndFormatsConfiguredLabels) { Scene2D plot; plot.init(); const auto root = plot.root_renderable(); - auto axis = Time_Axis::Builder(root, Orientation::Horizontal) - .set_time_format("hh-mm-ss.zzz") - .set_font(Font{18.0, 700, true}) - .set_tick_label_spacing_px(17) - .build(); + auto axis = build_initial(plot, [&](auto& attach) { + return Time_Axis::Builder(root, Orientation::Horizontal, &attach) + .set_time_format("hh-mm-ss.zzz") + .set_font(Font{18.0, 700, true}) + .set_tick_label_spacing_px(17) + .build(); + }); ASSERT_TRUE(axis); const int tick = axis->append_time(Time_Of_Day{3'723'045}); EXPECT_EQ(axis->tick_label(tick), "01-02-03.045"); @@ -285,24 +329,25 @@ TEST(Renderive_Core2, RetainedAxisAndTimeAxisApisRoundTripWithoutWebAdapters) { plot.init(); const auto root = plot.root_renderable(); ASSERT_TRUE(root); - const auto axis = Axis::Builder(root, Orientation::Vertical) - .set_x(31) - .set_y(17) - .set_pixel_length(240) - .set_tick_length(13) - .set_sub_tick_length(7) - .set_color({10, 20, 30, 255}) - .set_unit_text("dB") - .set_unit_text_font({15.0, 650, true}) - .set_unit_text_pen({Color{40, 50, 60, 255}, 2.0}) - .set_unit_text_background_brush( - {Color{3, 4, 5, 255}, Brush_Style::Solid}) - .set_label_rotation_degrees(27) - .set_coord_range({-120.0, -20.0}) - .set_label_precision(4) - .set_use_wheel(true) - .set_use_drag(true) - .build(); + const auto axis = build_initial(plot, [&](auto& attach) { + return Axis::Builder(root, Orientation::Vertical, &attach) + .set_x(31) + .set_y(17) + .set_pixel_length(240) + .set_tick_length(13) + .set_sub_tick_length(7) + .set_color({10, 20, 30, 255}) + .set_unit_text("dB") + .set_unit_text_font({15.0, 650, true}) + .set_unit_text_pen({Color{40, 50, 60, 255}, 2.0}) + .set_unit_text_background_brush({Color{3, 4, 5, 255}, Brush_Style::Solid}) + .set_label_rotation_degrees(27) + .set_coord_range({-120.0, -20.0}) + .set_label_precision(4) + .set_use_wheel(true) + .set_use_drag(true) + .build(); + }); ASSERT_TRUE(axis); EXPECT_EQ(axis->get<&Axis_Base_Properties::x>(), 31); EXPECT_EQ(axis->get<&Axis_Base_Properties::y>(), 17); @@ -333,16 +378,18 @@ TEST(Renderive_Core2, RetainedAxisAndTimeAxisApisRoundTripWithoutWebAdapters) { EXPECT_GT(static_cast(numeric_transform.pixel_length) + 1, 0); EXPECT_GT(axis->tick_step(numeric_transform.coordinate_range), 0.0); EXPECT_GE(axis->sub_tick_count(axis->tick_step(numeric_transform.coordinate_range)), 0); - const auto time_axis = Time_Axis::Builder(root, Orientation::Horizontal) - .set_x(12) - .set_y(280) - .set_pixel_length(300) - .set_visible_time_point_count(32) - .set_tick_label_spacing_px(19) - .set_time_format("hh:mm:ss.zzz") - .set_font({16.0, 700, true}) - .set_newest_at_axis_start(true) - .build(); + const auto time_axis = build_initial(plot, [&](auto& attach) { + return Time_Axis::Builder(root, Orientation::Horizontal, &attach) + .set_x(12) + .set_y(280) + .set_pixel_length(300) + .set_visible_time_point_count(32) + .set_tick_label_spacing_px(19) + .set_time_format("hh:mm:ss.zzz") + .set_font({16.0, 700, true}) + .set_newest_at_axis_start(true) + .build(); + }); ASSERT_TRUE(time_axis); const int first = time_axis->append_time({3'723'004}); const int second = time_axis->append_time({3'724'005}); @@ -363,25 +410,28 @@ TEST(Renderive_Core2, RetainedSpectrumApisRoundTripAndMarkersRemainObservable) { plot.set_viewport_size({360, 240}); plot.activate_view(); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal) - .set_x(30).set_y(210).set_pixel_length(300) - .set_coord_range({90.0, 110.0}).build(); - const auto power = Axis::Builder(root, Orientation::Vertical) - .set_x(30).set_y(20).set_pixel_length(190) - .set_coord_range({-20.0, -120.0}).build(); - auto spectrum = Spectrum::Builder{} - .set<&Spectrum::Properties::frequency_range>(Range{90.0, 110.0}) - .set<&Spectrum::Properties::frequency_point_size>(4) - .set<&Spectrum::Properties::center_frequency>(100.0) - .set<&Spectrum::Properties::sweep_frequency_range>(Range{96.0, 104.0}) - .set<&Spectrum::Properties::max_hold_visible>(true) - .set<&Spectrum::Properties::min_hold_visible>(true) - .set<&Spectrum::Properties::max_marker_visible>(true) - .set<&Spectrum::Properties::use_min_marker>(true) - .set<&Spectrum::Properties::sweep_region_visible>(true) - .set<&Spectrum::Properties::visible_range_only>(false) - .set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value) - .build(root, frequency, power); + auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach) + .set_x(30).set_y(210).set_pixel_length(300) + .set_coord_range({90.0, 110.0}).build(); + auto power_axis = Axis::Builder(root, Orientation::Vertical, &attach) + .set_x(30).set_y(20).set_pixel_length(190) + .set_coord_range({-20.0, -120.0}).build(); + auto value = Spectrum::Builder{&attach} + .set<&Spectrum::Properties::frequency_range>(Range{90.0, 110.0}) + .set<&Spectrum::Properties::frequency_point_size>(4) + .set<&Spectrum::Properties::center_frequency>(100.0) + .set<&Spectrum::Properties::sweep_frequency_range>(Range{96.0, 104.0}) + .set<&Spectrum::Properties::max_hold_visible>(true) + .set<&Spectrum::Properties::min_hold_visible>(true) + .set<&Spectrum::Properties::max_marker_visible>(true) + .set<&Spectrum::Properties::use_min_marker>(true) + .set<&Spectrum::Properties::sweep_region_visible>(true) + .set<&Spectrum::Properties::visible_range_only>(false) + .set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value) + .build(root, frequency_axis, power_axis); + return std::tuple{frequency_axis, power_axis, value}; + }); ASSERT_TRUE(spectrum); spectrum->set<&Spectrum::Properties::max_brush>(Brush{Color{1, 2, 3, 80}, Brush_Style::Solid}); spectrum->set<&Spectrum::Properties::current_brush>(Brush{Color{4, 5, 6, 90}, Brush_Style::Solid}); @@ -441,22 +491,25 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { plot.set_viewport_size({420, 280}); plot.activate_view(); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal) - .set_x(40).set_y(250).set_pixel_length(340) - .set_coord_range({0.0, 4.0}).build(); - const auto power = Axis::Builder(root, Orientation::Vertical) - .set_x(40).set_y(20).set_pixel_length(230) - .set_coord_range({0.0, -120.0}).build(); - const auto time = Time_Axis::Builder(root, Orientation::Vertical) - .set_x(40).set_y(20).set_pixel_length(230) - .set_visible_time_point_count(8).build(); - auto waterfall = Waterfall::Builder{} - .set<&Waterfall::Properties::frequency_range>(Range{0.0, 4.0}) - .set<&Waterfall::Properties::power_range>(Range{-120.0, 0.0}) - .set<&Waterfall::Properties::frequency_bin_count>(4) - .set<&Waterfall::Properties::visible_range_only>(false) - .set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bicubic) - .build(root, frequency, time); + auto [frequency, power, time, waterfall] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach) + .set_x(40).set_y(250).set_pixel_length(340) + .set_coord_range({0.0, 4.0}).build(); + auto power_axis = Axis::Builder(root, Orientation::Vertical, &attach) + .set_x(40).set_y(20).set_pixel_length(230) + .set_coord_range({0.0, -120.0}).build(); + auto time_axis = Time_Axis::Builder(root, Orientation::Vertical, &attach) + .set_x(40).set_y(20).set_pixel_length(230) + .set_visible_time_point_count(8).build(); + auto value = Waterfall::Builder{&attach} + .set<&Waterfall::Properties::frequency_range>(Range{0.0, 4.0}) + .set<&Waterfall::Properties::power_range>(Range{-120.0, 0.0}) + .set<&Waterfall::Properties::frequency_bin_count>(4) + .set<&Waterfall::Properties::visible_range_only>(false) + .set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bicubic) + .build(root, frequency_axis, time_axis); + return std::tuple{frequency_axis, power_axis, time_axis, value}; + }); ASSERT_TRUE(waterfall); EXPECT_EQ(waterfall->get<&Waterfall::Properties::frequency_range>(), (Range{0.0, 4.0})); EXPECT_EQ(waterfall->get<&Waterfall::Properties::power_range>(), (Range{-120.0, 0.0})); @@ -471,14 +524,16 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { EXPECT_EQ(waterfall->row_count(), 2U); EXPECT_EQ(waterfall->stored_point_count(), 8U); EXPECT_EQ(waterfall->rendered_cell_count(), 8U); - auto afterglow = Afterglow::Builder{} - .set<&Afterglow::Properties::frequency_range>(Range{0.0, 4.0}) - .set<&Afterglow::Properties::power_range>(Range{-120.0, 0.0}) - .set<&Afterglow::Properties::frequency_point_size>(4) - .set<&Afterglow::Properties::power_point_size>(8) - .set<&Afterglow::Properties::interpolate>(false) - .set<&Afterglow::Properties::attenuation_rate>(0.35) - .build(root, frequency, power); + auto afterglow = build_initial(plot, [&](auto& attach) { + return Afterglow::Builder{&attach} + .set<&Afterglow::Properties::frequency_range>(Range{0.0, 4.0}) + .set<&Afterglow::Properties::power_range>(Range{-120.0, 0.0}) + .set<&Afterglow::Properties::frequency_point_size>(4) + .set<&Afterglow::Properties::power_point_size>(8) + .set<&Afterglow::Properties::interpolate>(false) + .set<&Afterglow::Properties::attenuation_rate>(0.35) + .build(root, frequency, power); + }); ASSERT_TRUE(afterglow); EXPECT_EQ(afterglow->get<&Afterglow::Properties::frequency_range>(), (Range{0.0, 4.0})); EXPECT_EQ(afterglow->get<&Afterglow::Properties::power_range>(), (Range{-120.0, 0.0})); @@ -495,15 +550,17 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { EXPECT_EQ(afterglow->rendered_cell_count(), 32U); afterglow->set<&Afterglow::Properties::attenuation_rate>(2.0); EXPECT_DOUBLE_EQ(afterglow->get<&Afterglow::Properties::attenuation_rate>(), 1.0); - auto sweep = Sweep_Spectrum::Builder{} - .set<&Sweep_Spectrum::Properties::frequency_range>(Range{0.0, 8.0}) - .set<&Sweep_Spectrum::Properties::bins_per_block>(4) - .set<&Sweep_Spectrum::Properties::block_count>(2) - .set<&Sweep_Spectrum::Properties::pen>(Pen{Color{1, 120, 220, 255}, 2.0}) - .set<&Sweep_Spectrum::Properties::current_frequency_pen>(Pen{Color{240, 80, 20, 255}, 3.0}) - .set<&Sweep_Spectrum::Properties::visible_range_only>(false) - .set<&Sweep_Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Step_Right) - .build(root, frequency, power); + auto sweep = build_initial(plot, [&](auto& attach) { + return Sweep_Spectrum::Builder{&attach} + .set<&Sweep_Spectrum::Properties::frequency_range>(Range{0.0, 8.0}) + .set<&Sweep_Spectrum::Properties::bins_per_block>(4) + .set<&Sweep_Spectrum::Properties::block_count>(2) + .set<&Sweep_Spectrum::Properties::pen>(Pen{Color{1, 120, 220, 255}, 2.0}) + .set<&Sweep_Spectrum::Properties::current_frequency_pen>(Pen{Color{240, 80, 20, 255}, 3.0}) + .set<&Sweep_Spectrum::Properties::visible_range_only>(false) + .set<&Sweep_Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Step_Right) + .build(root, frequency, power); + }); ASSERT_TRUE(sweep); EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::frequency_range>(), (Range{0.0, 8.0})); EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::bins_per_block>(), 4); @@ -518,9 +575,11 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { EXPECT_EQ(sweep->stored_block_count(), 2U); EXPECT_EQ(sweep->stored_point_count(), 8U); EXPECT_GT(sweep->rendered_point_count(), 0U); - auto trace = Frequency_Trace::Builder{} - .set<&Frequency_Trace::Properties::pen>(Pen{Color{120, 240, 80, 255}, 2.0}) - .build(root, time, power); + auto trace = build_initial(plot, [&](auto& attach) { + return Frequency_Trace::Builder{&attach} + .set<&Frequency_Trace::Properties::pen>(Pen{Color{120, 240, 80, 255}, 2.0}) + .build(root, time, power); + }); ASSERT_TRUE(trace); trace->append_sample(Time_Of_Day{3000}, -80.0); trace->append_sample(Time_Of_Day{4000}, -70.0); @@ -534,18 +593,21 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa plot.set_viewport_size({360, 260}); plot.activate_view(); const auto root = plot.root_renderable(); - const auto horizontal = Axis::Builder(root, Orientation::Horizontal) - .set_x(30).set_y(230).set_pixel_length(300) - .set_coord_range({-3.0, 3.0}).build(); - const auto vertical = Axis::Builder(root, Orientation::Vertical) - .set_x(30).set_y(20).set_pixel_length(210) - .set_coord_range({3.0, -3.0}).build(); - auto selection = Selection_Rectangle_Overlay::Builder{} - .set<&Selection_Rectangle_Overlay::Properties::label_font>(Font{14.0, 600, true}) - .set<&Selection_Rectangle_Overlay::Properties::label_pen>(Pen{Color{20, 220, 180, 255}, 2.0}) - .set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{20, 80, 220, 60}, Brush_Style::Solid}) - .set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{240, 200, 60, 255}, 2.0, Line_Style::Dash}) - .build(root, horizontal, vertical); + auto [horizontal, vertical, selection] = build_initial(plot, [&](auto& attach) { + auto horizontal_axis = Axis::Builder(root, Orientation::Horizontal, &attach) + .set_x(30).set_y(230).set_pixel_length(300) + .set_coord_range({-3.0, 3.0}).build(); + auto vertical_axis = Axis::Builder(root, Orientation::Vertical, &attach) + .set_x(30).set_y(20).set_pixel_length(210) + .set_coord_range({3.0, -3.0}).build(); + auto value = Selection_Rectangle_Overlay::Builder{&attach} + .set<&Selection_Rectangle_Overlay::Properties::label_font>(Font{14.0, 600, true}) + .set<&Selection_Rectangle_Overlay::Properties::label_pen>(Pen{Color{20, 220, 180, 255}, 2.0}) + .set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{20, 80, 220, 60}, Brush_Style::Solid}) + .set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{240, 200, 60, 255}, 2.0, Line_Style::Dash}) + .build(root, horizontal_axis, vertical_axis); + return std::tuple{horizontal_axis, vertical_axis, value}; + }); ASSERT_TRUE(selection); EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::label_font>(), (Font{14.0, 600, true})); EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::label_pen>(), (Pen{Color{20, 220, 180, 255}, 2.0})); @@ -571,7 +633,8 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa EXPECT_FALSE(selection->selected_regions().front().empty()); selection->clear_selected_regions(); EXPECT_TRUE(selection->selected_regions().empty()); - auto constellation = Constellation_Diagram::Builder{} + auto constellation = build_initial(plot, [&](auto& attach) { + return Constellation_Diagram::Builder{&attach} .set<&Constellation_Diagram::Properties::i_range>(Range{-2.0, 2.0}) .set<&Constellation_Diagram::Properties::q_range>(Range{-3.0, 3.0}) .set<&Constellation_Diagram::Properties::point_color>(Color{80, 220, 255, 255}) @@ -579,7 +642,8 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa .set<&Constellation_Diagram::Properties::point_lifetime_ms>(2000) .set<&Constellation_Diagram::Properties::type>(Constellation_Diagram_Type::Psk16) .set<&Constellation_Diagram::Properties::phase_offset_radians>(0.25) - .build(root, horizontal, vertical); + .build(root, horizontal, vertical); + }); ASSERT_TRUE(constellation); EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::i_range>(), (Range{-2.0, 2.0})); EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::q_range>(), (Range{-3.0, 3.0})); @@ -598,11 +662,14 @@ TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { Scene2D plot; plot.init(); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); - const auto power = Axis::Builder(root, Orientation::Vertical).build(); - const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); + const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach).build(); + auto power_axis = Axis::Builder(root, Orientation::Vertical, &attach).build(); + auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); + return std::tuple{frequency_axis, power_axis, value}; + }); ASSERT_TRUE(spectrum); - const auto state = std::dynamic_pointer_cast<::State_Strategy_Base>(spectrum); + const auto state = renderive_dynamic_owner_cast<::State_Strategy_Base>(spectrum); ASSERT_TRUE(state); EXPECT_EQ(state->state_revision(), 0U); spectrum->set<&Spectrum::Properties::frequency_point_size>(128); @@ -616,9 +683,12 @@ TEST(Renderive_Core2, EveryStatefulRenderableOwnsItsStateObserver) { Scene2D plot; plot.init(); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); - const auto power = Axis::Builder(root, Orientation::Vertical).build(); - const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); + const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach).build(); + auto power_axis = Axis::Builder(root, Orientation::Vertical, &attach).build(); + auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); + return std::tuple{frequency_axis, power_axis, value}; + }); ASSERT_TRUE(frequency); ASSERT_TRUE(power); ASSERT_TRUE(spectrum); @@ -660,10 +730,15 @@ TEST(Renderive_Core2, PlottableDataUpdatesReachKernelRealTimeDataStrategy) { Scene2D plot; plot.init(); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); - const auto power = Axis::Builder(root, Orientation::Vertical).build(); - const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); - const auto afterglow = Afterglow::Builder{}.build(root, frequency, power); + const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach).build(); + auto power_axis = Axis::Builder(root, Orientation::Vertical, &attach).build(); + auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); + return std::tuple{frequency_axis, power_axis, value}; + }); + const auto afterglow = build_initial(plot, [&](auto& attach) { + return Afterglow::Builder{&attach}.build(root, frequency, power); + }); ASSERT_TRUE(spectrum); ASSERT_TRUE(afterglow); const auto before = plot.frame_observer_snapshot().observation_count; @@ -726,32 +801,32 @@ TEST(Renderive_Core2, PartitionedPlotsBuildPropertyDrivenRenderGraphs) { plot.init(); plot.set_viewport_size({320, 180}); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal) - .set_pixel_length(320) - .set_coord_range({0.0, 10.0}) - .build(); - const auto power = Axis::Builder(root, Orientation::Vertical) - .set_pixel_length(180) - .set_coord_range({-120.0, 0.0}) - .build(); - const auto time = Time_Axis::Builder(root, Orientation::Vertical) - .set_pixel_length(180) - .build(); - const auto spectrum = Spectrum::Builder{} - .set<&Spectrum::Properties::partition_mode>( - Render_Partition_Mode::Fixed) - .set<&Spectrum::Properties::partition_count>(4) - .build(root, frequency, power); - const auto waterfall = Waterfall::Builder{} - .set<&Waterfall::Properties::partition_mode>( - Render_Partition_Mode::Fixed) - .set<&Waterfall::Properties::partition_count>(4) - .build(root, frequency, time); - const auto afterglow = Afterglow::Builder{} - .set<&Afterglow::Properties::partition_mode>( - Render_Partition_Mode::Fixed) - .set<&Afterglow::Properties::partition_count>(4) - .build(root, frequency, power); + const auto [frequency, power, time, spectrum, waterfall, afterglow] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach) + .set_pixel_length(320) + .set_coord_range({0.0, 10.0}) + .build(); + auto power_axis = Axis::Builder(root, Orientation::Vertical, &attach) + .set_pixel_length(180) + .set_coord_range({-120.0, 0.0}) + .build(); + auto time_axis = Time_Axis::Builder(root, Orientation::Vertical, &attach) + .set_pixel_length(180) + .build(); + auto spectrum_value = Spectrum::Builder{&attach} + .set<&Spectrum::Properties::partition_mode>(Render_Partition_Mode::Fixed) + .set<&Spectrum::Properties::partition_count>(4) + .build(root, frequency_axis, power_axis); + auto waterfall_value = Waterfall::Builder{&attach} + .set<&Waterfall::Properties::partition_mode>(Render_Partition_Mode::Fixed) + .set<&Waterfall::Properties::partition_count>(4) + .build(root, frequency_axis, time_axis); + auto afterglow_value = Afterglow::Builder{&attach} + .set<&Afterglow::Properties::partition_mode>(Render_Partition_Mode::Fixed) + .set<&Afterglow::Properties::partition_count>(4) + .build(root, frequency_axis, power_axis); + return std::tuple{frequency_axis, power_axis, time_axis, spectrum_value, waterfall_value, afterglow_value}; + }); std::array samples{}; spectrum->update_samples(samples); waterfall->append_row(0, samples); @@ -841,20 +916,21 @@ TEST(Renderive_Core2, DynamicWaterfallCaptureStressPreservesPlansSlotsAndExactSe plot.init(); plot.set_viewport_size({640, 360}); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal) - .set_pixel_length(640) - .set_coord_range({0.0, 32'767.0}) - .build(); - const auto time = Time_Axis::Builder(root, Orientation::Vertical) - .set_pixel_length(360) - .build(); - const auto waterfall = Waterfall::Builder{} - .set<&Waterfall::Properties::partition_mode>( - Render_Partition_Mode::Fixed) - .set<&Waterfall::Properties::partition_count>(64) - .set<&Waterfall::Properties::frequency_range>( - Range{0.0, 32'767.0}) - .build(root, frequency, time); + const auto [frequency, time, waterfall] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach) + .set_pixel_length(640) + .set_coord_range({0.0, 32'767.0}) + .build(); + auto time_axis = Time_Axis::Builder(root, Orientation::Vertical, &attach) + .set_pixel_length(360) + .build(); + auto value = Waterfall::Builder{&attach} + .set<&Waterfall::Properties::partition_mode>(Render_Partition_Mode::Fixed) + .set<&Waterfall::Properties::partition_count>(64) + .set<&Waterfall::Properties::frequency_range>(Range{0.0, 32'767.0}) + .build(root, frequency_axis, time_axis); + return std::tuple{frequency_axis, time_axis, value}; + }); std::array samples{}; for (std::size_t index = 0; index < samples.size(); ++index) samples[index] = static_cast(index % 256); @@ -899,9 +975,12 @@ TEST(Renderive_Core2, PaintOnlyStyleChangesPreservePrepareCache) { plot.init(); plot.set_viewport_size({320, 180}); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); - const auto power = Axis::Builder(root, Orientation::Vertical).build(); - const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); + const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach).build(); + auto power_axis = Axis::Builder(root, Orientation::Vertical, &attach).build(); + auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); + return std::tuple{frequency_axis, power_axis, value}; + }); const std::array samples{-80.0, -70.0, -60.0, -50.0}; spectrum->update_samples(samples); ASSERT_TRUE(plot.render_frame(true)); @@ -946,9 +1025,12 @@ TEST(Renderive_Core2, PlottableAxesAreDataDependenciesAndPaintOverlays) { Scene2D plot; plot.init(); const auto root = plot.root_renderable(); - const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); - const auto power = Axis::Builder(root, Orientation::Vertical).build(); - const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); + const auto [frequency, power, spectrum] = build_initial(plot, [&](auto& attach) { + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal, &attach).build(); + auto power_axis = Axis::Builder(root, Orientation::Vertical, &attach).build(); + auto value = Spectrum::Builder{&attach}.build(root, frequency_axis, power_axis); + return std::tuple{frequency_axis, power_axis, value}; + }); ASSERT_TRUE(spectrum); const auto topology = spectrum->scene().topology_snapshot(); const auto has_relationship = [](const auto& relationships, const auto* child, const auto* parent) { @@ -980,9 +1062,12 @@ TEST(Renderive_Core2, InteractionOverlayPaintsAboveItsAxes) { Scene2D plot; plot.init(); const auto root = plot.root_renderable(); - const auto horizontal = Axis::Builder(root, Orientation::Horizontal).build(); - const auto vertical = Axis::Builder(root, Orientation::Vertical).build(); - const auto selection = Selection_Rectangle_Overlay::Builder{}.build(root, horizontal, vertical); + const auto [horizontal, vertical, selection] = build_initial(plot, [&](auto& attach) { + auto horizontal_axis = Axis::Builder(root, Orientation::Horizontal, &attach).build(); + auto vertical_axis = Axis::Builder(root, Orientation::Vertical, &attach).build(); + auto value = Selection_Rectangle_Overlay::Builder{&attach}.build(root, horizontal_axis, vertical_axis); + return std::tuple{horizontal_axis, vertical_axis, value}; + }); ASSERT_TRUE(selection); const auto paint_order = selection->scene().paint_order_snapshot(); const auto position = [&paint_order](const auto* target) { diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index e7bcabb..5701bda 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -29,12 +29,12 @@ namespace renderive::web { namespace { template -void update_axis_state(const std::shared_ptr& axis, Update&& update) { - if (const auto numeric = std::dynamic_pointer_cast(axis)) { +void update_axis_state(const renderive_Owner& axis, Update&& update) { + if (const auto numeric = renderive_dynamic_owner_cast(axis)) { numeric->update([&](Axis_Properties& state) { std::forward(update)(static_cast(state)); }); - } else if (const auto time = std::dynamic_pointer_cast(axis)) { + } else if (const auto time = renderive_dynamic_owner_cast(axis)) { time->update([&](auto& state) { std::forward(update)(static_cast(state)); }); @@ -107,13 +107,24 @@ public: plot_.set_viewport_size({560, 320}); root_ = plot_.root_renderable(); root_->set_object_name("画布根节点"); - axes_node_ = plot_.create_renderable_node(root_, "坐标轴层"); - data_node_ = plot_.create_renderable_node(root_, "数据绘制层"); - overlay_node_ = plot_.create_renderable_node(root_, "交互覆盖层"); - axes_node_->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); + { + auto attach = plot_.attach_builder(); + const auto make_group = [&](std::string name) { + auto group = detail::make_renderable_group(true); + group->set_object_name(std::move(name)); + attach.attach(group); + attach.add_parent(::Scene_Base::Relationship::display, group, root_); + attach.add_parent(::Scene_Base::Relationship::dependency, group, root_); + return group; + }; + axes_node_ = make_group("坐标轴层"); + data_node_ = make_group("数据绘制层"); + overlay_node_ = make_group("交互覆盖层"); + axes_node_->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); + build_axes(attach); + build_primary(attach); + } attach_performance_overlay(plot_); - build_axes(); - build_primary(); apply_layout(); session_control_ = std::make_unique>( plot_, *primary_, feedback_policy_); @@ -922,7 +933,7 @@ private: }; write_web_performance_log(line.dump()); } - void build_axes() { + void build_axes(::Scene_Base::Attach_Builder& attach) { constexpr Color axis_color{118, 145, 184, 255}; Range coordinate_range{88'000'000.0, 108'000'000.0}; if (case_id_ == "sweep_spectrum") @@ -930,7 +941,7 @@ private: else if (case_id_ == "constellation") coordinate_range = {-1.25, 1.25}; if (case_id_ == "frequency_trace") { - time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Horizontal) + time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Horizontal, &attach) .set_visible_time_point_count(80) .set_tick_label_spacing_px(28) .set_time_format("mm:ss.zzz") @@ -938,7 +949,7 @@ private: .build(); } else if (case_id_ == "sweep_spectrum" || case_id_ == "constellation") { - numeric_domain_axis_ = Gallery_Axis::Builder(axes_node_, Orientation::Horizontal) + numeric_domain_axis_ = Gallery_Axis::Builder(axes_node_, Orientation::Horizontal, &attach) .set_coord_range(coordinate_range) .set_use_wheel(true) .set_use_drag(true) @@ -946,7 +957,7 @@ private: .build(); } else { - frequency_domain_axis_ = Gallery_Frequency_Axis::Builder(axes_node_, Orientation::Horizontal) + frequency_domain_axis_ = Gallery_Frequency_Axis::Builder(axes_node_, Orientation::Horizontal, &attach) .set_coord_range(coordinate_range) .set_use_wheel(true) .set_use_drag(true) @@ -954,7 +965,7 @@ private: .build(); } if (case_id_ == "waterfall") { - time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Vertical) + time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Vertical, &attach) .set_visible_time_point_count(80) .set_tick_label_spacing_px(28) .set_time_format("mm:ss.zzz") @@ -967,21 +978,21 @@ private: value_range = {-1.0, 1.0}; else if (case_id_ == "constellation") value_range = {-1.25, 1.25}; - value_axis_ = Gallery_Axis::Builder(axes_node_, Orientation::Vertical) + value_axis_ = Gallery_Axis::Builder(axes_node_, Orientation::Vertical, &attach) .set_coord_range(value_range) .set_label_precision(case_id_ == "constellation" ? 2 : 0) .set_color(axis_color) .build(); } if (case_id_ == "axis_lab") { - time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Horizontal) + time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Horizontal, &attach) .set_visible_time_point_count(80) .set_tick_label_spacing_px(28) .set_time_format("mm:ss.zzz") .set_color({69, 221, 190, 255}) .build(); } - const auto style = [this](const std::shared_ptr& axis) { + const auto style = [this](const renderive_Owner& axis) { if (!axis) return; update_axis_state(axis, [this](Axis_Base_Properties& value) { @@ -1001,9 +1012,9 @@ private: if (case_id_ == "axis_lab") style(time_axis_); } - void build_primary() { + void build_primary(::Scene_Base::Attach_Builder& attach) { if (case_id_ == "axis_lab") { - primary_ = std::static_pointer_cast(frequency_domain_axis_); + primary_ = renderive_static_owner_cast(frequency_domain_axis_); return; } if (case_id_ == "spectrum" || case_id_ == "selection_overlay") { @@ -1026,7 +1037,7 @@ private: properties.marker_pen = {color_from_hex("#ff7a59"), 1.2}; properties.middle_frequency_pen = {color_from_hex("#6bc8ff"), 1.0, Line_Style::Dash}; properties.sweep_region_brush = {color_from_hex("#293257", 80), Brush_Style::Solid}; - spectrum_ = Gallery_Spectrum::Builder{properties}.build( + spectrum_ = Gallery_Spectrum::Builder{properties, &attach}.build( data_node_, frequency_domain_axis_, value_axis_); primary_ = spectrum_; if (case_id_ == "selection_overlay") { @@ -1034,7 +1045,7 @@ private: overlay.label_font = {12.0, 500, false}; overlay.selection_brush = {color_from_hex("#173b66", 70), Brush_Style::Solid}; overlay.selection_border_pen = {color_from_hex("#7dd3fc"), 1.0, Line_Style::Dash}; - selection_ = Gallery_Selection_Rectangle_Overlay::Builder{overlay}.build( + selection_ = Gallery_Selection_Rectangle_Overlay::Builder{overlay, &attach}.build( overlay_node_, frequency_domain_axis_, value_axis_); primary_ = selection_; } @@ -1045,7 +1056,7 @@ private: properties.frequency_range = {88'000'000.0, 108'000'000.0}; properties.power_range = {-120.0, -20.0}; properties.frequency_bin_count = 256; - waterfall_ = Gallery_Waterfall::Builder{properties}.build( + waterfall_ = Gallery_Waterfall::Builder{properties, &attach}.build( data_node_, frequency_domain_axis_, time_axis_); primary_ = waterfall_; return; @@ -1057,7 +1068,7 @@ private: properties.frequency_point_size = 192; properties.power_point_size = 96; properties.attenuation_rate = 0.18; - afterglow_ = Gallery_Afterglow::Builder{properties}.build( + afterglow_ = Gallery_Afterglow::Builder{properties, &attach}.build( data_node_, frequency_domain_axis_, value_axis_); primary_ = afterglow_; return; @@ -1069,7 +1080,7 @@ private: properties.block_count = 8; properties.pen = {color_from_hex("#ffd166"), 1.8}; properties.current_frequency_pen = {color_from_hex("#ff5d73"), 2.0}; - sweep_ = Gallery_Sweep_Spectrum::Builder{properties}.build( + sweep_ = Gallery_Sweep_Spectrum::Builder{properties, &attach}.build( data_node_, numeric_domain_axis_, value_axis_); primary_ = sweep_; return; @@ -1078,7 +1089,7 @@ private: Frequency_Trace_Properties properties; properties.pen = {color_from_hex("#35e6b2"), 2.0, Line_Style::Solid, Line_Cap::Round, Line_Join::Round}; - trace_ = Gallery_Frequency_Trace::Builder{properties}.build( + trace_ = Gallery_Frequency_Trace::Builder{properties, &attach}.build( data_node_, time_axis_, value_axis_); primary_ = trace_; return; @@ -1090,19 +1101,19 @@ private: properties.point_color = color_from_hex("#49e6c3"); properties.anchor_color = color_from_hex("#ffd166"); properties.point_lifetime_ms = 1800; - constellation_ = Gallery_Constellation_Diagram::Builder{properties}.build( + constellation_ = Gallery_Constellation_Diagram::Builder{properties, &attach}.build( data_node_, numeric_domain_axis_, value_axis_); primary_ = constellation_; } } - std::shared_ptr horizontal_axis() const { + renderive_Owner horizontal_axis() const { if (frequency_domain_axis_) return frequency_domain_axis_; if (numeric_domain_axis_) return numeric_domain_axis_; return time_axis_; } - std::shared_ptr vertical_axis() const { + renderive_Owner vertical_axis() const { if (case_id_ == "waterfall") return time_axis_; return value_axis_; @@ -1115,7 +1126,7 @@ private: const int bottom = 46; const int width = std::max(1, viewport.width - left - right); const int height = std::max(1, viewport.height - top - bottom); - const auto layout_axis = [=](const std::shared_ptr& target) { + const auto layout_axis = [=](const renderive_Owner& target) { if (!target) return; update_axis_state(target, [=](Axis_Base_Properties& axis) { @@ -1253,22 +1264,22 @@ private: Gallery_Controls controls_; std::chrono::steady_clock::time_point performance_started_; std::chrono::steady_clock::time_point last_performance_log_; - std::shared_ptr root_; - std::shared_ptr axes_node_; - std::shared_ptr data_node_; - std::shared_ptr overlay_node_; - std::shared_ptr numeric_domain_axis_; - std::shared_ptr frequency_domain_axis_; - std::shared_ptr value_axis_; - std::shared_ptr time_axis_; - std::shared_ptr primary_; - std::shared_ptr spectrum_; - std::shared_ptr waterfall_; - std::shared_ptr trace_; - std::shared_ptr afterglow_; - std::shared_ptr sweep_; - std::shared_ptr selection_; - std::shared_ptr constellation_; + renderive_Owner root_; + renderive_Owner axes_node_; + renderive_Owner data_node_; + renderive_Owner overlay_node_; + renderive_Owner numeric_domain_axis_; + renderive_Owner frequency_domain_axis_; + renderive_Owner value_axis_; + renderive_Owner time_axis_; + renderive_Owner primary_; + renderive_Owner spectrum_; + renderive_Owner waterfall_; + renderive_Owner trace_; + renderive_Owner afterglow_; + renderive_Owner sweep_; + renderive_Owner selection_; + renderive_Owner constellation_; std::uint64_t frame_index_{}; std::uint64_t render_attempt_count_{}; std::uint64_t successful_render_count_{}; diff --git a/web_server/app/Web_Plot_Session.cpp b/web_server/app/Web_Plot_Session.cpp index 0993ab0..0e94744 100644 --- a/web_server/app/Web_Plot_Session.cpp +++ b/web_server/app/Web_Plot_Session.cpp @@ -50,13 +50,13 @@ double gaussian(double x, double center, double width) { } // namespace struct Web_Plot_Session::Impl { Scene2D plot; - std::shared_ptr spectrum_frequency_axis; - std::shared_ptr spectrum_power_axis; - std::shared_ptr waterfall_frequency_axis; - std::shared_ptr waterfall_time_axis; - std::shared_ptr spectrum; - std::shared_ptr waterfall; - std::shared_ptr selection; + renderive_Owner spectrum_frequency_axis; + renderive_Owner spectrum_power_axis; + renderive_Owner waterfall_frequency_axis; + renderive_Owner waterfall_time_axis; + renderive_Owner spectrum; + renderive_Owner waterfall; + renderive_Owner selection; Demo_Mode mode = Demo_Mode::Fm; double gain_db = 8.0; std::uint64_t frame_index{}; @@ -67,14 +67,24 @@ struct Web_Plot_Session::Impl { plot.set_max_render_fps(30.0); plot.set_viewport_size({960, 600}); const auto root = plot.root_renderable(); - const auto data = plot.create_renderable_node(root, "Web_Data"); - const auto axes = plot.create_renderable_node(root, "Web_Axes"); - const auto overlay = plot.create_renderable_node(root, "Web_Overlay"); - axes->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); constexpr Color axis_color{93, 116, 151, 255}; const Range initial_frequency{101'300'000.0, 103'700'000.0}; - spectrum_frequency_axis = - Frequency_Axis::Builder(axes, Orientation::Horizontal) + { + auto attach = plot.attach_builder(); + const auto make_group = [&](std::string name) { + auto group = detail::make_renderable_group(true); + group->set_object_name(std::move(name)); + attach.attach(group); + attach.add_parent(::Scene_Base::Relationship::display, group, root); + attach.add_parent(::Scene_Base::Relationship::dependency, group, root); + return group; + }; + const auto data = make_group("Web_Data"); + const auto axes = make_group("Web_Axes"); + const auto overlay = make_group("Web_Overlay"); + axes->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); + spectrum_frequency_axis = + Frequency_Axis::Builder(axes, Orientation::Horizontal, &attach) .set_coord_range(initial_frequency) .set_label_precision(2) .set_tick_length(8) @@ -84,7 +94,7 @@ struct Web_Plot_Session::Impl { .set_use_drag(true) .build(); spectrum_power_axis = - Axis::Builder(axes, Orientation::Vertical) + Axis::Builder(axes, Orientation::Vertical, &attach) .set_coord_range({-20.0, -120.0}) .set_label_precision(0) .set_tick_length(-8) @@ -93,7 +103,7 @@ struct Web_Plot_Session::Impl { .set_unit_text("dBm") .build(); waterfall_frequency_axis = - Frequency_Axis::Builder(axes, Orientation::Horizontal) + Frequency_Axis::Builder(axes, Orientation::Horizontal, &attach) .set_coord_range(initial_frequency) .set_label_precision(2) .set_tick_length(8) @@ -103,7 +113,7 @@ struct Web_Plot_Session::Impl { .set_use_drag(true) .build(); waterfall_time_axis = - Time_Axis::Builder(axes, Orientation::Vertical) + Time_Axis::Builder(axes, Orientation::Vertical, &attach) .set_visible_time_point_count(72) .set_tick_label_spacing_px(34) .set_time_format("mm:ss") @@ -111,7 +121,7 @@ struct Web_Plot_Session::Impl { .set_sub_tick_length(-4) .set_color(axis_color) .build(); - spectrum = Spectrum::Builder{} + spectrum = Spectrum::Builder{&attach} .set<&Spectrum::Properties::frequency_range>(initial_frequency) .set<&Spectrum::Properties::frequency_point_size>(768) .set<&Spectrum::Properties::center_frequency>(102'500'000.0) @@ -130,17 +140,18 @@ struct Web_Plot_Session::Impl { Color{56, 189, 248, 220}, 1.0, Line_Style::Dash }); - waterfall = Waterfall::Builder{} + waterfall = Waterfall::Builder{&attach} .set<&Waterfall::Properties::frequency_range>(initial_frequency) .set<&Waterfall::Properties::power_range>(Range{-120.0, -20.0}) .set<&Waterfall::Properties::frequency_bin_count>(768) .set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bilinear) .set<&Waterfall::Properties::color_map>(radio_color_map()) .build(data, waterfall_frequency_axis, waterfall_time_axis); - selection = Selection_Rectangle_Overlay::Builder{} + selection = Selection_Rectangle_Overlay::Builder{&attach} .set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{56, 189, 248, 36}, Brush_Style::Solid}) .set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{125, 211, 252, 230}, 1.0, Line_Style::Dash}) .build(overlay, spectrum_frequency_axis, spectrum_power_axis); + } apply_layout(plot.viewport_size()); plot.activate_view(); update_model();