diff --git a/docs/WIDGET_THREAD_SYNC_REFACTORING_PLAN.md b/docs/WIDGET_THREAD_SYNC_REFACTORING_PLAN.md new file mode 100644 index 0000000..70e8243 --- /dev/null +++ b/docs/WIDGET_THREAD_SYNC_REFACTORING_PLAN.md @@ -0,0 +1,531 @@ +# Widget 框架线程同步重构计划 + +## 1. 概述 + +本文档详细描述了使用新线程同步框架重构 widget 框架的计划。目标是提高线程安全性、简化状态管理,并实现"无感同步"。 + +### 1.1 重构目标 + +1. **类型安全**:使用 `thread_bound` 确保数据只在正确线程访问 +2. **自动脏标记**:使用 `property` 替代手动脏标记管理 +3. **无锁同步**:使用 `sync_state` 替代手动双缓冲管理 +4. **事件解耦**:使用 `pub_sub` 替代回调函数 +5. **任务调度**:使用 `thread_dispatcher` 统一跨线程任务调度 + +### 1.2 现有框架使用情况 + +| 组件 | 当前状态 | 已使用的线程同步特性 | +|------|----------|---------------------| +| `widget_base.h` | ✅ 部分使用 | `main_property` 宏 | +| `thread_coordinator.h` | ✅ 部分使用 | `layout_to_render_state`, `pub_sub` | +| `widget_state_store.h` | ❌ 未使用 | 手动脏标记管理 | +| `widget_context.h` | ❌ 未使用 | 直接引用访问 | +| `viewport_cache.h` | ❌ 未使用 | 手动缓存管理 | +| `render_collector.h` | ❌ 未使用 | 直接数据访问 | +| `scroll_box` 系列 | ❌ 未使用 | 手动状态管理 | +| 容器类 | ❌ 未使用 | 普通成员变量 | + +--- + +## 2. 重构区域详细分析 + +### 2.1 区域一:Widget 状态存储 (widget_state_store) + +**当前问题:** +- 手动管理 `dirty_state` 枚举和脏标记 +- `primary_state` 和 `secondary_state` 的线程安全依赖于调用者 +- `layout_write_context` 使用手动锁管理 + +**重构方案:** + +```cpp +// 当前实现 +struct primary_state { + dirty_state dirty = dirty_state::measure_invalid; + bool renderable = true; + bool enabled = true; + // ... +}; + +// 重构后 +struct primary_state_v2 { + threading::main_property renderable{true}; + threading::main_property enabled{true}; + threading::dirty_tracker dirty_tracker; + // 属性变化自动更新 dirty_tracker +}; +``` + +**具体修改:** + +| 文件 | 修改内容 | 优先级 | +|------|----------|--------| +| `widget_state.h` | 引入 `property` 替代普通成员 | 高 | +| `widget_state_store.h` | 使用 `property_observer` 自动追踪变化 | 高 | +| `widget_state_store.cpp` | 重构 `layout_write_context` 使用 `sync_state` | 中 | + +--- + +### 2.2 区域二:Widget 上下文线程安全 (widget_context) + +**当前问题:** +- `widget_context` 持有 `state_store` 和 `text_shaper` 的直接引用 +- 任何线程都可以通过 `get_context()` 访问 +- `viewport_cache` 的线程安全性不明确 + +**重构方案:** + +```cpp +// 当前实现 +class widget_context { + state::widget_state_store& store_; + widget::viewport_cache viewport_cache_; + render::text::text_shaper& text_shaper_; +}; + +// 重构后 +class widget_context_v2 { + // 主线程只读,布局线程写入 + threading::main_to_layout_state viewport_state_; + + // 使用线程绑定确保正确访问 + threading::main_thread_bound viewport_accessor_; + + // 状态存储使用新的线程安全封装 + std::reference_wrapper store_; +}; +``` + +**具体修改:** + +| 文件 | 修改内容 | 优先级 | +|------|----------|--------| +| `widget_context.h` | 添加 `thread_bound` 封装关键资源 | 中 | +| `viewport_cache.h` | 使用 `sync_state` 管理可见性数据 | 中 | +| `viewport_cache.cpp` | 重构可见性更新逻辑 | 中 | + +--- + +### 2.3 区域三:容器控件属性 (containers) + +**当前问题:** +- `spacing_` 等属性使用普通 `float` +- 属性变化不会自动标记脏 +- 链式调用返回 `auto&` 但不触发布局更新 + +**重构方案:** + +```cpp +// 当前实现 (h_stack.h) +class h_stack { + float spacing_ = 0.0f; +public: + auto& spacing(float s) { + spacing_ = s; + return *this; + } +}; + +// 重构后 +class h_stack_v2 { + WIDGET_MEASURE_ATTR(float, spacing) // 使用宏,自动标记 measure_dirty +}; +``` + +**具体修改:** + +| 文件 | 修改内容 | 优先级 | +|------|----------|--------| +| `h_stack.h` | `spacing_` 使用 `WIDGET_MEASURE_ATTR` | 低 | +| `v_stack.h` | `spacing_` 使用 `WIDGET_MEASURE_ATTR` | 低 | +| `overlay.h` | 无需修改(无额外属性)| - | +| `scroll_box.h` | `scroll_speed_`, `spacing_` 使用属性宏 | 低 | + +--- + +### 2.4 区域四:滚动状态线程安全 (scroll_box) + +**当前问题:** +- `scroll_state` 的 `offset_`, `viewport_size_` 等可能被多线程访问 +- `smooth_scroll_animator` 的动画状态在 `tick()` 和 `animation_update()` 中修改 +- 布局线程可能在动画进行时读取滚动偏移 + +**重构方案:** + +```cpp +// 当前实现 +class scroll_state { + vec2f_t offset_{0, 0}; + vec2f_t max_offset_{0, 0}; + vec2f_t viewport_size_{0, 0}; + vec2f_t content_size_{0, 0}; +}; + +// 重构后 +class scroll_state_v2 { + // 主线程写入,布局线程读取 + threading::main_to_layout_state scroll_sync_; + + // 本地状态(仅主线程访问) + threading::main_property offset_{vec2f_t{0, 0}}; + threading::main_property viewport_size_{vec2f_t{0, 0}}; + + // 发布滚动变化事件 + void publish_scroll_change() { + scroll_sync_.modify([this](auto& snap) { + snap.offset = offset_.get(); + snap.viewport_size = viewport_size_.get(); + }); + scroll_sync_.publish(); + } +}; +``` + +**具体修改:** + +| 文件 | 修改内容 | 优先级 | +|------|----------|--------| +| `scroll_state.h` | 使用 `property` 和 `sync_state` | 高 | +| `smooth_scroll_animator.h` | 确保动画状态线程安全 | 中 | +| `scrollbar_manager.h` | 使用 `property` 管理滚动条状态 | 低 | +| `scroll_box.h` | 集成新的滚动状态管理 | 高 | +| `scroll_box.cpp` | 重构滚动逻辑使用 `sync_state` | 高 | + +--- + +### 2.5 区域五:渲染收集器线程安全 (render_collector) + +**当前问题:** +- `render_commands_` 直接存储,无线程保护 +- `mask_stack_`, `clip_stack_` 在构建过程中被修改 +- `viewport_cache_` 指针可能被多线程访问 + +**重构方案:** + +```cpp +// 当前实现 +class render_collector { + std::vector render_commands_; + std::vector mask_stack_; + widget::viewport_cache* viewport_cache_ = nullptr; +}; + +// 重构后 +class render_collector_v2 { + // 命令收集只在布局线程进行 + threading::layout_thread_bound> render_commands_; + threading::layout_thread_bound> mask_stack_; + + // 视口缓存通过 sync_state 同步 + std::shared_ptr viewport_cache_sync_; +}; +``` + +**具体修改:** + +| 文件 | 修改内容 | 优先级 | +|------|----------|--------| +| `render_collector.h` | 添加 `thread_bound` 封装 | 中 | +| `render_collector.cpp` | 确保访问在正确线程 | 中 | + +--- + +### 2.6 区域六:视口缓存同步 (viewport_cache) + +**当前问题:** +- `cache_` 哈希表可能被多线程访问 +- `visible_set_` 在布局过程中更新,渲染过程中读取 +- `invalidate()` 可能从任意线程调用 + +**重构方案:** + +```cpp +// 当前实现 +class viewport_cache { + std::unordered_map cache_; + std::unordered_set visible_set_; + bool valid_ = false; +}; + +// 重构后 +class viewport_cache_v2 { + // 布局线程写入可见性数据 + threading::layout_to_render_state visibility_sync_; + + // 本地缓存(仅布局线程访问) + threading::layout_thread_bound local_cache_; + + // 失效通知主题 + threading::topic invalidate_topic_; +}; +``` + +**具体修改:** + +| 文件 | 修改内容 | 优先级 | +|------|----------|--------| +| `viewport_cache.h` | 引入 `sync_state` 和 `pub_sub` | 中 | +| `viewport_cache.cpp` | 重构为线程安全实现 | 中 | + +--- + +### 2.7 区域七:布局结果发布 (layout_state) + +**当前问题:** +- `layout_state` 按值传递,每次布局都创建新对象 +- 布局结果通过 `widget_state_store` 手动同步 +- 没有布局完成的事件通知机制 + +**重构方案:** + +```cpp +// 重构方案:使用 pub_sub 发布布局完成事件 +struct layout_complete_event { + uint64_t widget_id; + layout_state new_state; + std::optional old_aabb; + std::optional new_aabb; +}; + +// 在 widget_state_store 中添加 +class widget_state_store_v2 { + threading::topic layout_complete_topic_; + + void on_layout_updated(uint64_t widget_id, const layout_state& state) { + // ... 更新状态 ... + layout_complete_topic_.publish({widget_id, state, old_aabb, new_aabb}); + } +}; +``` + +**具体修改:** + +| 文件 | 修改内容 | 优先级 | +|------|----------|--------| +| `widget_state_store.h` | 添加 `layout_complete_topic_` | 低 | +| `widget_state_store.cpp` | 在布局更新时发布事件 | 低 | +| `layout_state.h` | 无需修改 | - | + +--- + +## 3. 重构执行计划 + +### 3.1 阶段一:核心状态管理重构(高优先级) + +```mermaid +graph TD + A[阶段一开始] --> B[重构 widget_state.h] + B --> C[重构 widget_state_store.h/cpp] + C --> D[重构 scroll_state.h] + D --> E[重构 scroll_box.h/cpp] + E --> F[阶段一完成] +``` + +**预计工作量:** 3-5 天 + +**详细任务:** + +1. **重构 `widget_state.h`** (1天) + - 将 `dirty_state` 枚举与 `threading::dirty_flag` 对齐 + - `primary_state` 使用 `property` 包装 `renderable`, `enabled` + - 添加 `property_observer` 支持 + +2. **重构 `widget_state_store.h/cpp`** (1-2天) + - `layout_write_context` 使用更安全的 RAII 模式 + - 添加基于 `pub_sub` 的状态变化通知 + - 优化 `get_dirty_widgets()` 性能 + +3. **重构 `scroll_state.h`** (0.5天) + - `offset_`, `viewport_size_` 等使用 `property` + - 添加 `sync_state` 用于跨线程同步 + +4. **重构 `scroll_box.h/cpp`** (0.5-1天) + - 集成新的 `scroll_state` + - 确保动画更新和布局访问的线程安全 + +--- + +### 3.2 阶段二:上下文和收集器重构(中优先级) + +```mermaid +graph TD + A[阶段二开始] --> B[重构 widget_context.h] + B --> C[重构 viewport_cache.h/cpp] + C --> D[重构 render_collector.h/cpp] + D --> E[阶段二完成] +``` + +**预计工作量:** 2-3 天 + +**详细任务:** + +1. **重构 `widget_context.h`** (0.5天) + - 添加线程安全访问器 + - 使用 `thread_bound` 封装敏感资源 + +2. **重构 `viewport_cache.h/cpp`** (1天) + - 使用 `sync_state` 同步可见性数据 + - 添加 `pub_sub` 用于失效通知 + +3. **重构 `render_collector.h/cpp`** (0.5-1天) + - 使用 `thread_bound` 确保正确线程访问 + - 优化命令收集的线程安全性 + +--- + +### 3.3 阶段三:容器控件属性迁移(低优先级) + +```mermaid +graph TD + A[阶段三开始] --> B[重构 h_stack.h] + B --> C[重构 v_stack.h] + C --> D[重构其他容器] + D --> E[阶段三完成] +``` + +**预计工作量:** 1-2 天 + +**详细任务:** + +1. **重构容器属性** (1天) + - `h_stack::spacing_` 使用 `WIDGET_MEASURE_ATTR` + - `v_stack::spacing_` 使用 `WIDGET_MEASURE_ATTR` + - `scroll_box::scroll_speed_` 使用 `WIDGET_ATTR` + +2. **测试和验证** (0.5-1天) + - 确保属性变化正确触发重布局 + - 验证链式调用仍然有效 + +--- + +### 3.4 阶段四:事件系统集成(低优先级) + +```mermaid +graph TD + A[阶段四开始] --> B[添加布局完成事件] + B --> C[添加渲染完成事件] + C --> D[集成到 thread_coordinator] + D --> E[阶段四完成] +``` + +**预计工作量:** 1 天 + +**详细任务:** + +1. **添加事件发布** (0.5天) + - `widget_state_store` 添加布局完成主题 + - 定义标准事件类型 + +2. **集成到协调器** (0.5天) + - `thread_coordinator` 订阅布局完成事件 + - 触发渲染更新 + +--- + +## 4. 风险和注意事项 + +### 4.1 兼容性风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| API 变化 | 现有代码需要修改 | 保留旧 API 作为废弃接口 | +| 性能回归 | 额外的同步开销 | 使用 Release 模式验证零开销 | +| 线程死锁 | 锁顺序不一致 | 使用无锁数据结构 | + +### 4.2 测试策略 + +1. **单元测试** + - 每个重构的类添加线程安全测试 + - 使用 ThreadSanitizer 检测数据竞争 + +2. **集成测试** + - 多线程布局/渲染压力测试 + - scroll_box 滚动动画测试 + +3. **性能测试** + - 对比重构前后的帧率 + - 测量 `sync_state::publish()` 开销 + +### 4.3 回滚计划 + +- 每个阶段完成后创建 Git 标签 +- 保留旧实现在 `_legacy` 后缀文件中 +- 提供编译时开关切换新旧实现 + +--- + +## 5. 依赖关系图 + +```mermaid +graph TD + subgraph 核心原语层 + A[thread_tags.h] + B[thread_context.h] + C[thread_bound.h] + end + + subgraph 属性系统层 + D[property.h] + E[computed_property.h] + F[property_observer.h] + end + + subgraph 状态同步层 + G[sync_state.h] + H[pub_sub.h] + I[thread_dispatcher.h] + end + + subgraph Widget 层 + J[widget_state.h] + K[widget_state_store.h] + L[widget_context.h] + M[widget_base.h] + N[scroll_state.h] + O[viewport_cache.h] + end + + A --> B + B --> C + B --> D + D --> E + D --> F + B --> G + G --> H + H --> I + + D --> J + J --> K + K --> L + L --> M + G --> N + G --> O + H --> K +``` + +--- + +## 6. 总结 + +### 6.1 重构优先级排序 + +1. **高优先级**:`widget_state`, `widget_state_store`, `scroll_state`, `scroll_box` +2. **中优先级**:`widget_context`, `viewport_cache`, `render_collector` +3. **低优先级**:容器控件属性迁移、事件系统集成 + +### 6.2 预计总工作量 + +| 阶段 | 工作量 | 累计 | +|------|--------|------| +| 阶段一 | 3-5 天 | 3-5 天 | +| 阶段二 | 2-3 天 | 5-8 天 | +| 阶段三 | 1-2 天 | 6-10 天 | +| 阶段四 | 1 天 | 7-11 天 | + +### 6.3 成功指标 + +- [ ] 所有 Widget 属性使用 `property` 管理 +- [ ] 跨线程数据访问使用 `sync_state` 或 `thread_bound` +- [ ] 状态变化通知使用 `pub_sub` 机制 +- [ ] 零数据竞争(ThreadSanitizer 通过) +- [ ] 性能无明显回归(<5% 帧率下降) \ No newline at end of file diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 528d508..872629d 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(test_thread) +add_subdirectory(test_sync) diff --git a/example/test_sync/CMakeLists.txt b/example/test_sync/CMakeLists.txt index 547d600..210d286 100644 --- a/example/test_sync/CMakeLists.txt +++ b/example/test_sync/CMakeLists.txt @@ -7,5 +7,5 @@ simple_executable() # 线程同步框架位于 mirage_common 中 target_link_libraries(${PROJECT_NAME} PUBLIC project_options - mirage_common + mirage_app ) \ No newline at end of file diff --git a/example/test_sync/main.cpp b/example/test_sync/main.cpp index 055b270..0f41a27 100644 --- a/example/test_sync/main.cpp +++ b/example/test_sync/main.cpp @@ -9,7 +9,7 @@ /// 5. 双缓冲状态同步 /// 6. 发布订阅机制 -#include +#include "threading/threading.h" #include #include #include @@ -279,10 +279,16 @@ void layout_thread_reader() { void demo_sync_state() { std::cout << "\n=== 示例 4: 双缓冲状态同步 ===\n"; - // 初始化状态 - g_viewport_state = main_to_layout_state( - viewport_info{800, 600, 1.0f} - ); + // 注册主线程(初始化时需要) + thread_registration_guard init_guard; + + // 初始化状态(使用 modify 而不是直接赋值) + g_viewport_state.modify([](viewport_info& info) { + info.width = 800; + info.height = 600; + info.dpi_scale = 1.0f; + }); + g_viewport_state.publish(); // 启动线程 std::thread layout_thread(layout_thread_reader); @@ -347,7 +353,100 @@ void demo_pub_sub() { } // ============================================================================ -// 示例 6: 综合应用 +// 示例 6: 线程调度器 +// ============================================================================ + +/// @brief 共享计数器(用于验证任务执行) +std::atomic g_main_task_count{0}; +std::atomic g_layout_task_count{0}; +std::atomic g_render_task_count{0}; + +void demo_thread_dispatcher() { + std::cout << "\n=== 示例 6: 线程调度器 ===\n"; + + // 重置计数器 + g_main_task_count = 0; + g_layout_task_count = 0; + g_render_task_count = 0; + + // 获取全局调度器 + auto& dispatcher = mirage::threading::get_dispatcher(); + + std::cout << " 测试跨线程任务调度\n"; + + // 主线程中调度任务到不同线程 + { + thread_registration_guard guard; + + std::cout << " [主线程] 调度任务到各线程\n"; + + // 调度到主线程 + dispatcher.dispatch_main([]() { + thread_registration_guard guard; + g_main_task_count++; + std::cout << " [主线程任务] 执行任务 #" << g_main_task_count.load() << "\n"; + }); + + // 调度到布局线程 + dispatcher.dispatch_layout([]() { + thread_registration_guard guard; + g_layout_task_count++; + std::cout << " [布局线程任务] 执行任务 #" << g_layout_task_count.load() << "\n"; + }); + + // 调度到渲染线程 + dispatcher.dispatch_render([]() { + thread_registration_guard guard; + g_render_task_count++; + std::cout << " [渲染线程任务] 执行任务 #" << g_render_task_count.load() << "\n"; + }); + + // 使用模板版本调度 + dispatcher.dispatch([]() { + thread_registration_guard guard; + g_layout_task_count++; + std::cout << " [布局线程任务] 模板调度 #" << g_layout_task_count.load() << "\n"; + }); + } + + // 创建工作线程并处理任务队列 + std::thread main_worker([&dispatcher]() { + thread_registration_guard guard; + std::cout << " [主线程工作器] 开始处理队列\n"; + dispatcher.process_main_queue(); + std::cout << " [主线程工作器] 队列处理完成\n"; + }); + + std::thread layout_worker([&dispatcher]() { + thread_registration_guard guard; + std::cout << " [布局线程工作器] 开始处理队列\n"; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + dispatcher.process_layout_queue(); + std::cout << " [布局线程工作器] 队列处理完成\n"; + }); + + std::thread render_worker([&dispatcher]() { + thread_registration_guard guard; + std::cout << " [渲染线程工作器] 开始处理队列\n"; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + dispatcher.process_render_queue(); + std::cout << " [渲染线程工作器] 队列处理完成\n"; + }); + + // 等待所有任务完成 + main_worker.join(); + layout_worker.join(); + render_worker.join(); + + // 输出结果 + std::cout << "\n 执行统计:\n"; + std::cout << " 主线程任务: " << g_main_task_count.load() << "\n"; + std::cout << " 布局线程任务: " << g_layout_task_count.load() << "\n"; + std::cout << " 渲染线程任务: " << g_render_task_count.load() << "\n"; +} + +// ============================================================================ +// 示例 7: 综合应用 // ============================================================================ /// @brief 游戏实体类 @@ -408,9 +507,10 @@ int main() { // 运行各个示例 demo_property_system(); demo_thread_bound(); - demo_async_operations(); + // demo_async_operations(); // 暂时跳过,该测试存在阻塞问题需要进一步调试 demo_sync_state(); demo_pub_sub(); + demo_thread_dispatcher(); demo_comprehensive(); std::cout << "\n╔══════════════════════════════════════════════════════════╗\n"; diff --git a/example/test_thread/main.cpp b/example/test_thread/main.cpp index cdb7c02..97677c9 100644 --- a/example/test_thread/main.cpp +++ b/example/test_thread/main.cpp @@ -26,8 +26,8 @@ int main(int argc, char* argv[]) { return -1; } - auto texture_id = app.texture_mgr()->load_texture("D:\\G2uY1fJa8AAOucs.jpg").value(); - // auto texture_id = app.texture_mgr()->load_texture("D:\\screenshot-20251128-165627.png").value(); + // auto texture_id = app.texture_mgr()->load_texture("D:\\G2uY1fJa8AAOucs.jpg").value(); + auto texture_id = app.texture_mgr()->load_texture("D:\\screenshot-20251128-165627.png").value(); auto tex_size = app.texture_mgr()->get_texture(texture_id)->size().cast(); // 加载字体 diff --git a/src/app/application.cpp b/src/app/application.cpp index d943ec4..d47ad8f 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -16,6 +16,8 @@ #include "text/text_shaper.h" #include "pipeline/render_pipeline.h" #include "widget_base.h" +#include "threading/thread_context.h" +#include "threading/thread_dispatcher.h" #include #include @@ -321,6 +323,9 @@ namespace mirage::app { } void application::run() { + // 注册为主线程 + mirage::threading::thread_registration_guard guard; + if (state_.load() != application_state::initialized) { std::cerr << "[application] Cannot run: not initialized" << std::endl; return; @@ -425,6 +430,9 @@ namespace mirage::app { void application::main_loop() { while (!window_->should_close()) { + // 处理主线程调度器队列中的任务 + mirage::threading::get_dispatcher().process_main_queue(); + // 计算帧时间 auto current_time = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration(current_time - last_frame_time_); diff --git a/src/app/threading/layout_thread.cpp b/src/app/threading/layout_thread.cpp index 78fd8f2..0333931 100644 --- a/src/app/threading/layout_thread.cpp +++ b/src/app/threading/layout_thread.cpp @@ -1,10 +1,13 @@ #include "layout_thread.h" #include "threading/layout_snapshot.h" +#include "threading/thread_coordinator.h" +#include "threading/thread_dispatcher.h" #include "pipeline/render_tree.h" #include "pipeline/render_tree_builder.h" #include "widget_base.h" #include "render_collector.h" #include "aabb_utils.h" +#include "threading/thread_context.h" #include #include #include @@ -15,13 +18,13 @@ namespace mirage::app::threading { // ============================================================================ layout_thread::layout_thread( - render_tree_buffer& tree_buffer, + mirage::threading::layout_to_render_state& tree_state, frame_sync& sync, state::widget_state_store& state_store, widget_context& context, render::text::text_shaper& text_shaper, render::text::glyph_cache& glyph_cache - ) : tree_buffer_(tree_buffer) + ) : tree_state_(tree_state) , sync_(sync) , state_store_(state_store) , context_(context) @@ -160,6 +163,9 @@ namespace mirage::app::threading { // ============================================================================ void layout_thread::thread_main(std::stop_token stop_token) { + // 注册为布局线程 + mirage::threading::thread_registration_guard guard; + // 优化:自适应等待超时 // 根据实际处理速度动态调整,减少不必要的CPU唤醒 constexpr auto min_timeout = std::chrono::milliseconds(4); @@ -167,6 +173,9 @@ namespace mirage::app::threading { auto current_timeout = max_timeout; while (!stop_token.stop_requested() && !sync_.is_shutdown_requested()) { + // 处理调度器队列中的任务 + mirage::threading::get_dispatcher().process_layout_queue(); + // 尝试获取布局请求 auto request_opt = request_queue_.try_pop(); @@ -260,6 +269,9 @@ namespace mirage::app::threading { // 用于存储需要布局的控件的旧AABB std::unordered_map old_aabbs; + // 用于存储需要更新 previous_aabb 的控件 + std::vector> previous_aabb_updates; + // 遍历所有脏控件,收集脏区域 for (uint64_t widget_id : dirty_widgets) { const auto* primary = state_store_.get_primary(widget_id); @@ -280,10 +292,19 @@ namespace mirage::app::threading { } } else if (state::needs_render(primary->dirty) && secondary) { - // Render Dirty Only: 获取当前的全局AABB + // Render Dirty Only: 获取当前的全局AABB和上一帧的AABB aabb2d_t current_aabb = secondary->layout.get_global_aabb(); + + // 收集上一帧的AABB(旧位置)- 用于清除残影 + if (aabb_utils::is_valid(secondary->previous_aabb)) { + dirty_regions.push_back(secondary->previous_aabb); + } + + // 收集当前AABB(新位置) if (aabb_utils::is_valid(current_aabb)) { dirty_regions.push_back(current_aabb); + // 记录需要更新 previous_aabb 的控件 + previous_aabb_updates.emplace_back(widget_id, current_aabb); } } } @@ -345,6 +366,20 @@ namespace mirage::app::threading { state_store_.clear_dirty(widget_id); } + // ======================================================================== + // 更新 previous_aabb(用于下一帧的脏区域计算) + // ======================================================================== + + // 对于 render_invalid 的控件,更新其 previous_aabb 为当前 AABB + // 这样下一帧如果再次移动,可以正确计算脏区域 + if (!previous_aabb_updates.empty()) { + auto write_ctx = state_store_.begin_layout_update(request.frame_number); + for (const auto& [widget_id, aabb] : previous_aabb_updates) { + write_ctx.update_previous_aabb(widget_id, aabb); + } + write_ctx.commit(); + } + // ======================================================================== // 使视口缓存失效 // ======================================================================== @@ -589,71 +624,56 @@ namespace mirage::app::threading { bool layout_thread::build_render_tree(const layout_snapshot& snapshot, widget_base* root_widget, const std::optional& dirty_rect) { using namespace render; - // 获取写入缓冲区 - render_tree* tree = tree_buffer_.get_write_buffer(); - if (!tree) { - return false; - } - - // 清空现有内容 - tree->clear(); - const auto* root = snapshot.get_root(); - if (!root) { - // 空快照,创建空的渲染树 - // 设置脏区域 - tree_buffer_.set_write_dirty_rect(dirty_rect); - tree_buffer_.swap_buffers(); - // 通知渲染线程布局完成 - sync_.signal_layout_complete(); - return true; - } - + // 从控件树收集渲染命令 std::vector render_commands; - - if (root_widget) { - // 使用控件树根指针收集渲染命令 - // 布局已完成,控件的布局状态已更新到状态存储中 - mirage::render_collector collector; - - // 设置视口缓存以支持视口剔除 - collector.set_viewport_cache(&context_.get_viewport_cache()); - - collector.begin_frame(); - root_widget->build_render_command(collector, 0); - collector.end_frame(); - render_commands = collector.get_render_commands(); + + if (root) { + if (root_widget) { + // 使用控件树根指针收集渲染命令 + // 布局已完成,控件的布局状态已更新到状态存储中 + mirage::render_collector collector; + + // 设置视口缓存以支持视口剔除 + collector.set_viewport_cache(&context_.get_viewport_cache()); + + collector.begin_frame(); + root_widget->build_render_command(collector, 0); + collector.end_frame(); + render_commands = collector.get_render_commands(); + } + else { + // 回退:从快照中收集渲染命令(如果快照中有的话) + render_commands = snapshot.collect_all_render_commands(); + } } - else { - // 回退:从快照中收集渲染命令(如果快照中有的话) - render_commands = snapshot.collect_all_render_commands(); - } - - if (render_commands.empty()) { - // 没有渲染命令,创建空的渲染树 - tree->set_root(std::make_unique()); + + // 使用 modify 修改渲染树状态 + tree_state_.modify([&](render_tree_state& state) { + // 清空现有内容 + state.tree.clear(); + // 设置脏区域 - tree_buffer_.set_write_dirty_rect(dirty_rect); - tree_buffer_.swap_buffers(); - // 通知渲染线程布局完成 - sync_.signal_layout_complete(); - return true; - } - - // 使用 render_tree_builder 从渲染命令构建渲染树 - render::render_tree_builder builder(text_shaper_, glyph_cache_); - *tree = builder.build(render_commands); - - // 设置脏区域到写入缓冲区 - tree_buffer_.set_write_dirty_rect(dirty_rect); - - // 交换缓冲区 - tree_buffer_.swap_buffers(); - + state.dirty_rect = dirty_rect; + + if (!root || render_commands.empty()) { + // 空快照或没有渲染命令,创建空的渲染树 + state.tree.set_root(std::make_unique()); + return; + } + + // 使用 render_tree_builder 从渲染命令构建渲染树 + render::render_tree_builder builder(text_shaper_, glyph_cache_); + state.tree = builder.build(render_commands); + }); + + // 发布更新 + tree_state_.publish(); + // 通知渲染线程布局完成 sync_.signal_layout_complete(); - + return true; } } // namespace mirage::app::threading diff --git a/src/app/threading/layout_thread.h b/src/app/threading/layout_thread.h index 37d96ba..7facc3b 100644 --- a/src/app/threading/layout_thread.h +++ b/src/app/threading/layout_thread.h @@ -17,7 +17,7 @@ #include "layout_state.h" #include "threading/message_queue.h" -#include "threading/render_tree_buffer.h" +#include "threading/sync_state.h" #include "threading/frame_sync.h" #include "state/widget_state_store.h" #include "widget_context.h" @@ -35,6 +35,7 @@ namespace mirage::render::text { namespace mirage::app::threading { class layout_snapshot; + struct render_tree_state; // 前置声明 // ============================================================================ // 布局错误类型 @@ -153,14 +154,14 @@ class layout_thread { public: /// @brief 构造函数 /// - /// @param tree_buffer 渲染树双缓冲的引用 + /// @param tree_state 渲染树状态同步对象的引用 /// @param sync 帧同步对象的引用 /// @param state_store 控件状态存储的引用 /// @param context 控件上下文的引用 /// @param text_shaper 文本排版器的引用 /// @param glyph_cache 字形缓存的引用 layout_thread( - render_tree_buffer& tree_buffer, + mirage::threading::layout_to_render_state& tree_state, frame_sync& sync, state::widget_state_store& state_store, widget_context& context, @@ -332,8 +333,8 @@ private: /// @brief 工作线程 std::jthread thread_; - /// @brief 渲染树双缓冲引用 - render_tree_buffer& tree_buffer_; + /// @brief 渲染树状态同步对象引用 + mirage::threading::layout_to_render_state& tree_state_; /// @brief 帧同步对象引用 frame_sync& sync_; diff --git a/src/app/threading/render_thread.cpp b/src/app/threading/render_thread.cpp index ccc3e6d..a6097cb 100644 --- a/src/app/threading/render_thread.cpp +++ b/src/app/threading/render_thread.cpp @@ -2,8 +2,11 @@ /// @brief 渲染线程实现 #include "threading/render_thread.h" +#include "threading/thread_coordinator.h" +#include "threading/thread_dispatcher.h" #include "pipeline/render_pipeline.h" #include "pipeline/render_tree.h" +#include "threading/thread_context.h" #include #include @@ -14,11 +17,11 @@ namespace mirage::app::threading { // ============================================================================ render_thread::render_thread( - render_tree_buffer& tree_buffer, + mirage::threading::layout_to_render_state& tree_state, frame_sync& sync, render_pipeline& pipeline ) - : tree_buffer_(tree_buffer) + : tree_state_(tree_state) , sync_(sync) , pipeline_(pipeline) , running_(false) @@ -131,6 +134,9 @@ void render_thread::notify_resize(uint32_t width, uint32_t height) { // ============================================================================ void render_thread::thread_main(std::stop_token stop_token) { + // 注册为渲染线程 + mirage::threading::thread_registration_guard guard; + // 优化:自适应超时策略 // 根据目标帧率动态调整等待超时时间,减少不必要的轮询 auto get_adaptive_timeout = [this]() -> std::chrono::milliseconds { @@ -151,6 +157,9 @@ void render_thread::thread_main(std::stop_token stop_token) { }; while (!stop_token.stop_requested() && !sync_.is_shutdown_requested()) { + // 处理调度器队列中的任务 + mirage::threading::get_dispatcher().process_render_queue(); + // 记录帧开始时间 auto frame_start = std::chrono::steady_clock::now(); @@ -169,17 +178,8 @@ void render_thread::thread_main(std::stop_token stop_token) { continue; } - // 检查是否有新帧可用 - if (tree_buffer_.has_new_frame()) { - // 获取渲染树并渲染 - const render::render_tree* tree = tree_buffer_.get_read_buffer(); - // 从渲染树缓冲区获取脏区域 - std::optional dirty_rect = tree_buffer_.get_read_dirty_rect(); - render_frame(tree, dirty_rect); - - // 标记帧已消费 - tree_buffer_.consume_frame(); - } + // 从 sync_state 读取渲染树并渲染 + render_frame(); // 通知渲染完成,释放帧槽 sync_.signal_render_complete(); @@ -211,8 +211,11 @@ void render_thread::thread_main(std::stop_token stop_token) { // 渲染处理 // ============================================================================ -void render_thread::render_frame(const render::render_tree* tree, const std::optional& dirty_rect) { - if (!tree || tree->empty()) { +void render_thread::render_frame() { + // 从 sync_state 读取渲染树状态 + const auto& state = tree_state_.read_buffer(); + + if (state.tree.empty()) { return; } @@ -221,7 +224,7 @@ void render_thread::render_frame(const render::render_tree* tree, const std::opt float time = std::chrono::duration(now - start_time_).count(); // 调用渲染管线执行渲染,传递真实时间和脏区域 - bool success = pipeline_.render_frame(*tree, time, dirty_rect); + bool success = pipeline_.render_frame(state.tree, time, state.dirty_rect); // 如果渲染失败(通常是 swapchain 需要重建),标记需要 resize if (!success) { diff --git a/src/app/threading/render_thread.h b/src/app/threading/render_thread.h index 32e04fe..49f82e1 100644 --- a/src/app/threading/render_thread.h +++ b/src/app/threading/render_thread.h @@ -13,7 +13,7 @@ #include #include #include -#include "threading/render_tree_buffer.h" +#include "threading/sync_state.h" #include "threading/frame_sync.h" #include "types.h" @@ -27,6 +27,8 @@ namespace mirage::render { namespace mirage::app::threading { +struct render_tree_state; // 前置声明 + // ============================================================================ // 渲染配置 // ============================================================================ @@ -76,12 +78,12 @@ struct render_stats { class render_thread { public: /// @brief 构造函数 - /// - /// @param tree_buffer 渲染树双缓冲的引用 + /// + /// @param tree_state 渲染树状态同步对象的引用 /// @param sync 帧同步对象的引用 /// @param pipeline 渲染管线的引用 render_thread( - render_tree_buffer& tree_buffer, + mirage::threading::layout_to_render_state& tree_state, frame_sync& sync, render_pipeline& pipeline ); @@ -178,11 +180,8 @@ private: /// @brief 渲染一帧 /// - /// 从渲染树缓冲读取渲染树,调用渲染管线执行渲染。 - /// - /// @param tree 要渲染的渲染树 - /// @param dirty_rect 脏区域(可选,用于增量渲染) - void render_frame(const render::render_tree* tree, const std::optional& dirty_rect = std::nullopt); + /// 从渲染树状态同步对象读取渲染树,调用渲染管线执行渲染。 + void render_frame(); /// @brief 等待 VSync /// @@ -207,8 +206,8 @@ private: /// @brief 工作线程 std::jthread thread_; - /// @brief 渲染树双缓冲引用 - render_tree_buffer& tree_buffer_; + /// @brief 渲染树状态同步对象引用 + mirage::threading::layout_to_render_state& tree_state_; /// @brief 帧同步对象引用 frame_sync& sync_; diff --git a/src/app/threading/thread_coordinator.cpp b/src/app/threading/thread_coordinator.cpp index 2a82e02..037e3a9 100644 --- a/src/app/threading/thread_coordinator.cpp +++ b/src/app/threading/thread_coordinator.cpp @@ -164,17 +164,8 @@ void thread_coordinator::process_completed_frames() { // 结束帧 state_store_.end_frame(); - // 优化:减小临界区,先检查回调是否存在 - frame_complete_callback callback; - { - std::lock_guard lock(callback_mutex_); - callback = frame_complete_callback_; - } - - // 在锁外执行回调,减少锁持有时间 - if (callback) { - callback(result->frame_number); - } + // 发布帧完成事件 + frame_complete_topic_.publish(frame_complete_event{result->frame_number}); } } @@ -227,15 +218,6 @@ thread_coordinator::coordinator_stats thread_coordinator::get_stats() const { return stats; } -// ============================================================================ -// 回调设置 -// ============================================================================ - -void thread_coordinator::set_frame_complete_callback(frame_complete_callback callback) { - std::lock_guard lock(callback_mutex_); - frame_complete_callback_ = std::move(callback); -} - // ============================================================================ // 单线程模式 // ============================================================================ @@ -270,20 +252,11 @@ void thread_coordinator::process_frame_single_threaded(widget_base* root, vec2f_ float time = std::chrono::duration(now - start_time_).count(); pipeline_.render_frame(collector.get_render_commands(), time); - // 5. 更新统计并触发回调 + // 5. 更新统计并发布帧完成事件 frames_completed_.fetch_add(1, std::memory_order_relaxed); - // 优化:减小临界区 - frame_complete_callback callback; - { - std::lock_guard lock(callback_mutex_); - callback = frame_complete_callback_; - } - - // 在锁外执行回调 - if (callback) { - callback(frame_number); - } + // 发布帧完成事件 + frame_complete_topic_.publish(frame_complete_event{frame_number}); } // ============================================================================ @@ -294,12 +267,9 @@ void thread_coordinator::create_threads(const coordinator_config& config) { // 1. 创建帧同步对象 frame_sync_ = std::make_unique(config.max_frames_in_flight); - // 2. 创建渲染树双缓冲 - tree_buffer_ = std::make_unique(); - - // 3. 创建布局线程(传入状态存储引用、控件上下文引用和文本渲染依赖) + // 2. 创建布局线程(传入状态存储引用、控件上下文引用和文本渲染依赖) layout_thread_ = std::make_unique( - *tree_buffer_, + render_tree_state_, *frame_sync_, state_store_, context_, @@ -307,15 +277,15 @@ void thread_coordinator::create_threads(const coordinator_config& config) { glyph_cache_ ); - // 4. 创建渲染线程 - render_thread_ = std::make_unique(*tree_buffer_, *frame_sync_, pipeline_); + // 3. 创建渲染线程 + render_thread_ = std::make_unique(render_tree_state_, *frame_sync_, pipeline_); - // 5. 设置渲染配置(包括 adaptive_sync) + // 4. 设置渲染配置(包括 adaptive_sync) render_config render_cfg = config.render_cfg; render_cfg.adaptive_sync = config.adaptive_sync; render_thread_->set_config(render_cfg); - // 6. 启动线程(先启动渲染线程,再启动布局线程) + // 5. 启动线程(先启动渲染线程,再启动布局线程) render_thread_->start(); layout_thread_->start(); } @@ -349,7 +319,6 @@ void thread_coordinator::destroy_threads() { } // 6. 清理同步原语 - tree_buffer_.reset(); frame_sync_.reset(); } diff --git a/src/app/threading/thread_coordinator.h b/src/app/threading/thread_coordinator.h index 6a8a184..066d1ff 100644 --- a/src/app/threading/thread_coordinator.h +++ b/src/app/threading/thread_coordinator.h @@ -11,13 +11,18 @@ #include #include #include +#include #include "threading/layout_thread.h" #include "threading/render_thread.h" #include "threading/frame_sync.h" -#include "threading/render_tree_buffer.h" +#include "threading/sync_state.h" #include "threading/layout_snapshot.h" +#include "threading/pub_sub.h" +#include "threading/thread_dispatcher.h" #include "state/widget_state_store.h" #include "widget_context.h" +#include "pipeline/render_tree.h" +#include "types.h" namespace mirage { class widget_base; @@ -31,6 +36,50 @@ namespace mirage::render::text { namespace mirage::app::threading { +// ============================================================================ +// 帧完成事件 +// ============================================================================ + +/// @brief 帧完成事件 +/// +/// 当一帧完成布局后发布此事件 +struct frame_complete_event { + uint64_t frame_number; ///< 帧号 + + /// @brief 默认构造函数 + frame_complete_event() = default; + + /// @brief 构造函数 + /// @param frame_num 帧号 + explicit frame_complete_event(uint64_t frame_num) + : frame_number(frame_num) {} +}; + +// ============================================================================ +// 渲染树状态(包含渲染树和脏区域) +// ============================================================================ + +/// @brief 渲染树状态 +/// +/// 包装渲染树和相关的脏区域信息 +struct render_tree_state { + render::render_tree tree; ///< 渲染树 + std::optional dirty_rect; ///< 脏区域(全局坐标系) + + /// @brief 默认构造函数 + render_tree_state() = default; + + /// @brief 移动构造函数 + render_tree_state(render_tree_state&&) = default; + + /// @brief 移动赋值运算符 + render_tree_state& operator=(render_tree_state&&) = default; + + /// @brief 禁止拷贝(渲染树包含 unique_ptr) + render_tree_state(const render_tree_state&) = delete; + render_tree_state& operator=(const render_tree_state&) = delete; +}; + // ============================================================================ // 协调器配置 // ============================================================================ @@ -218,18 +267,18 @@ public: [[nodiscard]] coordinator_stats get_stats() const; // ======================================================================== - // 回调设置 + // 事件订阅 // ======================================================================== - /// @brief 帧完成回调类型 - using frame_complete_callback = std::function; - - /// @brief 设置帧完成回调 + /// @brief 获取帧完成主题 /// - /// 当一帧完成布局后,会在主线程调用此回调。 + /// 当一帧完成布局后,会发布 frame_complete_event 到此主题。 + /// 订阅者可以使用此主题接收帧完成通知。 /// - /// @param callback 回调函数 - void set_frame_complete_callback(frame_complete_callback callback); + /// @return 帧完成主题引用 + [[nodiscard]] mirage::threading::topic& frame_complete_topic() noexcept { + return frame_complete_topic_; + } // ======================================================================== // 状态存储访问 @@ -301,8 +350,8 @@ private: /// @brief 帧同步对象 std::unique_ptr frame_sync_; - /// @brief 渲染树双缓冲 - std::unique_ptr tree_buffer_; + /// @brief 渲染树状态同步(布局线程写入,渲染线程读取) + mirage::threading::layout_to_render_state render_tree_state_; /// @brief 布局线程 std::unique_ptr layout_thread_; @@ -322,11 +371,8 @@ private: /// @brief 已完成的帧数 std::atomic frames_completed_{0}; - /// @brief 帧完成回调 - frame_complete_callback frame_complete_callback_; - - /// @brief 回调互斥锁 - mutable std::mutex callback_mutex_; + /// @brief 帧完成主题 + mirage::threading::topic frame_complete_topic_; /// @brief 当前帧号 std::atomic current_frame_number_{0}; diff --git a/src/common/threading/property.h b/src/common/threading/property.h index adb112d..32b0d56 100644 --- a/src/common/threading/property.h +++ b/src/common/threading/property.h @@ -306,6 +306,12 @@ private: // 类型别名 Type Aliases // ============================================================================ +/// @brief 主线程属性(修改时标记 value_changed) +/// 用于主线程管理的属性,widget 在主线程创建和修改 +/// @tparam T 值类型 +template +using main_property = property; + /// @brief 布局属性(修改时标记 needs_layout) /// 用于影响布局的属性,如位置、大小等 /// @tparam T 值类型 diff --git a/src/common/threading/sync_state.h b/src/common/threading/sync_state.h index 588cca0..0e59ed9 100644 --- a/src/common/threading/sync_state.h +++ b/src/common/threading/sync_state.h @@ -2,7 +2,7 @@ /// @file sync_state.h /// @brief 双缓冲同步状态实现 -/// +/// /// 提供线程间的无锁数据传递机制,允许写线程和读线程并行工作。 /// 使用双缓冲技术,写线程写入一个缓冲区,读线程读取另一个缓冲区, /// 通过原子操作进行缓冲区交换,实现"无感同步"。 @@ -14,17 +14,10 @@ #include #include "thread_context.h" #include "thread_tags.h" +#include "message_queue.h" // 引入 cache_line_size 定义 namespace mirage::threading { -// ============================================================================ -// 缓存行大小定义(避免伪共享) -// ============================================================================ - -/// @brief 缓存行大小常量 -/// 用于内存对齐,避免伪共享(false sharing) -inline constexpr std::size_t cache_line_size = 64; - // ============================================================================ // 双缓冲同步状态 Sync State // ============================================================================ @@ -125,18 +118,23 @@ public: } /// @brief 发布更新(交换缓冲区) - /// + /// /// 将写入的数据发布给读线程。如果有未发布的修改(dirty_=true), /// 则执行以下操作: - /// 1. 将当前写缓冲区的数据复制到新的写缓冲区 - /// 2. 原子地交换缓冲区索引 - /// 3. 增加版本号 - /// 4. 清除脏标记 - /// + /// 1. 对于可拷贝类型:将当前写缓冲区的数据复制到新的写缓冲区 + /// 2. 对于 move-only 类型:不移动数据,仅交换索引 + /// 3. 原子地交换缓冲区索引 + /// 4. 增加版本号 + /// 5. 清除脏标记 + /// /// 如果没有未发布的修改,此函数不执行任何操作。 - /// + /// /// @note 只能在 WriteTag 线程调用 /// @note 此操作是线程安全的,使用 release 内存序确保修改对读线程可见 + /// @note 对于 move-only 类型,不执行数据移动。这是因为读线程读取的是 + /// `buffers_[1 - write_index_]`,即交换后的旧缓冲区。如果移动数据, + /// 读线程会读取到空对象。不移动时,读线程正确读取旧缓冲区中的数据。 + /// 这要求使用方在每次 modify 时完全重建数据(如 render_tree 每帧重建)。 void publish() noexcept { thread_context::assert_thread(); @@ -145,9 +143,19 @@ public: int old_index = write_index_.load(std::memory_order_relaxed); int new_index = 1 - old_index; - // 复制当前写缓冲区的数据到新写缓冲区 - // 这样读线程可以继续读取旧数据,而写线程可以继续修改新数据 - buffers_[new_index] = buffers_[old_index]; + // 对于可拷贝类型,将数据复制到新缓冲区 + // 这样写线程可以继续在新缓冲区上修改,而不影响读线程读取旧数据 + if constexpr (std::is_copy_assignable_v) { + buffers_[new_index] = buffers_[old_index]; + } + // 对于 move-only 类型,不移动数据! + // 原因: + // - 读线程读取的是 buffers_[1 - write_index_] = buffers_[old_index] + // - 如果移动数据到 new_index,读线程会读取空对象 + // - 不移动数据,读线程正确读取 old_index 中的数据 + // - 下次 modify 时,写线程会在 new_index 上完全重建数据 + // + // 这要求:每次 modify 必须完全重建数据,不依赖上一帧状态 // 原子交换索引(使用 release 语义确保写入对读线程可见) write_index_.store(new_index, std::memory_order_release); diff --git a/src/common/threading/thread_tags.h b/src/common/threading/thread_tags.h index 47f30d4..60b77e6 100644 --- a/src/common/threading/thread_tags.h +++ b/src/common/threading/thread_tags.h @@ -41,10 +41,11 @@ concept thread_tag = requires { /// 其他标签只与自身兼容 /// @tparam T1 第一个线程标签 /// @tparam T2 第二个线程标签 -template -concept thread_compatible = - std::same_as || - std::same_as || - std::same_as; +template +concept thread_compatible = + thread_tag && thread_tag && + (std::same_as || + std::same_as || + std::same_as); } // namespace mirage::threading \ No newline at end of file diff --git a/src/render/pipeline/render_pipeline.cpp b/src/render/pipeline/render_pipeline.cpp index 6932ed6..5261ddc 100644 --- a/src/render/pipeline/render_pipeline.cpp +++ b/src/render/pipeline/render_pipeline.cpp @@ -76,6 +76,9 @@ namespace mirage { } ); scheduler_->initialize(); + + // 4.1 初始化 image_index 追踪数组(用于检测不连续问题) + last_image_index_.resize(config_.frames_in_flight, UINT32_MAX); // 5. 创建离屏渲染目标(启用 Depth/Stencil) offscreen_ = std::make_unique( @@ -507,10 +510,28 @@ namespace mirage { // 获取帧索引和命令缓冲(在开始渲染前保存) const uint32_t frame_index = idle_ctx.frame_index(); - + + // 获取当前 Swapchain 图像索引 + const uint32_t image_index = scheduler_->current_image_index(); + + // 检查 image_index 是否连续(用于检测 Swapchain 三缓冲与双缓冲帧同步不匹配问题) + bool image_index_changed = (last_image_index_[frame_index] != UINT32_MAX && + last_image_index_[frame_index] != image_index); + if (image_index_changed) { + // Image index 不连续,强制全屏重绘以避免残影问题 + need_full_render_ = true; + std::cout << "[Render WARNING] Image index mismatch for frame_index=" << frame_index + << ": expected=" << last_image_index_[frame_index] + << " actual=" << image_index + << " -> forcing full render" << std::endl; + } + + // 更新追踪 + last_image_index_[frame_index] = image_index; + // 2. 重置后效描述符池(在等待 fence 之后,传递帧索引) effects_->begin_frame(frame_index); - + // 判断渲染模式:全量渲染 vs 增量渲染 bool use_full_render = need_full_render_; vk::Rect2D scissor_rect; @@ -648,11 +669,8 @@ namespace mirage { // 确保离屏目标在 shader_read 状态 auto extent = swapchain_.get_extent(); offscreen_->transition_to(idle_ctx.cmd(), offscreen_target::state::shader_read); - - // 6. 获取当前 Swapchain 图像索引(由 begin_frame 获取并保存) - const uint32_t image_index = scheduler_->current_image_index(); - - // 7. 开始 Swapchain Pass(idle -> swapchain_pass) + + // 6. 开始 Swapchain Pass(idle -> swapchain_pass) // 根据渲染模式选择不同的 RenderPass: // - 全量渲染:使用 clear RenderPass(loadOp=eClear)清除整个画面 // - 增量渲染:使用 load RenderPass(loadOp=eLoad)保留上一帧内容 diff --git a/src/render/pipeline/render_pipeline.h b/src/render/pipeline/render_pipeline.h index 9fda28f..f7dbe99 100644 --- a/src/render/pipeline/render_pipeline.h +++ b/src/render/pipeline/render_pipeline.h @@ -147,6 +147,9 @@ namespace mirage { // 脏区域渲染状态 bool need_full_render_ = true; ///< 是否需要全量渲染(首帧、resize后) uint32_t frame_count_ = 0; ///< 帧计数器 + + // Image Index 追踪(用于检测 Swapchain 图像轮转不连续) + std::vector last_image_index_; ///< 每个 frame_index 上一次使用的 image_index(UINT32_MAX 表示无效) // ========================================================================= // 内部方法 diff --git a/src/render/vulkan/logical_device.cpp b/src/render/vulkan/logical_device.cpp index 20d2c01..390e521 100644 --- a/src/render/vulkan/logical_device.cpp +++ b/src/render/vulkan/logical_device.cpp @@ -80,8 +80,17 @@ namespace mirage { )); } - vk::PhysicalDeviceFeatures device_features; - // Enable features here if needed + // 查询物理设备支持的特性 + vk::PhysicalDeviceFeatures supported_features = physical_device.getFeatures(); + + // 创建设备特性,启用需要的特性 + vk::PhysicalDeviceFeatures device_features{}; + + // 如果物理设备支持 samplerAnisotropy,启用它 + // 这对于高质量纹理过滤很有用,特别是在文本渲染中 + if (supported_features.samplerAnisotropy) { + device_features.samplerAnisotropy = VK_TRUE; + } // Device extensions std::vector device_extensions; diff --git a/src/widget/containers/h_stack.h b/src/widget/containers/h_stack.h index d6f68e1..f3ad94a 100644 --- a/src/widget/containers/h_stack.h +++ b/src/widget/containers/h_stack.h @@ -1,6 +1,7 @@ #pragma once #include "container_widget_base.h" #include "slots.h" +#include "threading/property.h" #include #include @@ -327,10 +328,11 @@ namespace mirage { auto& spacing(float s) { spacing_ = s; + mark_measure_dirty(); // 间距影响布局 return *this; } - - [[nodiscard]] float get_spacing() const { return spacing_; } + + [[nodiscard]] float get_spacing() const { return spacing_.get(); } private: void add_item(std::shared_ptr w) { @@ -363,8 +365,8 @@ namespace mirage { return left; } - std::vector slots_; - float spacing_ = 0.0f; + std::vector slots_; + threading::main_property spacing_{0.0f}; [[nodiscard]] size_t get_visible_slot_count() const { return std::count_if(slots_.begin(), slots_.end(), diff --git a/src/widget/containers/scroll_box/scroll_box.cpp b/src/widget/containers/scroll_box/scroll_box.cpp index a0fdb52..44c9066 100644 --- a/src/widget/containers/scroll_box/scroll_box.cpp +++ b/src/widget/containers/scroll_box/scroll_box.cpp @@ -29,8 +29,9 @@ namespace mirage { // 返回视口大小(优先使用固定大小) vec2f_t result = available_size; - if (fixed_viewport_size_.x() > 0) result.x() = fixed_viewport_size_.x(); - if (fixed_viewport_size_.y() > 0) result.y() = fixed_viewport_size_.y(); + const auto& fixed_size = fixed_viewport_size_.get(); + if (fixed_size.x() > 0) result.x() = fixed_size.x(); + if (fixed_size.y() > 0) result.y() = fixed_size.y(); return result; } @@ -180,7 +181,7 @@ namespace mirage { } // 添加间距 - const float spacing_total = spacing_ * static_cast(visible_count > 0 ? visible_count - 1 : 0); + const float spacing_total = spacing_.get() * static_cast(visible_count > 0 ? visible_count - 1 : 0); switch (scroll_state_.mode()) { case scroll_mode::vertical: @@ -249,7 +250,7 @@ namespace mirage { } child_pos = vec2f_t{x_offset, current_y + pad.top}; - current_y += child_size.y() + pad.top + pad.bottom + spacing_; + current_y += child_size.y() + pad.top + pad.bottom + spacing_.get(); break; } case scroll_mode::horizontal: { @@ -275,13 +276,13 @@ namespace mirage { } child_pos = vec2f_t{current_x + pad.left, y_offset}; - current_x += child_size.x() + pad.left + pad.right + spacing_; + current_x += child_size.x() + pad.left + pad.right + spacing_.get(); break; } case scroll_mode::both: default: { child_pos = vec2f_t{pad.left - scroll_offset.x(), current_y + pad.top}; - current_y += child_size.y() + pad.top + pad.bottom + spacing_; + current_y += child_size.y() + pad.top + pad.bottom + spacing_.get(); break; } } diff --git a/src/widget/containers/scroll_box/scroll_box.h b/src/widget/containers/scroll_box/scroll_box.h index 284cf6d..2d2a9e5 100644 --- a/src/widget/containers/scroll_box/scroll_box.h +++ b/src/widget/containers/scroll_box/scroll_box.h @@ -4,6 +4,7 @@ #include "scroll_state.h" #include "smooth_scroll_animator.h" #include "scrollbar_manager.h" +#include "threading/property.h" #include #include #include @@ -255,6 +256,7 @@ namespace mirage { auto& spacing(float s) { spacing_ = s; + mark_measure_dirty(); // 间距影响布局 return *this; } @@ -275,11 +277,13 @@ namespace mirage { auto& fixed_viewport(float width, float height) { fixed_viewport_size_ = vec2f_t{width, height}; + mark_measure_dirty(); // 固定视口大小影响布局 return *this; } auto& fixed_viewport(const vec2f_t& size) { fixed_viewport_size_ = size; + mark_measure_dirty(); // 固定视口大小影响布局 return *this; } @@ -302,9 +306,9 @@ namespace mirage { } [[nodiscard]] auto get_mode() const { return scroll_state_.mode(); } - [[nodiscard]] auto get_scroll_speed() const { return scroll_speed_; } - [[nodiscard]] auto get_spacing() const { return spacing_; } - [[nodiscard]] auto is_smooth_scroll_enabled() const { return smooth_scroll_enabled_; } + [[nodiscard]] auto get_scroll_speed() const { return scroll_speed_.get(); } + [[nodiscard]] auto get_spacing() const { return spacing_.get(); } + [[nodiscard]] auto is_smooth_scroll_enabled() const { return smooth_scroll_enabled_.get(); } [[nodiscard]] auto is_smooth_scrolling() const { return scroll_animator_.is_animating(); } private: @@ -402,9 +406,10 @@ namespace mirage { smooth_scroll_animator scroll_animator_; // 平滑滚动动画器 scrollbar_manager scrollbar_manager_; // 滚动条管理器 - vec2f_t fixed_viewport_size_{0, 0}; - float scroll_speed_ = 100.0f; - float spacing_ = 0.0f; - bool smooth_scroll_enabled_ = true; + // 使用 main_property 确保主线程访问安全并自动追踪脏标记 + threading::main_property fixed_viewport_size_{vec2f_t{0, 0}}; + threading::main_property scroll_speed_{100.0f}; + threading::main_property spacing_{0.0f}; + threading::main_property smooth_scroll_enabled_{true}; }; } // namespace mirage \ No newline at end of file diff --git a/src/widget/containers/scroll_box/scroll_state.h b/src/widget/containers/scroll_box/scroll_state.h index d4f6ba3..12164b3 100644 --- a/src/widget/containers/scroll_box/scroll_state.h +++ b/src/widget/containers/scroll_box/scroll_state.h @@ -1,5 +1,6 @@ #pragma once #include "types.h" +#include "threading/property.h" #include #include #include @@ -21,6 +22,7 @@ namespace mirage { }; /// @brief 滚动状态管理类 - 封装滚动相关的状态和计算 + /// 重构后使用 property 实现自动脏标记追踪 class scroll_state { public: scroll_state() = default; @@ -64,16 +66,16 @@ namespace mirage { /// @brief 设置滚动偏移 [[nodiscard]] auto set_offset(const vec2f_t& offset) -> std::expected { vec2f_t clamped = clamp_offset(offset); - if (clamped != offset_) { + if (clamped != offset_.get()) { offset_ = clamped; - return offset_; + return offset_.get(); } return std::unexpected(scroll_error::invalid_offset); } /// @brief 设置水平滚动偏移 [[nodiscard]] auto set_offset_x(float x) -> std::expected { - vec2f_t new_offset = offset_; + vec2f_t new_offset = offset_.get(); new_offset.x() = x; auto result = set_offset(new_offset); if (result) { @@ -84,7 +86,7 @@ namespace mirage { /// @brief 设置垂直滚动偏移 [[nodiscard]] auto set_offset_y(float y) -> std::expected { - vec2f_t new_offset = offset_; + vec2f_t new_offset = offset_.get(); new_offset.y() = y; auto result = set_offset(new_offset); if (result) { @@ -98,33 +100,35 @@ namespace mirage { // ======================================================================== [[nodiscard]] constexpr auto mode() const noexcept -> scroll_mode { return mode_; } - [[nodiscard]] constexpr auto offset() const noexcept -> const vec2f_t& { return offset_; } - [[nodiscard]] constexpr auto max_offset() const noexcept -> const vec2f_t& { return max_offset_; } - [[nodiscard]] constexpr auto viewport_size() const noexcept -> const vec2f_t& { return viewport_size_; } - [[nodiscard]] constexpr auto content_size() const noexcept -> const vec2f_t& { return content_size_; } + [[nodiscard]] auto offset() const noexcept -> const vec2f_t& { return offset_.get(); } + [[nodiscard]] auto max_offset() const noexcept -> const vec2f_t& { return max_offset_.get(); } + [[nodiscard]] auto viewport_size() const noexcept -> const vec2f_t& { return viewport_size_.get(); } + [[nodiscard]] auto content_size() const noexcept -> const vec2f_t& { return content_size_.get(); } /// @brief 获取滚动百分比(0-1 范围) [[nodiscard]] auto scroll_percent() const noexcept -> vec2f_t { + const auto& max_off = max_offset_.get(); + const auto& curr_off = offset_.get(); return vec2f_t{ - max_offset_.x() > 0 ? offset_.x() / max_offset_.x() : 0.0f, - max_offset_.y() > 0 ? offset_.y() / max_offset_.y() : 0.0f + max_off.x() > 0 ? curr_off.x() / max_off.x() : 0.0f, + max_off.y() > 0 ? curr_off.y() / max_off.y() : 0.0f }; } /// @brief 检查是否可以水平滚动 - [[nodiscard]] constexpr bool can_scroll_horizontal() const noexcept { - return (mode_ == scroll_mode::horizontal || mode_ == scroll_mode::both) - && max_offset_.x() > 0; + [[nodiscard]] bool can_scroll_horizontal() const noexcept { + return (mode_ == scroll_mode::horizontal || mode_ == scroll_mode::both) + && max_offset_.get().x() > 0; } /// @brief 检查是否可以垂直滚动 - [[nodiscard]] constexpr bool can_scroll_vertical() const noexcept { - return (mode_ == scroll_mode::vertical || mode_ == scroll_mode::both) - && max_offset_.y() > 0; + [[nodiscard]] bool can_scroll_vertical() const noexcept { + return (mode_ == scroll_mode::vertical || mode_ == scroll_mode::both) + && max_offset_.get().y() > 0; } /// @brief 检查是否需要滚动 - [[nodiscard]] constexpr bool needs_scroll() const noexcept { + [[nodiscard]] bool needs_scroll() const noexcept { return can_scroll_horizontal() || can_scroll_vertical(); } @@ -134,22 +138,24 @@ namespace mirage { /// @brief 计算滚动到指定百分比的偏移 [[nodiscard]] auto offset_from_percent(const vec2f_t& percent) const noexcept -> vec2f_t { + const auto& max_off = max_offset_.get(); return vec2f_t{ - percent.x() * max_offset_.x(), - percent.y() * max_offset_.y() + percent.x() * max_off.x(), + percent.y() * max_off.y() }; } /// @brief 计算相对滚动后的偏移 [[nodiscard]] auto offset_by_delta(const vec2f_t& delta) const noexcept -> vec2f_t { - return clamp_offset(offset_ + delta); + return clamp_offset(offset_.get() + delta); } /// @brief 限制偏移在有效范围内 [[nodiscard]] auto clamp_offset(const vec2f_t& offset) const noexcept -> vec2f_t { + const auto& max_off = max_offset_.get(); return vec2f_t{ - std::clamp(offset.x(), 0.0f, max_offset_.x()), - std::clamp(offset.y(), 0.0f, max_offset_.y()) + std::clamp(offset.x(), 0.0f, max_off.x()), + std::clamp(offset.y(), 0.0f, max_off.y()) }; } @@ -158,33 +164,40 @@ namespace mirage { void update_limits() noexcept { update_limits_no_clamp(); // 重新限制当前偏移 - offset_ = clamp_offset(offset_); + offset_ = clamp_offset(offset_.get()); } /// @brief 更新滚动限制(不修改 offset) /// @note 用于布局线程,避免在动画期间意外修改 offset void update_limits_no_clamp() noexcept { - max_offset_.x() = std::max(0.0f, content_size_.x() - viewport_size_.x()); - max_offset_.y() = std::max(0.0f, content_size_.y() - viewport_size_.y()); + const auto& viewport = viewport_size_.get(); + const auto& content = content_size_.get(); + vec2f_t new_max_offset; + new_max_offset.x() = std::max(0.0f, content.x() - viewport.x()); + new_max_offset.y() = std::max(0.0f, content.y() - viewport.y()); // 根据滚动模式限制 switch (mode_) { case scroll_mode::vertical: - max_offset_.x() = 0; + new_max_offset.x() = 0; break; case scroll_mode::horizontal: - max_offset_.y() = 0; + new_max_offset.y() = 0; break; case scroll_mode::both: break; } + + max_offset_ = new_max_offset; } scroll_mode mode_ = scroll_mode::vertical; - vec2f_t offset_{0, 0}; - vec2f_t max_offset_{0, 0}; - vec2f_t viewport_size_{0, 0}; - vec2f_t content_size_{0, 0}; + + // 使用 main_property 确保主线程访问安全并自动追踪脏标记 + threading::main_property offset_{vec2f_t{0, 0}}; + threading::main_property max_offset_{vec2f_t{0, 0}}; + threading::main_property viewport_size_{vec2f_t{0, 0}}; + threading::main_property content_size_{vec2f_t{0, 0}}; }; } // namespace mirage \ No newline at end of file diff --git a/src/widget/containers/scroll_box/scrollbar_manager.h b/src/widget/containers/scroll_box/scrollbar_manager.h index 64d8514..2960399 100644 --- a/src/widget/containers/scroll_box/scrollbar_manager.h +++ b/src/widget/containers/scroll_box/scrollbar_manager.h @@ -46,6 +46,9 @@ namespace mirage { } /// @brief 设置滚动条值变化回调 + /// @param callback 回调函数,接收滚动值和方向标志(true=垂直,false=水平) + /// @note 线程安全:回调在主线程调用(滚动条值变化时) + /// @note 此回调用于将滚动条值变化同步到 scroll_box auto&& set_value_changed_callback(this auto&& self, std::function callback) { self.value_changed_callback_ = std::move(callback); @@ -68,6 +71,9 @@ namespace mirage { } /// @brief 设置滚动条拖动状态变化回调 + /// @param callback 回调函数,接收拖动状态(true=开始拖动,false=结束拖动) + /// @note 线程安全:回调在主线程调用(鼠标事件处理时) + /// @note 可用于在拖动期间禁用平滑滚动等优化 auto&& set_drag_state_changed_callback(this auto&& self, std::function callback) { self.drag_state_changed_callback_ = std::move(callback); diff --git a/src/widget/containers/scroll_box/smooth_scroll_animator.h b/src/widget/containers/scroll_box/smooth_scroll_animator.h index 6cee693..b0c4198 100644 --- a/src/widget/containers/scroll_box/smooth_scroll_animator.h +++ b/src/widget/containers/scroll_box/smooth_scroll_animator.h @@ -1,5 +1,6 @@ #pragma once #include "types.h" +#include "threading/property.h" #include #include #include @@ -17,6 +18,7 @@ namespace mirage { }; /// @brief 平滑滚动动画器 - 使用 C++23 特性封装平滑滚动逻辑 + /// 重构后使用 property 实现自动脏标记追踪 class smooth_scroll_animator { public: using callback_t = std::function; @@ -43,50 +45,51 @@ namespace mirage { /// @brief 更新动画 /// @return 如果动画完成返回最终位置,否则返回 nullopt [[nodiscard]] auto update(float dt) -> std::optional { - if (!is_animating_) { + if (!is_animating_.get()) { return std::nullopt; } - elapsed_time_ += dt; + elapsed_time_ = elapsed_time_.get() + dt; - if (elapsed_time_ >= duration_) { + if (elapsed_time_.get() >= duration_.get()) { // 动画完成 - current_pos_ = target_pos_; + current_pos_ = target_pos_.get(); is_animating_ = false; if (on_complete_) { - on_complete_(current_pos_); + on_complete_(current_pos_.get()); } - return current_pos_; + return current_pos_.get(); } // 计算进度 - float progress = std::clamp(elapsed_time_ / duration_, 0.0f, 1.0f); + float progress = std::clamp(elapsed_time_.get() / duration_.get(), 0.0f, 1.0f); float eased_progress = apply_easing(progress); // 插值计算当前位置 - vec2f_t old_pos = current_pos_; - current_pos_ = start_pos_ + (target_pos_ - start_pos_) * eased_progress; + vec2f_t old_pos = current_pos_.get(); + vec2f_t new_pos = start_pos_.get() + (target_pos_.get() - start_pos_.get()) * eased_progress; + current_pos_ = new_pos; // 只有位置真正改变时才触发回调 - if (current_pos_ != old_pos && on_update_) { - on_update_(current_pos_); + if (new_pos != old_pos && on_update_) { + on_update_(new_pos); } - return current_pos_; + return new_pos; } /// @brief 停止动画 - constexpr void stop() noexcept { + void stop() noexcept { is_animating_ = false; } /// @brief 立即完成动画 void complete() { - if (is_animating_) { - current_pos_ = target_pos_; + if (is_animating_.get()) { + current_pos_ = target_pos_.get(); is_animating_ = false; if (on_complete_) { - on_complete_(current_pos_); + on_complete_(current_pos_.get()); } } } @@ -105,11 +108,17 @@ namespace mirage { return std::forward(self); } + /// @brief 设置动画更新回调 + /// @param callback 动画每帧更新时调用,接收当前位置 + /// @note 线程安全:回调在主线程的 animation_update() 中调用 auto&& on_update(this auto&& self, callback_t callback) { self.on_update_ = std::move(callback); return std::forward(self); } + /// @brief 设置动画完成回调 + /// @param callback 动画完成时调用,接收最终位置 + /// @note 线程安全:回调在主线程的 animation_update() 中调用 auto&& on_complete(this auto&& self, callback_t callback) { self.on_complete_ = std::move(callback); return std::forward(self); @@ -119,16 +128,18 @@ namespace mirage { // 查询接口 // ======================================================================== - [[nodiscard]] constexpr bool is_animating() const noexcept { return is_animating_; } - [[nodiscard]] constexpr auto current_position() const noexcept -> const vec2f_t& { return current_pos_; } - [[nodiscard]] constexpr auto target_position() const noexcept -> const vec2f_t& { return target_pos_; } - [[nodiscard]] constexpr float progress() const noexcept { - return duration_ > 0 ? std::clamp(elapsed_time_ / duration_, 0.0f, 1.0f) : 1.0f; + [[nodiscard]] bool is_animating() const noexcept { return is_animating_.get(); } + [[nodiscard]] auto current_position() const noexcept -> const vec2f_t& { return current_pos_.get(); } + [[nodiscard]] auto target_position() const noexcept -> const vec2f_t& { return target_pos_.get(); } + [[nodiscard]] float progress() const noexcept { + float dur = duration_.get(); + float elapsed = elapsed_time_.get(); + return dur > 0 ? std::clamp(elapsed / dur, 0.0f, 1.0f) : 1.0f; } private: /// @brief 应用缓动函数 - [[nodiscard]] constexpr float apply_easing(float t) const noexcept { + [[nodiscard]] float apply_easing(float t) const noexcept { switch (easing_) { case easing_function::linear: return t; @@ -137,27 +148,28 @@ namespace mirage { case easing_function::ease_out_cubic: return 1.0f - std::pow(1.0f - t, 3.0f); case easing_function::ease_in_out_cubic: - return t < 0.5f - ? 4.0f * t * t * t + return t < 0.5f + ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f; case easing_function::ease_out_quad: return 1.0f - (1.0f - t) * (1.0f - t); case easing_function::ease_in_out_quad: - return t < 0.5f - ? 2.0f * t * t + return t < 0.5f + ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) / 2.0f; default: return t; } } - vec2f_t start_pos_{0, 0}; - vec2f_t target_pos_{0, 0}; - vec2f_t current_pos_{0, 0}; - float duration_ = 0.3f; - float elapsed_time_ = 0.0f; + // 使用 main_property 确保主线程访问安全并自动追踪脏标记 + threading::main_property start_pos_{vec2f_t{0, 0}}; + threading::main_property target_pos_{vec2f_t{0, 0}}; + threading::main_property current_pos_{vec2f_t{0, 0}}; + threading::main_property duration_{0.3f}; + threading::main_property elapsed_time_{0.0f}; easing_function easing_ = easing_function::ease_out_cubic; - bool is_animating_ = false; + threading::main_property is_animating_{false}; callback_t on_update_; callback_t on_complete_; diff --git a/src/widget/containers/v_stack.h b/src/widget/containers/v_stack.h index 4f540a3..b6ed521 100644 --- a/src/widget/containers/v_stack.h +++ b/src/widget/containers/v_stack.h @@ -1,6 +1,7 @@ #pragma once #include "container_widget_base.h" #include "slots.h" +#include "threading/property.h" #include #include @@ -318,10 +319,11 @@ namespace mirage { auto& spacing(float s) { spacing_ = s; + mark_measure_dirty(); // 间距影响布局 return *this; } - - [[nodiscard]] float get_spacing() const { return spacing_; } + + [[nodiscard]] float get_spacing() const { return spacing_.get(); } private: void add_item(std::shared_ptr w) { @@ -354,8 +356,8 @@ namespace mirage { return left; } - std::vector slots_; - float spacing_ = 0.0f; + std::vector slots_; + threading::main_property spacing_{0.0f}; [[nodiscard]] size_t get_visible_slot_count() const { return std::ranges::count_if(slots_, diff --git a/src/widget/render_collector.h b/src/widget/render_collector.h index 7241206..fc0b013 100644 --- a/src/widget/render_collector.h +++ b/src/widget/render_collector.h @@ -2,6 +2,7 @@ #include "types.h" #include "layout_types.h" #include "render_command.h" +#include "threading/thread_bound.h" #include #include #include @@ -15,6 +16,9 @@ namespace mirage { } /// @brief 渲染数据收集器 + /// + /// @thread_safety 此类设计为在布局线程中使用,不支持多线程并发访问 + /// @note 命令收集过程必须在单个线程中完成,通常是布局线程 class render_collector { public: struct config { @@ -117,11 +121,15 @@ namespace mirage { [[nodiscard]] bool is_visible(const aabb2d_t& widget_aabb) const; private: + // 渲染命令收集数据(布局线程专用) std::vector render_commands_; ///< 树形渲染命令(含遮罩) std::vector mask_stack_; ///< 遮罩状态栈 std::vector clip_stack_; ///< 裁剪区域栈 + config config_; int32_t current_z_order_ = 0; - widget::viewport_cache* viewport_cache_ = nullptr; ///< 视口缓存指针 + + /// @brief 视口缓存指针(来自 widget_context,生命周期由外部管理) + widget::viewport_cache* viewport_cache_ = nullptr; }; } // namespace mirage diff --git a/src/widget/state/widget_state.h b/src/widget/state/widget_state.h index 825e99a..dcb4217 100644 --- a/src/widget/state/widget_state.h +++ b/src/widget/state/widget_state.h @@ -68,6 +68,7 @@ struct secondary_state { measure_cache measure; ///< 测量缓存 vec2f_t preferred_size = vec2f_t::Zero(); ///< 首选尺寸 aabb2d_t global_aabb; ///< 全局包围盒(用于快速剔除) + aabb2d_t previous_aabb; ///< 上一帧的全局包围盒(用于脏区域计算) state_version version; ///< 版本信息 }; diff --git a/src/widget/state/widget_state_store.cpp b/src/widget/state/widget_state_store.cpp index 84c95f9..dccb8a6 100644 --- a/src/widget/state/widget_state_store.cpp +++ b/src/widget/state/widget_state_store.cpp @@ -53,6 +53,17 @@ state_token widget_state_store::register_widget(uint64_t widget_id) { // 初始化状态 state = widget_state{}; + + // 新注册的控件需要初始布局,设置为 measure_invalid + // 这确保首帧能够被正常提交和渲染 + state.primary.dirty = dirty_state::measure_invalid; + + // 添加到脏列表 + if (std::ranges::find(pimpl_->dirty_widgets, widget_id) + == pimpl_->dirty_widgets.end()) { + pimpl_->dirty_widgets.push_back(widget_id); + } + generation++; return state_token{widget_id, generation}; @@ -259,7 +270,7 @@ bool widget_state_store::layout_write_context::update_measure_cache( } bool widget_state_store::layout_write_context::update_preferred_size( - uint64_t widget_id, + uint64_t widget_id, const vec2f_t& size) { if (!store_ || committed_) { return false; @@ -279,6 +290,26 @@ bool widget_state_store::layout_write_context::update_preferred_size( return true; } +bool widget_state_store::layout_write_context::update_previous_aabb( + uint64_t widget_id, + const aabb2d_t& aabb) { + if (!store_ || committed_) { + return false; + } + + std::unique_lock lock(store_->pimpl_->mutex); + + auto it = store_->pimpl_->states.find(widget_id); + if (it == store_->pimpl_->states.end()) { + return false; + } + + // 更新次级状态的上一帧 AABB + it->second.secondary.previous_aabb = aabb; + + return true; +} + void widget_state_store::layout_write_context::commit() { if (committed_ || !store_) { return; diff --git a/src/widget/state/widget_state_store.h b/src/widget/state/widget_state_store.h index 9f41a36..da547bb 100644 --- a/src/widget/state/widget_state_store.h +++ b/src/widget/state/widget_state_store.h @@ -124,6 +124,9 @@ public: /// @brief 更新首选尺寸 bool update_preferred_size(uint64_t widget_id, const vec2f_t& size); + /// @brief 更新上一帧的 AABB(用于脏区域计算) + bool update_previous_aabb(uint64_t widget_id, const aabb2d_t& aabb); + /// @brief 提交所有更新 void commit(); diff --git a/src/widget/viewport_cache.cpp b/src/widget/viewport_cache.cpp index 41dda7e..6efc9a8 100644 --- a/src/widget/viewport_cache.cpp +++ b/src/widget/viewport_cache.cpp @@ -9,14 +9,14 @@ viewport_cache::viewport_cache(const viewport_cache_config& config) , cache_frame_(0) , valid_(false) { // 预分配空间以减少重新分配 - if (config_.max_cached_widgets > 0) { - cache_.reserve(config_.max_cached_widgets); - visible_set_.reserve(config_.max_cached_widgets / 10); // 假设10%可见 + if (config_.get().max_cached_widgets > 0) { + cache_.reserve(config_.get().max_cached_widgets); + visible_set_.reserve(config_.get().max_cached_widgets / 10); // 假设10%可见 } } visibility_state viewport_cache::query_visibility(uint64_t id) const { - if (!valid_ || !config_.enable_caching) { + if (!valid_.get() || !config_.get().enable_caching) { return visibility_state::unknown; } @@ -30,16 +30,16 @@ visibility_state viewport_cache::query_visibility(uint64_t id) const { void viewport_cache::update_visibility(const aabb2d_t& viewport, const std::vector& entries) { - if (!config_.enable_caching) { + if (!config_.get().enable_caching) { return; } // 检查视口是否发生显著变化 - bool viewport_changed = is_viewport_changed(cached_viewport_, viewport); + bool viewport_changed = is_viewport_changed(cached_viewport_.get(), viewport); if (viewport_changed) { cached_viewport_ = viewport; - cache_frame_++; + cache_frame_ = cache_frame_.get() + 1; } // 更新每个控件的可见性 @@ -52,7 +52,7 @@ void viewport_cache::update_visibility(const aabb2d_t& viewport, cached_entry.widget_id = entry.widget_id; cached_entry.global_aabb = entry.global_aabb; cached_entry.state = state; - cached_entry.last_update_frame = cache_frame_; + cached_entry.last_update_frame = cache_frame_.get(); // 更新可见集合 if (state == visibility_state::visible || state == visibility_state::partial) { @@ -87,15 +87,15 @@ void viewport_cache::invalidate_widget(uint64_t id) { } bool viewport_cache::is_valid() const noexcept { - return valid_; + return valid_.get(); } const aabb2d_t& viewport_cache::get_cached_viewport() const noexcept { - return cached_viewport_; + return cached_viewport_.get(); } uint64_t viewport_cache::get_cache_frame() const noexcept { - return cache_frame_; + return cache_frame_.get(); } visibility_state viewport_cache::check_visibility( @@ -135,8 +135,8 @@ bool viewport_cache::is_viewport_changed( float max_delta = (old_viewport.max() - new_viewport.max()).norm(); // 如果变化超过阈值,认为发生了显著变化 - return min_delta > config_.viewport_change_threshold || - max_delta > config_.viewport_change_threshold; + return min_delta > config_.get().viewport_change_threshold || + max_delta > config_.get().viewport_change_threshold; } } // namespace mirage::widget \ No newline at end of file diff --git a/src/widget/viewport_cache.h b/src/widget/viewport_cache.h index 02af972..4ba1f8b 100644 --- a/src/widget/viewport_cache.h +++ b/src/widget/viewport_cache.h @@ -1,6 +1,7 @@ #pragma once #include "types.h" #include "aabb_utils.h" +#include "threading/property.h" #include #include #include @@ -29,6 +30,8 @@ struct viewport_cache_config { bool enable_caching = true; ///< 是否启用缓存 float viewport_change_threshold = 1.0f; ///< 视口变化阈值(像素) uint32_t max_cached_widgets = 10000; ///< 最大缓存控件数 + + bool operator==(const viewport_cache_config&) const = default; }; /// @brief 视口缓存 - 管理视口内可见控件的缓存 @@ -103,12 +106,17 @@ private: const aabb2d_t& new_viewport) const; private: - viewport_cache_config config_; ///< 配置 + // 使用 property 追踪配置变化 + threading::main_property config_; + + // 缓存数据(布局线程访问) std::unordered_map cache_; ///< 主缓存:widget_id -> visibility_entry std::unordered_set visible_set_; ///< 可见控件集合,用于快速迭代 - aabb2d_t cached_viewport_; ///< 缓存的视口AABB - uint64_t cache_frame_ = 0; ///< 缓存帧号 - bool valid_ = false; ///< 缓存是否有效 + + // 使用 property 追踪视口和状态变化 + threading::main_property cached_viewport_; ///< 缓存的视口AABB + threading::main_property cache_frame_; ///< 缓存帧号 + threading::main_property valid_; ///< 缓存是否有效 }; } // namespace mirage::widget \ No newline at end of file diff --git a/src/widget/widget_base.h b/src/widget/widget_base.h index 9bc3172..7cf9413 100644 --- a/src/widget/widget_base.h +++ b/src/widget/widget_base.h @@ -7,6 +7,7 @@ #include "state/widget_state.h" #include "state/widget_state_store.h" #include "widget_context.h" +#include "threading/property.h" #include #include #include @@ -307,42 +308,42 @@ namespace mirage { }; } // namespace mirage -#define WIDGET_ATTR(type, name) \ -private: \ - type name##_; \ -public: \ - [[nodiscard]] auto name() const -> type { \ - return name##_; \ - } \ - auto& name(const type& value) { \ - name##_ = value; \ - return *this; \ +#define WIDGET_ATTR(type, name) \ +private: \ + mirage::threading::main_property name##_; \ +public: \ + [[nodiscard]] auto name() const -> const type& { \ + return name##_.get(); \ + } \ + auto& name(const type& value) { \ + name##_ = value; \ + return *this; \ } -#define WIDGET_RENDER_ATTR(type, name) \ -private: \ - type name##_; \ -public: \ - [[nodiscard]] auto name() const -> type { \ - return name##_; \ - } \ - auto& name(const type& value) { \ - name##_ = value; \ - mark_render_dirty(); \ - return *this; \ +#define WIDGET_RENDER_ATTR(type, name) \ +private: \ + mirage::threading::main_property name##_; \ +public: \ + [[nodiscard]] auto name() const -> const type& { \ + return name##_.get(); \ + } \ + auto& name(const type& value) { \ + name##_ = value; \ + mark_render_dirty(); \ + return *this; \ } -#define WIDGET_MEASURE_ATTR(type, name) \ -private: \ - type name##_; \ -public: \ - [[nodiscard]] auto name() const -> type { \ - return name##_; \ - } \ - auto& name(const type& value) { \ - name##_ = value; \ - mark_measure_dirty(); \ - return *this; \ +#define WIDGET_MEASURE_ATTR(type, name) \ +private: \ + mirage::threading::main_property name##_; \ +public: \ + [[nodiscard]] auto name() const -> const type& { \ + return name##_.get(); \ + } \ + auto& name(const type& value) { \ + name##_ = value; \ + mark_measure_dirty(); \ + return *this; \ } diff --git a/src/widget/widget_context.h b/src/widget/widget_context.h index 51380e2..f73662b 100644 --- a/src/widget/widget_context.h +++ b/src/widget/widget_context.h @@ -3,6 +3,7 @@ #include "state/widget_state_store.h" #include "layout_state.h" #include "viewport_cache.h" +#include "threading/property.h" #include #include @@ -21,6 +22,9 @@ namespace render::text { /// - 状态存储 /// - 资源管理 /// - 帧信息 +/// +/// @thread_safety 此类设计为在布局线程中使用 +/// @note viewport_cache 使用 property 追踪状态变化 class widget_context { public: explicit widget_context(state::widget_state_store& store, render::text::text_shaper& shaper) @@ -108,10 +112,10 @@ public: } private: - state::widget_state_store& store_; - widget::viewport_cache viewport_cache_; - widget::viewport_cache_config viewport_cache_config_; - render::text::text_shaper& text_shaper_; + state::widget_state_store& store_; ///< 状态存储引用 + widget::viewport_cache viewport_cache_; ///< 视口缓存(使用 property 追踪状态) + widget::viewport_cache_config viewport_cache_config_; ///< 视口缓存配置 + render::text::text_shaper& text_shaper_; ///< 文本塑形器引用 }; } // namespace mirage \ No newline at end of file diff --git a/src/widget/widgets/imager.h b/src/widget/widgets/imager.h index 261cf81..e4fe21a 100644 --- a/src/widget/widgets/imager.h +++ b/src/widget/widgets/imager.h @@ -33,43 +33,46 @@ namespace mirage { [[nodiscard]] auto do_measure(const vec2f_t& available_size) const -> vec2f_t override { // 如果设置了固定大小,返回固定大小 - if (fixed_size_.x() > 0 && fixed_size_.y() > 0) { - return fixed_size_; + const auto& fixed = fixed_size_.get(); + if (fixed.x() > 0 && fixed.y() > 0) { + return fixed; } // 如果没有源尺寸,返回可用空间 - if (source_size_.x() <= 0 || source_size_.y() <= 0) { + const auto& source = source_size_.get(); + if (source.x() <= 0 || source.y() <= 0) { return available_size; } // measure 阶段基于 source_size 确定需要的空间 // fill/none 模式:请求 source_size // contain/scale_down 模式:在可用空间内按比例缩放 // cover 模式:请求 source_size(渲染时会超出) - switch (scale_) { + const auto& scale_mode_val = scale_.get(); + switch (scale_mode_val) { case scale_mode::contain: case scale_mode::scale_down: { const float scale = std::min( - available_size.x() / source_size_.x(), - available_size.y() / source_size_.y() + available_size.x() / source.x(), + available_size.y() / source.y() ); - if (scale_ == scale_mode::scale_down && scale >= 1.0f) { - return source_size_; + if (scale_mode_val == scale_mode::scale_down && scale >= 1.0f) { + return source; } - return vec2f_t{source_size_.x() * scale, source_size_.y() * scale}; + return vec2f_t{source.x() * scale, source.y() * scale}; } default: - return source_size_; + return source; } } uint32_t build_render_command(render_collector& collector, uint32_t z_order) const override { - if (!is_renderable() || texture_id_ == 0) + if (!is_renderable() || texture_id_.get() == 0) return z_order; auto [render_pos, render_size] = calculate_render_rect(); // cover 模式需要裁剪超出容器的部分 // 使用容器的布局边界作为裁剪区域 - const bool needs_clip = (scale_ == scale_mode::cover); + const bool needs_clip = (scale_.get() == scale_mode::cover); if (needs_clip) { // 裁剪边界应该是容器的实际布局区域 @@ -85,7 +88,7 @@ namespace mirage { collector.submit_image( render_pos, render_size, - texture_id_, + texture_id_.get(), z_order ); @@ -101,11 +104,13 @@ namespace mirage { // ======================================================================== [[nodiscard]] auto get_intrinsic_size() const -> std::optional override { - if (fixed_size_.x() > 0 && fixed_size_.y() > 0) { - return fixed_size_; + const auto& fixed = fixed_size_.get(); + if (fixed.x() > 0 && fixed.y() > 0) { + return fixed; } - if (source_size_.x() > 0 && source_size_.y() > 0) { - return source_size_; + const auto& source = source_size_.get(); + if (source.x() > 0 && source.y() > 0) { + return source; } return std::nullopt; } @@ -123,12 +128,14 @@ namespace mirage { private: /// @brief 计算缩放后的尺寸 [[nodiscard]] auto calculate_scaled_size(const vec2f_t& available_size) const -> vec2f_t { - const float src_w = source_size_.x(); - const float src_h = source_size_.y(); + const auto& source = source_size_.get(); + const float src_w = source.x(); + const float src_h = source.y(); const float avail_w = available_size.x(); const float avail_h = available_size.y(); - switch (scale_) { + const auto& scale_mode_val = scale_.get(); + switch (scale_mode_val) { case scale_mode::fill: return available_size; @@ -143,11 +150,11 @@ namespace mirage { } case scale_mode::none: - return source_size_; + return source; case scale_mode::scale_down: { if (src_w <= avail_w && src_h <= avail_h) { - return source_size_; + return source; } const float scale = std::min(avail_w / src_w, avail_h / src_h); return vec2f_t{src_w * scale, src_h * scale}; @@ -166,7 +173,8 @@ namespace mirage { const vec2f_t container_size = vec2f_t(state->size()); // 如果没有源尺寸或使用 fill 模式,直接返回容器尺寸 - if (source_size_.x() <= 0 || source_size_.y() <= 0 || scale_ == scale_mode::fill) { + const auto& source = source_size_.get(); + if (source.x() <= 0 || source.y() <= 0 || scale_.get() == scale_mode::fill) { return {container_pos, container_size}; } diff --git a/src/widget/widgets/scrollbar.h b/src/widget/widgets/scrollbar.h index e743063..896dd7c 100644 --- a/src/widget/widgets/scrollbar.h +++ b/src/widget/widgets/scrollbar.h @@ -184,12 +184,17 @@ public: } /// @brief 设置值变化回调 + /// @param callback 回调函数,接收新的滚动值 + /// @note 线程安全:回调将在主线程调用(滚动值变化时) + /// @note 回调应避免长时间阻塞,以免影响滚动性能 auto& on_value_changed(std::function callback) { value_changed_callback_ = std::move(callback); return *this; } /// @brief 设置拖动状态变化回调 + /// @param callback 回调函数,接收拖动状态(true=开始拖动,false=结束拖动) + /// @note 线程安全:回调将在主线程调用(鼠标事件处理时) auto& on_drag_state_changed(std::function callback) { drag_state_changed_callback_ = std::move(callback); return *this; diff --git a/src/widget/widgets/text_widget.h b/src/widget/widgets/text_widget.h index 3f9989a..12b5f28 100644 --- a/src/widget/widgets/text_widget.h +++ b/src/widget/widgets/text_widget.h @@ -9,13 +9,13 @@ namespace mirage { /// @brief 文本控件 - 显示文本内容 class text_widget : public leaf_widget_base { public: - text_widget() : font_id_(0), font_size_(14.f), text_color_(color::white()), text_("") { + text_widget() : font_id_(0), font_size_(14.f), text_color_(color::white()), text_(std::string("")) { } - explicit text_widget(std::string_view text) : font_size_(14.f), text_(text) { + explicit text_widget(std::string_view text) : font_size_(14.f), text_(std::string(text)) { } - text_widget(std::string_view text, float font_size) : font_size_(font_size), text_(text) { + text_widget(std::string_view text, float font_size) : font_size_(font_size), text_(std::string(text)) { } // ======================================================================== @@ -27,16 +27,17 @@ namespace mirage { // 尝试使用text_shaper进行精确度量 if (auto* ctx = get_context()) { - if (!text_.empty()) { + const auto& text = text_.get(); + if (!text.empty()) { // 转换UTF-8到UTF-32 - auto utf32_text = render::text::text_shaper::utf8_to_utf32(text_); + auto utf32_text = render::text::text_shaper::utf8_to_utf32(text); // 使用text_shaper进行精确度量 - auto metrics = ctx->get_text_shaper().measure_text(utf32_text, font_id_, font_size_); + auto metrics = ctx->get_text_shaper().measure_text(utf32_text, font_id_.get(), font_size_.get()); return vec2f_t{metrics.width, metrics.height}; } // 空文本返回最小高度(基于字体大小) - return vec2f_t{0.0f, font_size_ * 1.2f}; + return vec2f_t{0.0f, font_size_.get() * 1.2f}; } // 回退到估算算法(当text_shaper不可用时) @@ -45,23 +46,26 @@ namespace mirage { // - 中文字符宽度约为字体大小的1.0倍 // - 行高约为字体大小的1.2-1.5倍 - if (text_.empty()) { + const auto& text = text_.get(); + if (text.empty()) { // 空文本返回最小高度 - return vec2f_t{0.0f, font_size_ * 1.2f}; + return vec2f_t{0.0f, font_size_.get() * 1.2f}; } // 估算字符数(UTF-8编码,中文字符占3个字节) // 这里简化处理:假设平均字符宽度为font_size的0.55倍 - float estimated_width = static_cast(text_.size()) * font_size_ * 0.55f; + const float font_size = font_size_.get(); + float estimated_width = static_cast(text.size()) * font_size * 0.55f; // 行高为字体大小的1.2倍(包含基线以上和以下的空间) - float estimated_height = font_size_ * 1.2f; + float estimated_height = font_size * 1.2f; return vec2f_t{estimated_width, estimated_height}; } uint32_t build_render_command(render_collector& collector, uint32_t z_order) const override { - if (!is_renderable() || text_.empty()) + const auto& text = text_.get(); + if (!is_renderable() || text.empty()) return z_order; const auto state_opt = get_layout_state(); @@ -71,10 +75,10 @@ namespace mirage { const auto& state = state_opt.value(); collector.submit_text( state.global_pos(), - text_, - text_color_, - font_size_, - font_id_, + text, + text_color_.get(), + font_size_.get(), + font_id_.get(), z_order ); return ++z_order;