Finalize library bootstrap status and stabilize async asset regressions
This commit is contained in:
692
docs/plan/Library启动预热与运行时异步加载混合重构计划_2026-04-04.md
Normal file
692
docs/plan/Library启动预热与运行时异步加载混合重构计划_2026-04-04.md
Normal file
@@ -0,0 +1,692 @@
|
||||
# Library启动预热与运行时异步加载混合重构计划
|
||||
|
||||
文档日期:2026-04-04
|
||||
|
||||
适用范围:当前仓库中的 `project` 单项目工作流。
|
||||
|
||||
文档目标:在现有 `Library` 资产导入与缓存系统基础上,继续收口为一套更接近 Unity 的正式方案,即“启动阶段前置索引与缓存有效性恢复,运行时按需异步加载资源 payload”,同时避免重新引入旧版本兼容、迁移工具、路径双写等过渡态实现。
|
||||
|
||||
---
|
||||
|
||||
## 1. 这轮重构要解决什么问题
|
||||
|
||||
当前 `Library` 模块已经完成了第一阶段收口,具备了以下能力:
|
||||
|
||||
- `Assets.meta` / `*.meta` / `AssetGUID`
|
||||
- `Library/SourceAssetDB`
|
||||
- `Library/ArtifactDB`
|
||||
- `Library/Artifacts`
|
||||
- `AssetImportService`
|
||||
- `ProjectAssetIndex`
|
||||
- `ResourceManager` 运行时缓存与异步调度
|
||||
- `AssetRef` 驱动的项目资产恢复
|
||||
- `mesh/material/texture` artifact 体系
|
||||
|
||||
但最近几轮实际使用已经暴露出两个非常关键的问题:
|
||||
|
||||
### 1.1 问题一:artifact 命中路径不够纯净
|
||||
|
||||
已经生成好的 artifact 路径,例如:
|
||||
|
||||
- `Library/Artifacts/.../main.xcmesh`
|
||||
- `Library/Artifacts/.../material_0.xcmat`
|
||||
- `Library/Artifacts/.../texture_0.xctex`
|
||||
|
||||
在某些运行时链路里被误当成源资源再次送回 `EnsureArtifact()`,导致:
|
||||
|
||||
- 报错 `Failed to build asset artifact: Library/Artifacts/...`
|
||||
- 资源恢复失败
|
||||
- 运行时重复做无意义工作
|
||||
- UI 上出现误导性的导入失败状态
|
||||
|
||||
这个问题已经定位并修复,但它说明当前系统在“源资源路径”和“artifact 路径”的边界定义上还不够硬。
|
||||
|
||||
### 1.2 问题二:缓存命中了,但热路径仍然很重
|
||||
|
||||
这是最近 “为什么打开带 obj 的 scene 还是会卡” 的根本原因。
|
||||
|
||||
当 `backpack.obj` 这类模型已经有 artifact 时,理论上应该走:
|
||||
|
||||
- 命中 `ArtifactDB`
|
||||
- 直接读取 `xcmesh/xcmat/xctex`
|
||||
- 异步恢复运行时对象
|
||||
|
||||
但实际上旧实现里在 `AreDependenciesCurrent()` 阶段还会对依赖文件反复做重成本校验:
|
||||
|
||||
- `backpack.mtl`
|
||||
- `diffuse.jpg`
|
||||
- `normal.png`
|
||||
- `specular.jpg`
|
||||
|
||||
尤其是对大贴图重复做 hash,会把“缓存命中”退化成“每次打开 scene 都重新扫描大资源”。
|
||||
|
||||
这也是为什么用户体感上会觉得:
|
||||
|
||||
- 明明已经有 `Library`
|
||||
- 明明已经不是第一次打开
|
||||
- 但打开带大模型的场景依然卡顿很明显
|
||||
|
||||
这个问题也已经定位并修复,当前改为缓存命中时只使用 `fileSize + writeTime` 快速校验,而不是每次重算依赖 hash。
|
||||
|
||||
### 1.3 问题三:当前系统仍偏“运行时按需补救”,缺少正式的启动前置阶段
|
||||
|
||||
目前系统已经比较偏向:
|
||||
|
||||
- 编辑器启动快
|
||||
- 打开 scene 时按需异步恢复 mesh/material/texture
|
||||
|
||||
这条路线本身没有问题,但如果不补一个正式的“项目启动预热阶段”,就会有几个副作用:
|
||||
|
||||
- 项目启动后第一次打开大场景,工作量全部堆在 scene open 时刻
|
||||
- 视口、Inspector、Picker 等编辑器系统更容易在首帧触发同步兜底
|
||||
- 用户对 `Library` 的感知会变成“有缓存但还是随机卡”
|
||||
- 问题定位难,因为启动阶段和运行时阶段职责混在一起
|
||||
|
||||
### 1.4 问题四:编辑器主线程仍存在同步兜底风险
|
||||
|
||||
虽然 deferred scene load 与异步恢复已经打通,但编辑器里仍有若干系统会在渲染或交互路径中触发:
|
||||
|
||||
- `GetMesh()`
|
||||
- `GetMaterial()`
|
||||
- 选择框、Gizmo、拾取、Inspector 读取资源状态
|
||||
|
||||
如果这些路径访问时机不对,仍然可能把本该后台完成的工作拉回主线程。
|
||||
|
||||
也就是说,现在系统已经不是“完全同步导入”,但还没完全达到“严格异步、主线程只消费结果”的成熟状态。
|
||||
|
||||
---
|
||||
|
||||
## 2. 这轮要采用的正式方向
|
||||
|
||||
这轮不走两个极端:
|
||||
|
||||
- 不走 Unity 式“把几乎所有成本都压到启动时”的纯前置模式
|
||||
- 也不走“什么都按需,启动几乎不做准备”的纯懒加载模式
|
||||
|
||||
正式目标是混合式方案:
|
||||
|
||||
### 2.1 启动阶段前置做什么
|
||||
|
||||
项目启动时只前置做轻量但必要的内容:
|
||||
|
||||
- 读取 `SourceAssetDB`
|
||||
- 读取 `ArtifactDB`
|
||||
- 建立 `GUID <-> path <-> AssetRef` 快照索引
|
||||
- 恢复 `ProjectAssetIndex`
|
||||
- 对项目资源做轻量级有效性校验
|
||||
- 检查 artifact 文件是否存在
|
||||
- 对最近使用场景或关键高频资源做 metadata 级预热
|
||||
|
||||
这些工作应该:
|
||||
|
||||
- 不解码大贴图
|
||||
- 不重建 mesh
|
||||
- 不恢复完整 material runtime object
|
||||
- 不做 GPU payload 上传
|
||||
|
||||
### 2.2 运行时按需异步做什么
|
||||
|
||||
真正重的 payload 继续按需异步进入:
|
||||
|
||||
- `xcmesh` -> runtime mesh
|
||||
- `xcmat` -> runtime material
|
||||
- `xctex` -> runtime texture
|
||||
- 贴图绑定的延迟解析与加载
|
||||
|
||||
也就是说:
|
||||
|
||||
- 启动阶段前置“状态”
|
||||
- 运行时异步加载“内容”
|
||||
|
||||
### 2.3 这套方案为什么更适合当前项目
|
||||
|
||||
因为当前项目的真实诉求是:
|
||||
|
||||
- 编辑器本身不要像 Unity 那样每次启动都等很久
|
||||
- 但打开带大模型的场景也不能把主窗口卡死
|
||||
- 同时 `Library` 要真的发挥缓存价值,而不是只在目录结构上看起来像 Unity
|
||||
|
||||
对这个目标来说,最佳平衡点就是:
|
||||
|
||||
- 启动时恢复索引和缓存状态
|
||||
- 打开 scene 时只反序列化结构
|
||||
- 重资源 payload 严格异步
|
||||
- cache hit 路径绝不重新做大计算
|
||||
|
||||
---
|
||||
|
||||
## 3. 本轮重构后的目标架构
|
||||
|
||||
### 3.1 模块职责划分
|
||||
|
||||
#### `AssetDatabase`
|
||||
|
||||
只作为底层数据库与导入实现细节:
|
||||
|
||||
- 源资源记录
|
||||
- artifact 记录
|
||||
- `.meta` 文件管理
|
||||
- importer 分派
|
||||
- artifact 构建
|
||||
- 依赖采集
|
||||
- 缓存有效性校验
|
||||
|
||||
不直接承担 editor 级工作流协调。
|
||||
|
||||
#### `AssetImportService`
|
||||
|
||||
作为项目级导入与缓存服务层:
|
||||
|
||||
- 项目根目录绑定
|
||||
- 启动阶段缓存恢复
|
||||
- 轻量校验
|
||||
- `EnsureArtifact`
|
||||
- `Refresh`
|
||||
- `ReimportAsset`
|
||||
- `ReimportAllAssets`
|
||||
- `ClearLibraryCache`
|
||||
- 启动预热状态输出
|
||||
|
||||
后续新增的“启动预热流程”也应放在这一层,而不是塞回 `ResourceManager`。
|
||||
|
||||
#### `ProjectAssetIndex`
|
||||
|
||||
只承担索引快照职责:
|
||||
|
||||
- `AssetRef -> project path`
|
||||
- `project path -> AssetRef`
|
||||
- 项目资源查找缓存
|
||||
- 场景反序列化后的快速路径恢复
|
||||
|
||||
不参与真正的 artifact 构建。
|
||||
|
||||
#### `ResourceManager`
|
||||
|
||||
只承担运行时加载与缓存职责:
|
||||
|
||||
- runtime object cache
|
||||
- async queue
|
||||
- in-flight coalescing
|
||||
- 源资源与 artifact 路径路由
|
||||
- deferred scene load 期间的异步恢复入口
|
||||
|
||||
明确禁止它重新承担“项目导入管理器”角色。
|
||||
|
||||
### 3.2 正式的三段式生命周期
|
||||
|
||||
#### 阶段 A:Project Startup Bootstrap
|
||||
|
||||
项目打开时:
|
||||
|
||||
- 恢复 `Library` 数据库
|
||||
- 建索引
|
||||
- 快速验证缓存状态
|
||||
- 后台预热 metadata
|
||||
|
||||
不恢复大资源 payload。
|
||||
|
||||
#### 阶段 B:Scene Structural Load
|
||||
|
||||
打开 scene 时:
|
||||
|
||||
- 只做场景结构反序列化
|
||||
- 组件恢复 `AssetRef` 和资源路径 hint
|
||||
- 不在主线程同步恢复大 mesh/material/texture
|
||||
|
||||
#### 阶段 C:Runtime Payload Stream-In
|
||||
|
||||
场景打开后:
|
||||
|
||||
- 按需异步拉取 `xcmesh`
|
||||
- 再按需恢复 `xcmat`
|
||||
- 贴图绑定继续 lazy resolve / lazy load
|
||||
- 视口只消费当前已完成资源
|
||||
|
||||
---
|
||||
|
||||
## 4. 当前已完成内容与后续计划分界
|
||||
|
||||
## 4.1 已完成
|
||||
|
||||
以下内容已经可以视为本轮新方案的基础,不需要推倒重来:
|
||||
|
||||
- `Library` 目录结构已落地
|
||||
- `SourceAssetDB` / `ArtifactDB` 已落地
|
||||
- `.meta + AssetGUID` 已稳定
|
||||
- `AssetRef` 驱动的 scene/component 恢复已落地
|
||||
- `MeshFilterComponent` / `MeshRendererComponent` deferred async restore 已打通
|
||||
- `ProjectPanel` 已有最小 import status 输出
|
||||
- orphan artifact 清理已接入
|
||||
- artifact 路径误进 `EnsureArtifact()` 的问题已修
|
||||
- cache hit 时重复 hash 大依赖导致卡顿的问题已修
|
||||
- `AssetImportService::BootstrapProject()` 已落地,项目启动存在正式 bootstrap 生命周期
|
||||
- `ResourceManager::SetResourceRoot()` 已切到显式 bootstrap 路径,不再依赖首次资源查询时的隐式补救
|
||||
- `AssetDatabase::Initialize()` 已改为只恢复 DB 状态,不再在绑定项目根目录时隐式全量扫描
|
||||
- `ClearLibraryCache()` 已在清空后立即重建 source lookup,避免清库后留下半残状态
|
||||
- `ResourceHandle` 已改为持有稳定 `ResourceGUID`,`UnloadAll/Shutdown` 后不再因为悬空指针在析构期崩溃
|
||||
- `ResourceManager` 的 `Unload/UnloadAll/UnloadGroup` 已同步清理 `ResourceCache` 陈旧条目
|
||||
- `Material` 析构顺序问题已修,OBJ source-import 与 mesh artifact reimport 链路不再触发 debug heap 崩溃
|
||||
- `mesh/material/scene/components` 关键回归已重新验证通过
|
||||
|
||||
## 4.2 本轮仍然需要正式收口的内容
|
||||
|
||||
本轮重点不再是“把 `Library` 做出来”,而是把下面这些未完成项正式落地:
|
||||
|
||||
- 正式的启动阶段 bootstrap
|
||||
- 启动阶段的 metadata 预热策略
|
||||
- 项目打开后、scene 首次打开前的轻量准备
|
||||
- editor 主线程同步兜底点审计
|
||||
- 视口 / Inspector / Picker 对未加载资源的正式占位策略
|
||||
- `AssetImportService` 层级的启动状态与进度模型
|
||||
- 更清晰的“正在预热 / 正在导入 / 正在异步恢复”的 UI 区分
|
||||
|
||||
---
|
||||
|
||||
## 5. 详细实施阶段
|
||||
|
||||
## 阶段 0:基线固化与指标采样
|
||||
|
||||
### 目标
|
||||
|
||||
先把当前性能问题量化,避免后续优化没有参照系。
|
||||
|
||||
### 任务
|
||||
|
||||
- 为以下阶段分别记录耗时:
|
||||
- 项目启动到 editor 可交互
|
||||
- 打开 `Backpack.xc` 到 scene graph 可见
|
||||
- 打开 `Backpack.xc` 到 mesh 可见
|
||||
- 打开 `Backpack.xc` 到首帧可渲染
|
||||
- 记录 `AssetImportService` 各操作状态:
|
||||
- `Bootstrap`
|
||||
- `Refresh`
|
||||
- `Import Asset`
|
||||
- `Reimport`
|
||||
- 记录 `ResourceManager` 各类异步请求计数:
|
||||
- mesh
|
||||
- material
|
||||
- texture
|
||||
- 对 `backpack.obj` 链路增加 focused trace 开关
|
||||
|
||||
### 交付物
|
||||
|
||||
- 明确的启动时长基线
|
||||
- 明确的 scene 打开耗时基线
|
||||
- 一组稳定可复现的回归测试与日志样本
|
||||
|
||||
### 完成标准
|
||||
|
||||
- 以后任何优化都可以明确比较 “优化前 vs 优化后”
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1:启动阶段 Bootstrap 正式化
|
||||
|
||||
### 目标
|
||||
|
||||
把当前隐式分散在多个点的项目初始化流程,收成一个正式的启动阶段。
|
||||
|
||||
### 要做的事
|
||||
|
||||
- 在 `AssetImportService` 中新增明确的启动流程,例如:
|
||||
- `BeginProjectBootstrap()`
|
||||
- `RunProjectBootstrap()`
|
||||
- `GetBootstrapStatus()`
|
||||
- 启动阶段同步完成:
|
||||
- 绑定项目根目录
|
||||
- 读取 `SourceAssetDB`
|
||||
- 读取 `ArtifactDB`
|
||||
- 构建 `LookupSnapshot`
|
||||
- 恢复 `ProjectAssetIndex`
|
||||
- 快速检查 artifact 文件存在性
|
||||
- 启动阶段禁止:
|
||||
- 解码纹理
|
||||
- 读取 `.obj` 顶点数据
|
||||
- 恢复完整 material runtime object
|
||||
- 重算所有依赖 hash
|
||||
|
||||
### 推荐实现细节
|
||||
|
||||
- `SetProjectRoot()` 只负责绑定项目,不隐式做重活
|
||||
- 显式 bootstrap 负责状态恢复
|
||||
- `ResourceManager::SetResourceRoot()` 完成后应当触发一次 bootstrap,而不是等首次 `LoadResource()` 时再补救
|
||||
- `ProjectAssetIndex::RefreshFrom(...)` 要明确依赖 bootstrap 产出的快照,而不是在运行时随机 fallback
|
||||
|
||||
### 结果要求
|
||||
|
||||
- 项目启动后,`AssetRef` 和项目资源索引已经可用
|
||||
- 打开 scene 时不需要再顺便补建整套索引
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2:缓存命中路径彻底轻量化
|
||||
|
||||
### 目标
|
||||
|
||||
保证 “有 artifact 且未变化” 真正意味着快。
|
||||
|
||||
### 要做的事
|
||||
|
||||
- 固化当前已经完成的两个修复:
|
||||
- artifact 路径直载,不再送回 `EnsureArtifact`
|
||||
- cache hit 时依赖校验只走 `fileSize + writeTime`
|
||||
- 继续补强边界判定:
|
||||
- `builtin://...`
|
||||
- `Library/Artifacts/...`
|
||||
- `.xcmesh/.xcmat/.xctex/.xcshader`
|
||||
- `Assets/...`
|
||||
- 项目外绝对路径
|
||||
- 只允许明确的源资源路径触发导入
|
||||
- cache hit 时不允许再发生:
|
||||
- 重扫依赖目录
|
||||
- 重解析 `obj/mtl`
|
||||
- 重算大贴图 hash
|
||||
|
||||
### 推荐实现细节
|
||||
|
||||
- 在 `ResourceManager` 中形成统一 helper:
|
||||
- `ShouldUseProjectArtifactImport(path, type)`
|
||||
- 在 `AssetDatabase` 中形成统一 helper:
|
||||
- `IsFastCacheValidationEligible(...)`
|
||||
- 深度 hash 改为仅在这些场景触发:
|
||||
- 显式 Reimport
|
||||
- 怀疑 DB 损坏
|
||||
- 调试模式人工开启深校验
|
||||
|
||||
### 结果要求
|
||||
|
||||
- 二次打开 `Backpack.xc` 这类场景时,命中链路不应再接近首次导入成本
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3:Scene Open 零阻塞主路径收口
|
||||
|
||||
### 目标
|
||||
|
||||
打开 scene 时只恢复结构,不同步等 payload。
|
||||
|
||||
### 要做的事
|
||||
|
||||
- 审计 scene load 首帧中所有可能触发同步资源恢复的位置:
|
||||
- `MeshFilterComponent::GetMesh()`
|
||||
- `MeshRendererComponent::GetMaterial()`
|
||||
- render item 构建
|
||||
- scene picker
|
||||
- gizmo frame builder
|
||||
- inspector 材质预览
|
||||
- 禁止在 scene 刚打开时,视口渲染链路用同步 `Load<T>()` 兜底
|
||||
- 对未完成的资源加载使用正式占位策略:
|
||||
- mesh 未到位:跳过 render item 或显示加载占位
|
||||
- material 未到位:使用稳定 fallback material
|
||||
- texture 未到位:保持材质对象,但贴图槽位为空
|
||||
|
||||
### 推荐实现细节
|
||||
|
||||
- `GetMesh()` / `GetMaterial()` 保持“只触发异步请求,不阻塞等待”
|
||||
- 场景渲染代码消费的是“当前已完成状态”,而不是强行等待最终状态
|
||||
- `Loading scene assets...` 应仅表示后台恢复中,不应意味着主线程仍被卡住
|
||||
|
||||
### 结果要求
|
||||
|
||||
- 打开大场景时主窗口保持可交互
|
||||
- 首帧允许缺资源,但不能卡死
|
||||
- 资源陆续完成后画面逐步完善
|
||||
|
||||
---
|
||||
|
||||
## 阶段 4:启动预热策略
|
||||
|
||||
### 目标
|
||||
|
||||
在不走 Unity 全量前置的前提下,把最常用项目数据提前准备好。
|
||||
|
||||
### 候选预热内容
|
||||
|
||||
- 最近一次打开的 scene 列表
|
||||
- 当前项目默认 scene
|
||||
- 最近访问的材质/模型索引
|
||||
- `AssetRef -> path` 高频映射
|
||||
- scene 文件中出现过的主 mesh asset ref
|
||||
|
||||
### 预热分层
|
||||
|
||||
#### 同步预热
|
||||
|
||||
- 只预热索引和 metadata
|
||||
|
||||
#### 后台预热
|
||||
|
||||
- 检查对应 artifact 是否存在
|
||||
- 预先解析 scene 内引用的主资源清单
|
||||
- 但不真正构建 runtime payload
|
||||
|
||||
### 推荐实现细节
|
||||
|
||||
- 新增简单的 `Library/SessionCache` 或 `Library/BootstrapCache` 元数据文件
|
||||
- 记录最近场景和高频资源,不需要做复杂推荐系统
|
||||
- 预热任务进入专用后台队列,不占用用户交互关键线程
|
||||
|
||||
### 结果要求
|
||||
|
||||
- 项目打开后第一次打开最近场景更快
|
||||
- 不把全部项目资源都拉进启动成本
|
||||
|
||||
---
|
||||
|
||||
## 阶段 5:编辑器调用点清理
|
||||
|
||||
### 目标
|
||||
|
||||
把 editor 里所有“资源还没准备好时的坏行为”收掉。
|
||||
|
||||
### 要做的事
|
||||
|
||||
- 清理以下系统中的同步依赖:
|
||||
- `SceneViewportPicker`
|
||||
- `SceneViewportTransformGizmoFrameBuilder`
|
||||
- `RenderSceneUtility`
|
||||
- `InspectorPanel`
|
||||
- 材质/mesh 相关组件编辑器
|
||||
- 明确这些系统在资源未到位时的行为:
|
||||
- 不报错
|
||||
- 不触发重导入
|
||||
- 不锁主线程
|
||||
- 不污染 import status
|
||||
|
||||
### 推荐实现细节
|
||||
|
||||
- picker:未完成 mesh 时直接跳过,不同步等待
|
||||
- gizmo center:拿不到 mesh bounds 时退回 transform position
|
||||
- inspector:未完成材质时显示 loading / unresolved,而不是同步 `Load`
|
||||
- viewport:资源未到位时继续渲染其它可用对象
|
||||
|
||||
### 结果要求
|
||||
|
||||
- “打开 scene 不阻塞” 不仅存在于测试里,也存在于真实编辑器交互中
|
||||
|
||||
---
|
||||
|
||||
## 阶段 6:状态与观测模型正式化
|
||||
|
||||
### 目标
|
||||
|
||||
用户能看懂系统当前在干什么。
|
||||
|
||||
### 现状问题
|
||||
|
||||
现在 `ProjectPanel` 顶部已经有导入状态,但还不足以区分:
|
||||
|
||||
- 项目启动预热
|
||||
- 显式导入
|
||||
- 场景异步恢复
|
||||
- 运行时 payload stream-in
|
||||
|
||||
### 要做的事
|
||||
|
||||
- 将状态拆成至少三类:
|
||||
- `Bootstrap`
|
||||
- `Import/Reimport`
|
||||
- `Scene Asset Streaming`
|
||||
- UI 明确区分:
|
||||
- 正在恢复项目索引
|
||||
- 正在重建 artifact
|
||||
- 正在后台加载场景资源
|
||||
- 增加更清楚的 tooltip:
|
||||
- 当前目标路径
|
||||
- 剩余异步数量
|
||||
- 最近失败原因
|
||||
|
||||
### 推荐实现细节
|
||||
|
||||
- `AssetImportService::ImportStatusSnapshot` 扩展为更通用的 operation model
|
||||
- `ResourceManager` 增加 lightweight streaming status snapshot
|
||||
- `ProjectPanel` 与 viewport status text 分工:
|
||||
- `ProjectPanel` 展示项目级状态
|
||||
- viewport 展示当前 scene 级 streaming 状态
|
||||
|
||||
### 结果要求
|
||||
|
||||
- 用户不会再把“后台恢复中”和“导入失败”混淆
|
||||
|
||||
---
|
||||
|
||||
## 阶段 7:最终收口与归档
|
||||
|
||||
### 目标
|
||||
|
||||
把这套方案从“已经能用”收成“正式基线”。
|
||||
|
||||
### 要做的事
|
||||
|
||||
- 整理最终架构说明
|
||||
- 删除不再需要的临时日志和调试开关
|
||||
- 固化测试矩阵
|
||||
- 更新 README / Editor 架构文档中的 Library 模块说明
|
||||
- 将本计划归档到 `docs/plan/used`
|
||||
|
||||
### 完成标准
|
||||
|
||||
- `Library` 模块不再被视为过渡态系统
|
||||
- 项目启动与大场景打开的行为稳定、可解释、可回归
|
||||
|
||||
---
|
||||
|
||||
## 6. 需要修改或重点审查的代码范围
|
||||
|
||||
本轮预计主要涉及以下文件:
|
||||
|
||||
- `engine/src/Core/Asset/AssetDatabase.cpp`
|
||||
- `engine/include/XCEngine/Core/Asset/AssetDatabase.h`
|
||||
- `engine/src/Core/Asset/AssetImportService.cpp`
|
||||
- `engine/include/XCEngine/Core/Asset/AssetImportService.h`
|
||||
- `engine/src/Core/Asset/ProjectAssetIndex.cpp`
|
||||
- `engine/include/XCEngine/Core/Asset/ProjectAssetIndex.h`
|
||||
- `engine/src/Core/Asset/ResourceManager.cpp`
|
||||
- `engine/include/XCEngine/Core/Asset/ResourceManager.h`
|
||||
- `engine/src/Core/Asset/AsyncLoader.cpp`
|
||||
- `engine/src/Components/MeshFilterComponent.cpp`
|
||||
- `engine/src/Components/MeshRendererComponent.cpp`
|
||||
- `engine/src/Rendering/RenderSceneUtility.cpp`
|
||||
- `editor/src/Viewport/SceneViewportPicker.cpp`
|
||||
- `editor/src/Viewport/SceneViewportTransformGizmoFrameBuilder.h`
|
||||
- `editor/src/panels/InspectorPanel.cpp`
|
||||
- `editor/src/Viewport/ViewportHostService.h`
|
||||
- `editor/src/Managers/SceneManager.cpp`
|
||||
- `tests/Scene/test_scene.cpp`
|
||||
- `tests/Components/test_mesh_render_components.cpp`
|
||||
- `tests/Resources/Texture/test_texture_loader.cpp`
|
||||
- `tests/Core/Asset/test_resource_manager.cpp`
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
## 7.1 功能层
|
||||
|
||||
- 关闭 editor 再打开 `Backpack.xc` 时,模型 mesh/material/texture 能稳定恢复
|
||||
- 内置 primitive 不会因为缓存系统回归而丢失
|
||||
- 打开含 OBJ 的 scene 时,不再出现 artifact 路径误导入报错
|
||||
|
||||
## 7.2 性能层
|
||||
|
||||
- 项目启动时不会做完整 payload 导入
|
||||
- 打开 `Backpack.xc` 不会长时间阻塞主窗口
|
||||
- artifact 命中耗时显著低于首次导入
|
||||
- cache hit 路径不会再重复扫描大贴图 hash
|
||||
|
||||
## 7.3 架构层
|
||||
|
||||
- 启动阶段、scene open 阶段、runtime payload 阶段边界清晰
|
||||
- `AssetImportService`、`ProjectAssetIndex`、`ResourceManager` 职责边界清晰
|
||||
- editor 主线程不再承担重资源恢复职责
|
||||
|
||||
## 7.4 观测层
|
||||
|
||||
- 用户能区分 `Bootstrap`、`Import`、`Scene Streaming`
|
||||
- 失败原因可见
|
||||
- 后台工作数量可见
|
||||
|
||||
## 7.5 回归测试层
|
||||
|
||||
至少保住以下 focused 回归:
|
||||
|
||||
- `Scene_ProjectSample.AsyncLoadBackpackMeshArtifactCompletes`
|
||||
- `Scene_ProjectSample.DeferredLoadBackpackSceneEventuallyRestoresBackpackMesh`
|
||||
- `Scene_ProjectSample.DeferredLoadBackpackSceneEventuallyProducesVisibleRenderItems`
|
||||
- `MeshRendererComponent_Test.DeferredSceneDeserializeLoadsProjectMaterialAsync`
|
||||
- `TextureLoader.ResourceManagerLoadsLibraryArtifactTextureWithoutReimportingIt`
|
||||
- `ResourceManager_Test.ConcurrentAsyncLoadsCoalesceSameMeshPath`
|
||||
|
||||
---
|
||||
|
||||
## 8. 当前阶段完成情况
|
||||
|
||||
截至 2026-04-04 当前这一轮代码与测试状态,按这份新计划计:
|
||||
|
||||
| 阶段 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 阶段 0:基线固化 | 进行中 | focused test 已补强,但还缺项目启动耗时与 editor 实机场景打开指标归档 |
|
||||
| 阶段 1:启动阶段 Bootstrap 正式化 | 已完成 | `BootstrapProject()` / `BootstrapProjectAssets()` 已接入正式启动链路 |
|
||||
| 阶段 2:缓存命中路径轻量化 | 已完成 | artifact 直载、依赖快速校验、误重导入路径问题均已修复 |
|
||||
| 阶段 3:Scene Open 零阻塞主路径 | 已完成首轮 | backpack 场景异步恢复链路已稳定,scene open 不再因为 Library 命中路径本身阻塞主线程 |
|
||||
| 阶段 4:启动预热策略 | 未开始 | 还没有最近场景/高频资源 metadata 预热 |
|
||||
| 阶段 5:编辑器调用点清理 | 进行中 | 主要阻塞点已清掉,但 Inspector / Picker / viewport 仍需继续做系统化审计 |
|
||||
| 阶段 6:状态与观测模型 | 进行中 | 已有 bootstrap/import 状态基础,但还未正式区分 bootstrap / streaming / scene restore |
|
||||
| 阶段 7:最终收口与归档 | 未开始 | 等上述阶段完成后执行 |
|
||||
|
||||
补充说明:
|
||||
|
||||
- 本轮新增并通过的关键回归包括:
|
||||
- `AssetImportService_Test.BootstrapProjectBuildsLookupSnapshotAndReportsStatus`
|
||||
- `ResourceManager_Test.SetResourceRootBootstrapsProjectAssetCache`
|
||||
- `ResourceHandle.ResetDoesNotDereferenceDestroyedResourcePointer`
|
||||
- `MeshLoader.AssetDatabaseReimportsModelWhenDependencyChanges`
|
||||
- `MeshLoader.ResourceManagerLoadsModelByAssetRefFromProjectAssets`
|
||||
- 这意味着当前 `Library` 模块的主要风险已经从“缓存不生效/路径误判”转移为“剩余 editor 调用点审计、状态模型清晰化、预热策略补全”。
|
||||
|
||||
---
|
||||
|
||||
## 9. 推荐执行顺序
|
||||
|
||||
建议严格按下面顺序推进:
|
||||
|
||||
1. 先继续做阶段 5,把 editor 主线程剩余同步兜底点系统性扫完。
|
||||
2. 然后做阶段 4,把“最近场景 metadata 预热”补上。
|
||||
3. 接着做阶段 6,把状态模型和 UI 区分补完整。
|
||||
4. 再做阶段 0,把项目启动和大场景打开指标正式采样落档。
|
||||
5. 最后统一做阶段 7 收口归档。
|
||||
|
||||
原因很明确:
|
||||
|
||||
- 如果不先清主线程调用点,任何异步链路都可能再次被同步兜底破坏。
|
||||
- 如果不补预热策略,首次打开高频场景仍然会把轻量准备工作堆到打开时刻。
|
||||
- 如果不把状态模型做清楚,用户会继续把“后台恢复”误解成“又卡死了”。
|
||||
- 如果不把基线指标整理出来,后续收口就没有稳定对照。
|
||||
|
||||
---
|
||||
|
||||
## 10. 一句话结论
|
||||
|
||||
本轮不是继续“补 Library”,而是把现有 `Library` 系统正式提升为 Unity 式混合架构:
|
||||
|
||||
项目启动时前置恢复索引与缓存状态,运行时严格按需异步流入 mesh/material/texture payload,让缓存命中真正变快,同时保证打开大场景不再阻塞 editor 主线程。
|
||||
91
docs/plan/Library启动预热与运行时异步加载混合重构计划_进度更新_2026-04-04.md
Normal file
91
docs/plan/Library启动预热与运行时异步加载混合重构计划_进度更新_2026-04-04.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Library 启动预热与运行时异步加载混合重构计划进度更新
|
||||
|
||||
文档日期:2026-04-04
|
||||
|
||||
对应主计划:
|
||||
- `docs/plan/Library启动预热与运行时异步加载混合重构计划_2026-04-04.md`
|
||||
|
||||
## 本轮已完成
|
||||
|
||||
- `mesh_tests` 全量 `34/34` 通过。
|
||||
- `scene_tests` 全量 `63/63` 通过。
|
||||
- `asset_tests` 全量 `55/55` 通过。
|
||||
- `texture_tests` 全量 `28/28` 通过。
|
||||
- `XCEditor` Debug 构建通过。
|
||||
|
||||
- `Main.xc` 轻量场景测试预期已和当前项目夹具同步:
|
||||
- 当前场景对象是 `Sphere`
|
||||
- 不再沿用旧的 `Cube` 预期
|
||||
|
||||
- `AssetImportService::ImportStatusSnapshot` 已增加:
|
||||
- `startedAtMs`
|
||||
- `completedAtMs`
|
||||
- `durationMs`
|
||||
|
||||
- `ProjectPanel` 的 Library 状态展示已增强:
|
||||
- 状态文案可显示耗时
|
||||
- tooltip 可区分 operation / state / duration / target / imported count / removed count
|
||||
|
||||
- 一个阻塞当前回归验证的无关编译问题已修复:
|
||||
- `engine/src/Resources/UI/UIDocumentLoaders.cpp`
|
||||
- 不再非法调用 `IResourceLoader::GetExtension()` 的 protected 接口
|
||||
|
||||
## 当前阶段判断
|
||||
|
||||
当前这条 Library 重构线已经达到下面这个阶段性状态:
|
||||
|
||||
- 启动阶段负责恢复索引和缓存状态
|
||||
- 运行时继续按需异步恢复 mesh / material / texture payload
|
||||
- 关键回归测试已经稳定通过
|
||||
- Editor 主程序可以正常构建
|
||||
|
||||
也就是说,前面最容易反复回退的两类问题已经清掉了:
|
||||
|
||||
- `Library` / artifact / `AssetRef` 主链路回归
|
||||
- scene fixture 漂移导致的假失败
|
||||
|
||||
## 相对主计划的完成情况
|
||||
|
||||
- 阶段 1:已完成
|
||||
- `BootstrapProject()` / `BootstrapProjectAssets()` 已正式接入启动链路
|
||||
|
||||
- 阶段 2:已完成
|
||||
- artifact 直载
|
||||
- cache hit 快速校验
|
||||
- 避免把 artifact 路径重新误送回导入链路
|
||||
|
||||
- 阶段 3:已完成首轮并稳定
|
||||
- scene open 不再因为 Library 命中路径本身阻塞主线程
|
||||
- backpack 场景异步恢复链路回归通过
|
||||
|
||||
- 阶段 5:推进中
|
||||
- 主要阻塞点已经清掉
|
||||
- 但 editor 里剩余同步兜底访问点仍需继续系统化审计
|
||||
|
||||
- 阶段 6:推进中
|
||||
- 现在已经有 bootstrap/import 状态
|
||||
- 但还需要继续区分 bootstrap / scene restore / runtime streaming
|
||||
|
||||
- 阶段 0 / 阶段 4 / 阶段 7:未收口
|
||||
- 启动与首帧指标基线还没正式固化
|
||||
- metadata 预热策略还没补完
|
||||
- 最终收口文档和归档还没做
|
||||
|
||||
## 下一步建议
|
||||
|
||||
下一阶段不要再回头重做 `Library` 主链路本身,重点应当转到“观测”和“收口”:
|
||||
|
||||
1. 固化启动基线与场景首帧指标
|
||||
2. 审计 editor 主线程剩余同步兜底点
|
||||
3. 给 viewport / inspector / picker 加上未完成异步资源的占位与状态提示
|
||||
4. 把“正在预热 / 正在导入 / 正在异步恢复”拆成更明确的 UI 状态
|
||||
|
||||
## 这一轮改动直接涉及的文件
|
||||
|
||||
- `engine/include/XCEngine/Core/Asset/AssetImportService.h`
|
||||
- `engine/src/Core/Asset/AssetImportService.cpp`
|
||||
- `editor/src/panels/ProjectPanel.cpp`
|
||||
- `tests/core/Asset/test_resource_manager.cpp`
|
||||
- `tests/Resources/Texture/test_texture_loader.cpp`
|
||||
- `tests/Scene/test_scene.cpp`
|
||||
- `engine/src/Resources/UI/UIDocumentLoaders.cpp`
|
||||
@@ -9,8 +9,12 @@
|
||||
#include "Utils/ProjectFileUtils.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <imgui.h>
|
||||
#include <shellapi.h>
|
||||
@@ -24,6 +28,85 @@ namespace {
|
||||
constexpr float kProjectToolbarHeight = 26.0f;
|
||||
constexpr float kProjectToolbarPaddingY = 3.0f;
|
||||
|
||||
std::uint64_t ResolveImportStatusElapsedMs(
|
||||
const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
|
||||
if (!status.HasValue() || status.startedAtMs == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!status.inProgress) {
|
||||
return static_cast<std::uint64_t>(status.durationMs);
|
||||
}
|
||||
|
||||
const auto nowMs = static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
return nowMs >= status.startedAtMs
|
||||
? nowMs - static_cast<std::uint64_t>(status.startedAtMs)
|
||||
: 0;
|
||||
}
|
||||
|
||||
ImVec4 ResolveImportStatusColor(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
|
||||
if (!status.HasValue()) {
|
||||
return XCEngine::Editor::UI::ConsoleSecondaryTextColor();
|
||||
}
|
||||
|
||||
if (status.inProgress) {
|
||||
return XCEngine::Editor::UI::ConsoleWarningColor();
|
||||
}
|
||||
|
||||
return status.success
|
||||
? XCEngine::Editor::UI::ConsoleSecondaryTextColor()
|
||||
: XCEngine::Editor::UI::ConsoleErrorColor();
|
||||
}
|
||||
|
||||
std::string BuildImportStatusText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
|
||||
if (!status.HasValue()) {
|
||||
return "Library: ready";
|
||||
}
|
||||
|
||||
std::string text = "Library: ";
|
||||
text += status.message.CStr();
|
||||
const std::uint64_t elapsedMs = ResolveImportStatusElapsedMs(status);
|
||||
if (elapsedMs > 0) {
|
||||
text += " (";
|
||||
text += std::to_string(elapsedMs);
|
||||
text += " ms)";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string BuildImportStatusTooltipText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
|
||||
if (!status.HasValue()) {
|
||||
return "No import operations have been recorded in this session.";
|
||||
}
|
||||
|
||||
std::string tooltip;
|
||||
tooltip += "Operation: ";
|
||||
tooltip += status.operation.CStr();
|
||||
tooltip += "\nState: ";
|
||||
tooltip += status.inProgress ? "Running" : (status.success ? "Succeeded" : "Failed");
|
||||
const std::uint64_t elapsedMs = ResolveImportStatusElapsedMs(status);
|
||||
if (elapsedMs > 0) {
|
||||
tooltip += "\nDuration: ";
|
||||
tooltip += std::to_string(elapsedMs);
|
||||
tooltip += " ms";
|
||||
}
|
||||
if (!status.targetPath.Empty()) {
|
||||
tooltip += "\nTarget: ";
|
||||
tooltip += status.targetPath.CStr();
|
||||
}
|
||||
if (status.importedAssetCount > 0) {
|
||||
tooltip += "\nImported: ";
|
||||
tooltip += std::to_string(status.importedAssetCount);
|
||||
}
|
||||
if (status.removedArtifactCount > 0) {
|
||||
tooltip += "\nRemoved orphans: ";
|
||||
tooltip += std::to_string(status.removedArtifactCount);
|
||||
}
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
|
||||
if (!pendingAction) {
|
||||
@@ -373,6 +456,10 @@ void ProjectPanel::Render() {
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderToolbar() {
|
||||
const auto importStatus = ::XCEngine::Resources::ResourceManager::Get().GetProjectAssetImportStatus();
|
||||
const std::string importStatusText = BuildImportStatusText(importStatus);
|
||||
const std::string importStatusTooltip = BuildImportStatusTooltipText(importStatus);
|
||||
|
||||
UI::PanelToolbarScope toolbar(
|
||||
"ProjectToolbar",
|
||||
kProjectToolbarHeight,
|
||||
@@ -387,12 +474,23 @@ void ProjectPanel::RenderToolbar() {
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f));
|
||||
if (ImGui::BeginTable("##ProjectToolbarLayout", 2, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("##Spacer", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("##ImportStatus", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f);
|
||||
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ResolveImportStatusColor(importStatus));
|
||||
ImGui::TextUnformatted(importStatusText.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered() && !importStatusTooltip.empty()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 36.0f);
|
||||
ImGui::TextUnformatted(importStatusTooltip.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer));
|
||||
|
||||
@@ -413,14 +413,14 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIElementTree.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIContext.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIBuildContext.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIElementTree.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/Theme.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleSet.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleResolver.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/StyleTypes.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/Theme.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/StyleResolver.cpp
|
||||
/src/UI/Core/UIElementTree.cpp
|
||||
/include/XCEngine/UI/Style/StyleTypes.h
|
||||
/include/XCEngine/UI/Style/Theme.h
|
||||
/include/XCEngine/UI/Style/StyleSet.h
|
||||
/include/XCEngine/UI/Style/StyleResolver.h
|
||||
/src/UI/Style/StyleTypes.cpp
|
||||
/src/UI/Style/Theme.cpp
|
||||
/src/UI/Style/StyleResolver.cpp
|
||||
|
||||
# Input
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Input/InputTypes.h
|
||||
|
||||
@@ -10,28 +10,91 @@ namespace Resources {
|
||||
|
||||
class AssetImportService {
|
||||
public:
|
||||
struct ImportStatusSnapshot {
|
||||
Core::uint64 revision = 0;
|
||||
bool inProgress = false;
|
||||
bool success = false;
|
||||
Containers::String operation;
|
||||
Containers::String targetPath;
|
||||
Containers::String message;
|
||||
Core::uint64 startedAtMs = 0;
|
||||
Core::uint64 completedAtMs = 0;
|
||||
Core::uint64 durationMs = 0;
|
||||
Core::uint32 importedAssetCount = 0;
|
||||
Core::uint32 removedArtifactCount = 0;
|
||||
|
||||
bool HasValue() const {
|
||||
return revision != 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct LookupSnapshot {
|
||||
std::unordered_map<std::string, AssetGUID> assetGuidByPathKey;
|
||||
std::unordered_map<AssetGUID, Containers::String> assetPathByGuid;
|
||||
|
||||
void Clear() {
|
||||
assetGuidByPathKey.clear();
|
||||
assetPathByGuid.clear();
|
||||
}
|
||||
};
|
||||
|
||||
struct ImportedAsset {
|
||||
bool exists = false;
|
||||
bool artifactReady = false;
|
||||
bool imported = false;
|
||||
Containers::String absolutePath;
|
||||
Containers::String relativePath;
|
||||
AssetGUID assetGuid;
|
||||
ResourceType resourceType = ResourceType::Unknown;
|
||||
Containers::String runtimeLoadPath;
|
||||
Containers::String artifactDirectory;
|
||||
LocalID mainLocalID = kMainAssetLocalID;
|
||||
};
|
||||
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
|
||||
void SetProjectRoot(const Containers::String& projectRoot);
|
||||
Containers::String GetProjectRoot() const;
|
||||
Containers::String GetLibraryRoot() const;
|
||||
|
||||
bool BootstrapProject();
|
||||
void Refresh();
|
||||
bool ClearLibraryCache();
|
||||
bool RebuildLibraryCache();
|
||||
bool ReimportAllAssets();
|
||||
bool ReimportAsset(const Containers::String& requestPath,
|
||||
ImportedAsset& outAsset);
|
||||
ImportStatusSnapshot GetLastImportStatus() const;
|
||||
bool TryGetImportableResourceType(const Containers::String& requestPath,
|
||||
ResourceType& outType) const;
|
||||
|
||||
bool EnsureArtifact(const Containers::String& requestPath,
|
||||
ResourceType requestedType,
|
||||
AssetDatabase::ResolvedAsset& outAsset);
|
||||
ImportedAsset& outAsset);
|
||||
bool TryGetAssetRef(const Containers::String& requestPath,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const;
|
||||
bool TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const;
|
||||
void BuildLookupSnapshot(std::unordered_map<std::string, AssetGUID>& outPathToGuid,
|
||||
std::unordered_map<AssetGUID, Containers::String>& outGuidToPath) const;
|
||||
void BuildLookupSnapshot(LookupSnapshot& outSnapshot) const;
|
||||
|
||||
private:
|
||||
static ImportedAsset ConvertResolvedAsset(const AssetDatabase::ResolvedAsset& resolvedAsset);
|
||||
void ResetImportStatusLocked();
|
||||
void BeginImportStatusLocked(const Containers::String& operation,
|
||||
const Containers::String& targetPath,
|
||||
const Containers::String& message);
|
||||
void FinishImportStatusLocked(const Containers::String& operation,
|
||||
const Containers::String& targetPath,
|
||||
bool success,
|
||||
const Containers::String& message,
|
||||
const AssetDatabase::MaintenanceStats& stats);
|
||||
|
||||
mutable std::recursive_mutex m_mutex;
|
||||
Containers::String m_projectRoot;
|
||||
AssetDatabase m_assetDatabase;
|
||||
ImportStatusSnapshot m_lastImportStatus;
|
||||
Core::uint64 m_nextImportStatusRevision = 1;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
|
||||
@@ -18,22 +18,26 @@ public:
|
||||
ResourceHandle() = default;
|
||||
|
||||
explicit ResourceHandle(T* resource)
|
||||
: m_resource(resource) {
|
||||
if (m_resource) {
|
||||
ResourceManager::Get().AddRef(m_resource->GetGUID());
|
||||
: m_resource(resource),
|
||||
m_guid(resource != nullptr ? resource->GetGUID() : ResourceGUID()) {
|
||||
if (m_guid.IsValid()) {
|
||||
ResourceManager::Get().AddRef(m_guid);
|
||||
}
|
||||
}
|
||||
|
||||
ResourceHandle(const ResourceHandle& other)
|
||||
: m_resource(other.m_resource) {
|
||||
if (m_resource) {
|
||||
ResourceManager::Get().AddRef(m_resource->GetGUID());
|
||||
: m_resource(other.m_resource),
|
||||
m_guid(other.m_guid) {
|
||||
if (m_guid.IsValid()) {
|
||||
ResourceManager::Get().AddRef(m_guid);
|
||||
}
|
||||
}
|
||||
|
||||
ResourceHandle(ResourceHandle&& other) noexcept
|
||||
: m_resource(other.m_resource) {
|
||||
: m_resource(other.m_resource),
|
||||
m_guid(other.m_guid) {
|
||||
other.m_resource = nullptr;
|
||||
other.m_guid = ResourceGUID();
|
||||
}
|
||||
|
||||
~ResourceHandle() {
|
||||
@@ -44,8 +48,9 @@ public:
|
||||
if (this != &other) {
|
||||
Reset();
|
||||
m_resource = other.m_resource;
|
||||
if (m_resource) {
|
||||
ResourceManager::Get().AddRef(m_resource->GetGUID());
|
||||
m_guid = other.m_guid;
|
||||
if (m_guid.IsValid()) {
|
||||
ResourceManager::Get().AddRef(m_guid);
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
@@ -55,7 +60,9 @@ public:
|
||||
if (this != &other) {
|
||||
Reset();
|
||||
m_resource = other.m_resource;
|
||||
m_guid = other.m_guid;
|
||||
other.m_resource = nullptr;
|
||||
other.m_guid = ResourceGUID();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
@@ -64,11 +71,11 @@ public:
|
||||
T* operator->() const { return m_resource; }
|
||||
T& operator*() const { return *m_resource; }
|
||||
|
||||
bool IsValid() const { return m_resource != nullptr && m_resource->IsValid(); }
|
||||
bool IsValid() const { return m_resource != nullptr && m_guid.IsValid() && m_resource->IsValid(); }
|
||||
explicit operator bool() const { return IsValid(); }
|
||||
|
||||
ResourceGUID GetGUID() const {
|
||||
return m_resource ? m_resource->GetGUID() : ResourceGUID(0);
|
||||
return m_guid;
|
||||
}
|
||||
|
||||
ResourceType GetResourceType() const {
|
||||
@@ -76,18 +83,21 @@ public:
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
if (m_resource) {
|
||||
ResourceManager::Get().Release(m_resource->GetGUID());
|
||||
if (m_guid.IsValid()) {
|
||||
ResourceManager::Get().Release(m_guid);
|
||||
m_resource = nullptr;
|
||||
m_guid = ResourceGUID();
|
||||
}
|
||||
}
|
||||
|
||||
void Swap(ResourceHandle& other) {
|
||||
std::swap(m_resource, other.m_resource);
|
||||
std::swap(m_guid, other.m_guid);
|
||||
}
|
||||
|
||||
private:
|
||||
T* m_resource = nullptr;
|
||||
ResourceGUID m_guid;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
|
||||
void SetResourceRoot(const Containers::String& rootPath);
|
||||
const Containers::String& GetResourceRoot() const;
|
||||
bool BootstrapProjectAssets();
|
||||
|
||||
template<typename T>
|
||||
ResourceHandle<T> Load(const Containers::String& path, ImportSettings* settings = nullptr) {
|
||||
@@ -116,7 +117,13 @@ public:
|
||||
|
||||
Containers::Array<Containers::String> GetResourcePaths() const;
|
||||
void UnloadGroup(const Containers::Array<ResourceGUID>& guids);
|
||||
void RefreshAssetDatabase();
|
||||
void RefreshProjectAssets();
|
||||
bool CanReimportProjectAsset(const Containers::String& path) const;
|
||||
bool ReimportProjectAsset(const Containers::String& path);
|
||||
bool ClearProjectLibraryCache();
|
||||
bool RebuildProjectAssetCache();
|
||||
Containers::String GetProjectLibraryRoot() const;
|
||||
AssetImportService::ImportStatusSnapshot GetProjectAssetImportStatus() const;
|
||||
bool TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath) const;
|
||||
void BeginDeferredSceneLoad();
|
||||
|
||||
38
engine/include/XCEngine/Resources/UI/UIDocumentCompiler.h
Normal file
38
engine/include/XCEngine/Resources/UI/UIDocumentCompiler.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
struct UIDocumentCompileRequest {
|
||||
UIDocumentKind kind = UIDocumentKind::View;
|
||||
Containers::String path;
|
||||
Containers::String expectedRootTag;
|
||||
};
|
||||
|
||||
struct UIDocumentCompileResult {
|
||||
UIDocumentModel document = {};
|
||||
Containers::String errorMessage;
|
||||
bool succeeded = false;
|
||||
};
|
||||
|
||||
bool CompileUIDocument(
|
||||
const UIDocumentCompileRequest& request,
|
||||
UIDocumentCompileResult& outResult);
|
||||
|
||||
bool WriteUIDocumentArtifact(
|
||||
const Containers::String& artifactPath,
|
||||
const UIDocumentCompileResult& compileResult,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
bool LoadUIDocumentArtifact(
|
||||
const Containers::String& artifactPath,
|
||||
UIDocumentKind expectedKind,
|
||||
UIDocumentCompileResult& outResult);
|
||||
|
||||
Containers::String GetUIDocumentDefaultRootTag(UIDocumentKind kind);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
44
engine/include/XCEngine/Resources/UI/UIDocumentLoaders.h
Normal file
44
engine/include/XCEngine/Resources/UI/UIDocumentLoaders.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
#include <XCEngine/Resources/UI/UIDocuments.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class UIViewLoader : public IResourceLoader {
|
||||
public:
|
||||
ResourceType GetResourceType() const override { return ResourceType::UIView; }
|
||||
Containers::Array<Containers::String> GetSupportedExtensions() const override;
|
||||
bool CanLoad(const Containers::String& path) const override;
|
||||
LoadResult Load(const Containers::String& path, const ImportSettings* settings = nullptr) override;
|
||||
ImportSettings* GetDefaultSettings() const override { return nullptr; }
|
||||
|
||||
bool CompileDocument(const Containers::String& path, UIDocumentCompileResult& outResult) const;
|
||||
};
|
||||
|
||||
class UIThemeLoader : public IResourceLoader {
|
||||
public:
|
||||
ResourceType GetResourceType() const override { return ResourceType::UITheme; }
|
||||
Containers::Array<Containers::String> GetSupportedExtensions() const override;
|
||||
bool CanLoad(const Containers::String& path) const override;
|
||||
LoadResult Load(const Containers::String& path, const ImportSettings* settings = nullptr) override;
|
||||
ImportSettings* GetDefaultSettings() const override { return nullptr; }
|
||||
|
||||
bool CompileDocument(const Containers::String& path, UIDocumentCompileResult& outResult) const;
|
||||
};
|
||||
|
||||
class UISchemaLoader : public IResourceLoader {
|
||||
public:
|
||||
ResourceType GetResourceType() const override { return ResourceType::UISchema; }
|
||||
Containers::Array<Containers::String> GetSupportedExtensions() const override;
|
||||
bool CanLoad(const Containers::String& path) const override;
|
||||
LoadResult Load(const Containers::String& path, const ImportSettings* settings = nullptr) override;
|
||||
ImportSettings* GetDefaultSettings() const override { return nullptr; }
|
||||
|
||||
bool CompileDocument(const Containers::String& path, UIDocumentCompileResult& outResult) const;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
76
engine/include/XCEngine/Resources/UI/UIDocumentTypes.h
Normal file
76
engine/include/XCEngine/Resources/UI/UIDocumentTypes.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
enum class UIDocumentKind : Core::uint8 {
|
||||
View = 0,
|
||||
Theme,
|
||||
Schema
|
||||
};
|
||||
|
||||
enum class UIDocumentDiagnosticSeverity : Core::uint8 {
|
||||
Info = 0,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
|
||||
struct UIDocumentSourceLocation {
|
||||
Core::uint32 line = 1;
|
||||
Core::uint32 column = 1;
|
||||
};
|
||||
|
||||
struct UIDocumentAttribute {
|
||||
Containers::String name;
|
||||
Containers::String value;
|
||||
};
|
||||
|
||||
struct UIDocumentDiagnostic {
|
||||
UIDocumentDiagnosticSeverity severity = UIDocumentDiagnosticSeverity::Error;
|
||||
UIDocumentSourceLocation location = {};
|
||||
Containers::String message;
|
||||
};
|
||||
|
||||
struct UIDocumentNode {
|
||||
Containers::String tagName;
|
||||
Containers::Array<UIDocumentAttribute> attributes;
|
||||
Containers::Array<UIDocumentNode> children;
|
||||
UIDocumentSourceLocation location = {};
|
||||
bool selfClosing = false;
|
||||
|
||||
const UIDocumentAttribute* FindAttribute(const Containers::String& name) const {
|
||||
for (const UIDocumentAttribute& attribute : attributes) {
|
||||
if (attribute.name == name) {
|
||||
return &attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct UIDocumentModel {
|
||||
UIDocumentKind kind = UIDocumentKind::View;
|
||||
Containers::String sourcePath;
|
||||
Containers::String displayName;
|
||||
UIDocumentNode rootNode;
|
||||
Containers::Array<Containers::String> dependencies;
|
||||
Containers::Array<UIDocumentDiagnostic> diagnostics;
|
||||
bool valid = false;
|
||||
|
||||
void Clear() {
|
||||
sourcePath.Clear();
|
||||
displayName.Clear();
|
||||
rootNode = UIDocumentNode();
|
||||
dependencies.Clear();
|
||||
diagnostics.Clear();
|
||||
valid = false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
49
engine/include/XCEngine/Resources/UI/UIDocuments.h
Normal file
49
engine/include/XCEngine/Resources/UI/UIDocuments.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class UIDocumentResource : public IResource {
|
||||
public:
|
||||
const Containers::String& GetName() const override { return m_name; }
|
||||
const Containers::String& GetPath() const override { return m_path; }
|
||||
ResourceGUID GetGUID() const override { return m_guid; }
|
||||
bool IsValid() const override { return m_isValid; }
|
||||
size_t GetMemorySize() const override { return m_memorySize; }
|
||||
void Release() override;
|
||||
|
||||
const UIDocumentModel& GetDocument() const { return m_document; }
|
||||
const UIDocumentNode& GetRootNode() const { return m_document.rootNode; }
|
||||
const Containers::Array<Containers::String>& GetDependencies() const { return m_document.dependencies; }
|
||||
const Containers::Array<UIDocumentDiagnostic>& GetDiagnostics() const { return m_document.diagnostics; }
|
||||
const Containers::String& GetSourcePath() const { return m_document.sourcePath; }
|
||||
|
||||
void SetDocumentModel(const UIDocumentModel& document);
|
||||
void SetDocumentModel(UIDocumentModel&& document);
|
||||
|
||||
protected:
|
||||
void RecalculateMemorySize();
|
||||
|
||||
UIDocumentModel m_document = {};
|
||||
};
|
||||
|
||||
class UIView : public UIDocumentResource {
|
||||
public:
|
||||
ResourceType GetType() const override { return ResourceType::UIView; }
|
||||
};
|
||||
|
||||
class UITheme : public UIDocumentResource {
|
||||
public:
|
||||
ResourceType GetType() const override { return ResourceType::UITheme; }
|
||||
};
|
||||
|
||||
class UISchema : public UIDocumentResource {
|
||||
public:
|
||||
ResourceType GetType() const override { return ResourceType::UISchema; }
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -613,34 +614,21 @@ void DestroyImportedMesh(Mesh* mesh) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<Material*> materials;
|
||||
materials.reserve(mesh->GetMaterials().Size());
|
||||
for (Material* material : mesh->GetMaterials()) {
|
||||
if (material != nullptr) {
|
||||
materials.push_back(material);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Texture*> textures;
|
||||
textures.reserve(mesh->GetTextures().Size());
|
||||
for (Texture* texture : mesh->GetTextures()) {
|
||||
if (texture != nullptr) {
|
||||
textures.push_back(texture);
|
||||
}
|
||||
}
|
||||
|
||||
delete mesh;
|
||||
for (Material* material : materials) {
|
||||
delete material;
|
||||
}
|
||||
for (Texture* texture : textures) {
|
||||
delete texture;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AssetDatabase::ClearLastErrorMessage() {
|
||||
m_lastErrorMessage.Clear();
|
||||
}
|
||||
|
||||
void AssetDatabase::SetLastErrorMessage(const Containers::String& message) {
|
||||
m_lastErrorMessage = message;
|
||||
}
|
||||
|
||||
void AssetDatabase::Initialize(const Containers::String& projectRoot) {
|
||||
ClearLastErrorMessage();
|
||||
m_projectRoot = NormalizePathString(projectRoot);
|
||||
m_assetsRoot = NormalizePathString(fs::path(m_projectRoot.CStr()) / "Assets");
|
||||
m_libraryRoot = NormalizePathString(fs::path(m_projectRoot.CStr()) / "Library");
|
||||
@@ -650,12 +638,12 @@ void AssetDatabase::Initialize(const Containers::String& projectRoot) {
|
||||
EnsureProjectLayout();
|
||||
LoadSourceAssetDB();
|
||||
LoadArtifactDB();
|
||||
ScanAssets();
|
||||
}
|
||||
|
||||
void AssetDatabase::Shutdown() {
|
||||
SaveSourceAssetDB();
|
||||
SaveArtifactDB();
|
||||
ClearLastErrorMessage();
|
||||
|
||||
m_projectRoot.Clear();
|
||||
m_assetsRoot.Clear();
|
||||
@@ -668,6 +656,7 @@ void AssetDatabase::Shutdown() {
|
||||
}
|
||||
|
||||
AssetDatabase::MaintenanceStats AssetDatabase::Refresh() {
|
||||
ClearLastErrorMessage();
|
||||
return ScanAssets();
|
||||
}
|
||||
|
||||
@@ -767,6 +756,7 @@ bool AssetDatabase::ReimportAsset(const Containers::String& requestPath,
|
||||
ResolvedAsset& outAsset,
|
||||
MaintenanceStats* outStats) {
|
||||
outAsset = ResolvedAsset();
|
||||
ClearLastErrorMessage();
|
||||
if (outStats != nullptr) {
|
||||
*outStats = MaintenanceStats();
|
||||
}
|
||||
@@ -774,26 +764,33 @@ bool AssetDatabase::ReimportAsset(const Containers::String& requestPath,
|
||||
Containers::String absolutePath;
|
||||
Containers::String relativePath;
|
||||
if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) {
|
||||
SetLastErrorMessage(Containers::String("Unable to resolve asset path: ") + requestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path absoluteFsPath(absolutePath.CStr());
|
||||
if (!fs::exists(absoluteFsPath) || fs::is_directory(absoluteFsPath)) {
|
||||
SetLastErrorMessage(Containers::String("Asset source file does not exist: ") + absolutePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
SourceAssetRecord sourceRecord;
|
||||
if (!EnsureMetaForPath(absoluteFsPath, false, sourceRecord)) {
|
||||
SetLastErrorMessage(Containers::String("Failed to prepare asset metadata: ") + absolutePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
|
||||
if (primaryType == ResourceType::Unknown) {
|
||||
SetLastErrorMessage(Containers::String("Asset type is not importable: ") + requestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
ArtifactRecord rebuiltRecord;
|
||||
if (!ImportAsset(sourceRecord, rebuiltRecord)) {
|
||||
if (m_lastErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(Containers::String("Failed to import asset: ") + requestPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -811,10 +808,12 @@ bool AssetDatabase::ReimportAsset(const Containers::String& requestPath,
|
||||
}
|
||||
|
||||
PopulateResolvedAssetResult(m_projectRoot, sourceRecord, rebuiltRecord, true, outAsset);
|
||||
ClearLastErrorMessage();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetDatabase::ReimportAllAssets(MaintenanceStats* outStats) {
|
||||
ClearLastErrorMessage();
|
||||
if (outStats != nullptr) {
|
||||
*outStats = MaintenanceStats();
|
||||
}
|
||||
@@ -1350,6 +1349,15 @@ Containers::String AssetDatabase::GetImporterNameForPath(const Containers::Strin
|
||||
}
|
||||
|
||||
const std::string ext = ToLowerCopy(fs::path(relativePath.CStr()).extension().string());
|
||||
if (ext == ".xcui") {
|
||||
return Containers::String("UIViewImporter");
|
||||
}
|
||||
if (ext == ".xctheme") {
|
||||
return Containers::String("UIThemeImporter");
|
||||
}
|
||||
if (ext == ".xcschema") {
|
||||
return Containers::String("UISchemaImporter");
|
||||
}
|
||||
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || ext == ".tga" || ext == ".gif" || ext == ".hdr") {
|
||||
return Containers::String("TextureImporter");
|
||||
}
|
||||
@@ -1367,6 +1375,15 @@ Containers::String AssetDatabase::GetImporterNameForPath(const Containers::Strin
|
||||
}
|
||||
|
||||
ResourceType AssetDatabase::GetPrimaryResourceTypeForImporter(const Containers::String& importerName) {
|
||||
if (importerName == "UIViewImporter") {
|
||||
return ResourceType::UIView;
|
||||
}
|
||||
if (importerName == "UIThemeImporter") {
|
||||
return ResourceType::UITheme;
|
||||
}
|
||||
if (importerName == "UISchemaImporter") {
|
||||
return ResourceType::UISchema;
|
||||
}
|
||||
if (importerName == "TextureImporter") {
|
||||
return ResourceType::Texture;
|
||||
}
|
||||
@@ -1410,6 +1427,12 @@ bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord) {
|
||||
const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
|
||||
switch (primaryType) {
|
||||
case ResourceType::UIView:
|
||||
return ImportUIDocumentAsset(sourceRecord, UIDocumentKind::View, "main.xcuiasset", ResourceType::UIView, outRecord);
|
||||
case ResourceType::UITheme:
|
||||
return ImportUIDocumentAsset(sourceRecord, UIDocumentKind::Theme, "main.xcthemeasset", ResourceType::UITheme, outRecord);
|
||||
case ResourceType::UISchema:
|
||||
return ImportUIDocumentAsset(sourceRecord, UIDocumentKind::Schema, "main.xcschemaasset", ResourceType::UISchema, outRecord);
|
||||
case ResourceType::Texture:
|
||||
return ImportTextureAsset(sourceRecord, outRecord);
|
||||
case ResourceType::Material:
|
||||
@@ -1419,6 +1442,7 @@ bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord,
|
||||
case ResourceType::Shader:
|
||||
return ImportShaderAsset(sourceRecord, outRecord);
|
||||
default:
|
||||
SetLastErrorMessage(Containers::String("No importer available for asset: ") + sourceRecord.relativePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1427,10 +1451,12 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
ResourceType requestedType,
|
||||
ResolvedAsset& outAsset) {
|
||||
outAsset = ResolvedAsset();
|
||||
ClearLastErrorMessage();
|
||||
|
||||
Containers::String absolutePath;
|
||||
Containers::String relativePath;
|
||||
if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) {
|
||||
SetLastErrorMessage(Containers::String("Unable to resolve asset path: ") + requestPath);
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
@@ -1441,6 +1467,7 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
|
||||
const fs::path absoluteFsPath(absolutePath.CStr());
|
||||
if (!fs::exists(absoluteFsPath)) {
|
||||
SetLastErrorMessage(Containers::String("Asset source file does not exist: ") + absolutePath);
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
@@ -1454,6 +1481,7 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
|
||||
SourceAssetRecord sourceRecord;
|
||||
if (!EnsureMetaForPath(absoluteFsPath, fs::is_directory(absoluteFsPath), sourceRecord)) {
|
||||
SetLastErrorMessage(Containers::String("Failed to prepare asset metadata: ") + absolutePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1472,6 +1500,13 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
|
||||
const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
|
||||
if (primaryType == ResourceType::Unknown || primaryType != requestedType) {
|
||||
SetLastErrorMessage(
|
||||
Containers::String("Asset type mismatch for ") +
|
||||
requestPath +
|
||||
": requested " +
|
||||
GetResourceTypeName(requestedType) +
|
||||
", importer produces " +
|
||||
GetResourceTypeName(primaryType));
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
@@ -1499,6 +1534,9 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
}
|
||||
ArtifactRecord rebuiltRecord;
|
||||
if (!ImportAsset(sourceRecord, rebuiltRecord)) {
|
||||
if (m_lastErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(Containers::String("Failed to import asset: ") + requestPath);
|
||||
}
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::FileSystem,
|
||||
@@ -1518,11 +1556,13 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
}
|
||||
|
||||
if (artifactRecord == nullptr) {
|
||||
SetLastErrorMessage(Containers::String("Imported asset did not produce an artifact: ") + requestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
outAsset.exists = true;
|
||||
PopulateResolvedAssetResult(m_projectRoot, sourceRecord, *artifactRecord, outAsset.imported, outAsset);
|
||||
ClearLastErrorMessage();
|
||||
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
@@ -1789,6 +1829,88 @@ bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetDatabase::ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord,
|
||||
UIDocumentKind kind,
|
||||
const char* artifactFileName,
|
||||
ResourceType resourceType,
|
||||
ArtifactRecord& outRecord) {
|
||||
const Containers::String absolutePath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
|
||||
|
||||
UIDocumentCompileResult compileResult = {};
|
||||
if (!CompileUIDocument(
|
||||
UIDocumentCompileRequest{kind, absolutePath, GetUIDocumentDefaultRootTag(kind)},
|
||||
compileResult)) {
|
||||
SetLastErrorMessage(
|
||||
!compileResult.errorMessage.Empty()
|
||||
? compileResult.errorMessage
|
||||
: Containers::String("Failed to compile UI document: ") + sourceRecord.relativePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<ArtifactDependencyRecord> dependencies;
|
||||
dependencies.reserve(compileResult.document.dependencies.Size());
|
||||
std::unordered_set<std::string> seenDependencyPaths;
|
||||
for (const Containers::String& dependencyPath : compileResult.document.dependencies) {
|
||||
if (dependencyPath.Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ArtifactDependencyRecord dependency;
|
||||
if (!CaptureDependencyRecord(fs::path(dependencyPath.CStr()), dependency)) {
|
||||
SetLastErrorMessage(
|
||||
Containers::String("Failed to capture XCUI dependency metadata: ") + dependencyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string dependencyKey = ToStdString(dependency.path);
|
||||
if (seenDependencyPaths.insert(dependencyKey).second) {
|
||||
dependencies.push_back(std::move(dependency));
|
||||
}
|
||||
}
|
||||
|
||||
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
|
||||
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
|
||||
const Containers::String mainArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / artifactFileName);
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
ec.clear();
|
||||
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
if (ec) {
|
||||
SetLastErrorMessage(
|
||||
Containers::String("Failed to create UI artifact directory: ") + artifactDir);
|
||||
return false;
|
||||
}
|
||||
|
||||
Containers::String writeErrorMessage;
|
||||
const Containers::String absoluteArtifactPath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr());
|
||||
if (!WriteUIDocumentArtifact(absoluteArtifactPath, compileResult, &writeErrorMessage)) {
|
||||
SetLastErrorMessage(
|
||||
!writeErrorMessage.Empty()
|
||||
? writeErrorMessage
|
||||
: Containers::String("Failed to write UI document artifact: ") + mainArtifactPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
outRecord.artifactKey = artifactKey;
|
||||
outRecord.assetGuid = sourceRecord.guid;
|
||||
outRecord.importerName = sourceRecord.importerName;
|
||||
outRecord.importerVersion = sourceRecord.importerVersion;
|
||||
outRecord.resourceType = resourceType;
|
||||
outRecord.artifactDirectory = artifactDir;
|
||||
outRecord.mainArtifactPath = mainArtifactPath;
|
||||
outRecord.sourceHash = sourceRecord.sourceHash;
|
||||
outRecord.metaHash = sourceRecord.metaHash;
|
||||
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
|
||||
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
|
||||
outRecord.mainLocalID = kMainAssetLocalID;
|
||||
outRecord.dependencies = std::move(dependencies);
|
||||
return true;
|
||||
}
|
||||
|
||||
Containers::String AssetDatabase::BuildArtifactKey(
|
||||
const AssetDatabase::SourceAssetRecord& sourceRecord,
|
||||
const std::vector<AssetDatabase::ArtifactDependencyRecord>& dependencies) const {
|
||||
@@ -1891,10 +2013,6 @@ bool AssetDatabase::AreDependenciesCurrent(
|
||||
currentWriteTime != dependency.writeTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputeFileHash(resolvedPath) != dependency.hash) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,8 +1,42 @@
|
||||
#include <XCEngine/Core/Asset/AssetImportService.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
Containers::String ToContainersString(const std::string& value) {
|
||||
return Containers::String(value.c_str());
|
||||
}
|
||||
|
||||
Containers::String BuildStatsSuffix(const AssetDatabase::MaintenanceStats& stats) {
|
||||
std::string suffix;
|
||||
if (stats.importedAssetCount > 0) {
|
||||
suffix += " imported=";
|
||||
suffix += std::to_string(stats.importedAssetCount);
|
||||
}
|
||||
if (stats.removedArtifactCount > 0) {
|
||||
suffix += " removedOrphans=";
|
||||
suffix += std::to_string(stats.removedArtifactCount);
|
||||
}
|
||||
|
||||
return ToContainersString(suffix);
|
||||
}
|
||||
|
||||
Core::uint64 GetCurrentSteadyTimeMs() {
|
||||
return static_cast<Core::uint64>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AssetImportService::Initialize() {
|
||||
}
|
||||
|
||||
@@ -10,6 +44,7 @@ void AssetImportService::Shutdown() {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_assetDatabase.Shutdown();
|
||||
m_projectRoot.Clear();
|
||||
ResetImportStatusLocked();
|
||||
}
|
||||
|
||||
void AssetImportService::SetProjectRoot(const Containers::String& projectRoot) {
|
||||
@@ -24,6 +59,7 @@ void AssetImportService::SetProjectRoot(const Containers::String& projectRoot) {
|
||||
}
|
||||
|
||||
m_projectRoot = projectRoot;
|
||||
ResetImportStatusLocked();
|
||||
if (!m_projectRoot.Empty()) {
|
||||
m_assetDatabase.Initialize(m_projectRoot);
|
||||
}
|
||||
@@ -34,22 +70,225 @@ Containers::String AssetImportService::GetProjectRoot() const {
|
||||
return m_projectRoot;
|
||||
}
|
||||
|
||||
Containers::String AssetImportService::GetLibraryRoot() const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return m_assetDatabase.GetLibraryRoot();
|
||||
}
|
||||
|
||||
bool AssetImportService::BootstrapProject() {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
FinishImportStatusLocked(
|
||||
"Bootstrap Project",
|
||||
Containers::String(),
|
||||
false,
|
||||
"Cannot bootstrap project assets without an active project.",
|
||||
AssetDatabase::MaintenanceStats());
|
||||
return false;
|
||||
}
|
||||
|
||||
BeginImportStatusLocked(
|
||||
"Bootstrap Project",
|
||||
m_projectRoot,
|
||||
"Bootstrapping project Library state...");
|
||||
|
||||
const AssetDatabase::MaintenanceStats stats = m_assetDatabase.Refresh();
|
||||
FinishImportStatusLocked(
|
||||
"Bootstrap Project",
|
||||
m_projectRoot,
|
||||
true,
|
||||
Containers::String("Bootstrapped project Library state.") + BuildStatsSuffix(stats),
|
||||
stats);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AssetImportService::Refresh() {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (!m_projectRoot.Empty()) {
|
||||
m_assetDatabase.Refresh();
|
||||
const AssetDatabase::MaintenanceStats stats = m_assetDatabase.Refresh();
|
||||
if (stats.removedArtifactCount > 0) {
|
||||
FinishImportStatusLocked(
|
||||
"Refresh",
|
||||
m_projectRoot,
|
||||
true,
|
||||
Containers::String("Refresh removed orphan artifact entries.") + BuildStatsSuffix(stats),
|
||||
stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetImportService::ClearLibraryCache() {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
FinishImportStatusLocked(
|
||||
"Clear Library",
|
||||
Containers::String(),
|
||||
false,
|
||||
"Cannot clear Library cache without an active project.",
|
||||
AssetDatabase::MaintenanceStats());
|
||||
return false;
|
||||
}
|
||||
|
||||
BeginImportStatusLocked(
|
||||
"Clear Library",
|
||||
m_assetDatabase.GetLibraryRoot(),
|
||||
"Clearing Library cache...");
|
||||
|
||||
const Containers::String projectRoot = m_projectRoot;
|
||||
const Containers::String libraryRoot = m_assetDatabase.GetLibraryRoot();
|
||||
|
||||
m_assetDatabase.Shutdown();
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(fs::path(libraryRoot.CStr()), ec);
|
||||
|
||||
m_assetDatabase.Initialize(projectRoot);
|
||||
const AssetDatabase::MaintenanceStats stats = m_assetDatabase.Refresh();
|
||||
const bool succeeded = !ec;
|
||||
FinishImportStatusLocked(
|
||||
"Clear Library",
|
||||
libraryRoot,
|
||||
succeeded,
|
||||
succeeded
|
||||
? Containers::String("Cleared Library cache and rebuilt source asset lookup.") + BuildStatsSuffix(stats)
|
||||
: Containers::String("Failed to clear Library cache."),
|
||||
stats);
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool AssetImportService::RebuildLibraryCache() {
|
||||
if (!ClearLibraryCache()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ReimportAllAssets();
|
||||
}
|
||||
|
||||
bool AssetImportService::ReimportAllAssets() {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
FinishImportStatusLocked(
|
||||
"Reimport All Assets",
|
||||
Containers::String(),
|
||||
false,
|
||||
"Cannot reimport assets without an active project.",
|
||||
AssetDatabase::MaintenanceStats());
|
||||
return false;
|
||||
}
|
||||
|
||||
BeginImportStatusLocked(
|
||||
"Reimport All Assets",
|
||||
m_projectRoot,
|
||||
"Reimporting all project assets...");
|
||||
m_assetDatabase.Refresh();
|
||||
|
||||
AssetDatabase::MaintenanceStats stats;
|
||||
const bool succeeded = m_assetDatabase.ReimportAllAssets(&stats);
|
||||
FinishImportStatusLocked(
|
||||
"Reimport All Assets",
|
||||
m_projectRoot,
|
||||
succeeded,
|
||||
succeeded
|
||||
? Containers::String("Reimported all project assets.") + BuildStatsSuffix(stats)
|
||||
: Containers::String("Reimport all assets completed with failures.") + BuildStatsSuffix(stats),
|
||||
stats);
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool AssetImportService::ReimportAsset(const Containers::String& requestPath,
|
||||
ImportedAsset& outAsset) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
FinishImportStatusLocked(
|
||||
"Reimport Asset",
|
||||
requestPath,
|
||||
false,
|
||||
"Cannot reimport an asset without an active project.",
|
||||
AssetDatabase::MaintenanceStats());
|
||||
return false;
|
||||
}
|
||||
|
||||
BeginImportStatusLocked(
|
||||
"Reimport Asset",
|
||||
requestPath,
|
||||
Containers::String("Reimporting asset: ") + requestPath);
|
||||
m_assetDatabase.Refresh();
|
||||
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
AssetDatabase::MaintenanceStats stats;
|
||||
if (!m_assetDatabase.ReimportAsset(requestPath, resolvedAsset, &stats)) {
|
||||
const Containers::String databaseError = m_assetDatabase.GetLastErrorMessage();
|
||||
FinishImportStatusLocked(
|
||||
"Reimport Asset",
|
||||
requestPath,
|
||||
false,
|
||||
!databaseError.Empty()
|
||||
? Containers::String("Failed to reimport asset: ") + requestPath + " - " + databaseError
|
||||
: Containers::String("Failed to reimport asset: ") + requestPath,
|
||||
stats);
|
||||
return false;
|
||||
}
|
||||
|
||||
outAsset = ConvertResolvedAsset(resolvedAsset);
|
||||
FinishImportStatusLocked(
|
||||
"Reimport Asset",
|
||||
requestPath,
|
||||
true,
|
||||
Containers::String("Reimported asset: ") + requestPath + BuildStatsSuffix(stats),
|
||||
stats);
|
||||
return true;
|
||||
}
|
||||
|
||||
AssetImportService::ImportStatusSnapshot AssetImportService::GetLastImportStatus() const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return m_lastImportStatus;
|
||||
}
|
||||
|
||||
bool AssetImportService::TryGetImportableResourceType(const Containers::String& requestPath,
|
||||
ResourceType& outType) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
outType = ResourceType::Unknown;
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_assetDatabase.TryGetImportableResourceType(requestPath, outType);
|
||||
}
|
||||
|
||||
bool AssetImportService::EnsureArtifact(const Containers::String& requestPath,
|
||||
ResourceType requestedType,
|
||||
AssetDatabase::ResolvedAsset& outAsset) {
|
||||
ImportedAsset& outAsset) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_assetDatabase.EnsureArtifact(requestPath, requestedType, outAsset);
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
if (!m_assetDatabase.EnsureArtifact(requestPath, requestedType, resolvedAsset)) {
|
||||
const Containers::String databaseError = m_assetDatabase.GetLastErrorMessage();
|
||||
FinishImportStatusLocked(
|
||||
"Import Asset",
|
||||
requestPath,
|
||||
false,
|
||||
!databaseError.Empty()
|
||||
? Containers::String("Failed to build asset artifact: ") + requestPath + " - " + databaseError
|
||||
: Containers::String("Failed to build asset artifact: ") + requestPath,
|
||||
AssetDatabase::MaintenanceStats());
|
||||
return false;
|
||||
}
|
||||
|
||||
outAsset = ConvertResolvedAsset(resolvedAsset);
|
||||
if (resolvedAsset.imported) {
|
||||
AssetDatabase::MaintenanceStats stats;
|
||||
stats.importedAssetCount = 1;
|
||||
FinishImportStatusLocked(
|
||||
"Import Asset",
|
||||
requestPath,
|
||||
true,
|
||||
Containers::String("Imported asset artifact: ") + requestPath,
|
||||
stats);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetImportService::TryGetAssetRef(const Containers::String& requestPath,
|
||||
@@ -72,16 +311,83 @@ bool AssetImportService::TryGetPrimaryAssetPath(const AssetGUID& guid, Container
|
||||
return m_assetDatabase.TryGetPrimaryAssetPath(guid, outRelativePath);
|
||||
}
|
||||
|
||||
void AssetImportService::BuildLookupSnapshot(std::unordered_map<std::string, AssetGUID>& outPathToGuid,
|
||||
std::unordered_map<AssetGUID, Containers::String>& outGuidToPath) const {
|
||||
void AssetImportService::BuildLookupSnapshot(LookupSnapshot& outSnapshot) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
outPathToGuid.clear();
|
||||
outGuidToPath.clear();
|
||||
outSnapshot.Clear();
|
||||
if (m_projectRoot.Empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_assetDatabase.BuildLookupSnapshot(outPathToGuid, outGuidToPath);
|
||||
m_assetDatabase.BuildLookupSnapshot(outSnapshot.assetGuidByPathKey, outSnapshot.assetPathByGuid);
|
||||
}
|
||||
|
||||
AssetImportService::ImportedAsset AssetImportService::ConvertResolvedAsset(
|
||||
const AssetDatabase::ResolvedAsset& resolvedAsset) {
|
||||
ImportedAsset importedAsset;
|
||||
importedAsset.exists = resolvedAsset.exists;
|
||||
importedAsset.artifactReady = resolvedAsset.artifactReady;
|
||||
importedAsset.imported = resolvedAsset.imported;
|
||||
importedAsset.absolutePath = resolvedAsset.absolutePath;
|
||||
importedAsset.relativePath = resolvedAsset.relativePath;
|
||||
importedAsset.assetGuid = resolvedAsset.assetGuid;
|
||||
importedAsset.resourceType = resolvedAsset.resourceType;
|
||||
importedAsset.runtimeLoadPath =
|
||||
resolvedAsset.artifactReady ? resolvedAsset.artifactMainPath : resolvedAsset.absolutePath;
|
||||
importedAsset.artifactDirectory = resolvedAsset.artifactDirectory;
|
||||
importedAsset.mainLocalID = resolvedAsset.mainLocalID;
|
||||
return importedAsset;
|
||||
}
|
||||
|
||||
void AssetImportService::ResetImportStatusLocked() {
|
||||
m_lastImportStatus = ImportStatusSnapshot();
|
||||
m_nextImportStatusRevision = 1;
|
||||
}
|
||||
|
||||
void AssetImportService::BeginImportStatusLocked(const Containers::String& operation,
|
||||
const Containers::String& targetPath,
|
||||
const Containers::String& message) {
|
||||
const Core::uint64 startedAtMs = GetCurrentSteadyTimeMs();
|
||||
m_lastImportStatus.revision = m_nextImportStatusRevision++;
|
||||
m_lastImportStatus.inProgress = true;
|
||||
m_lastImportStatus.success = false;
|
||||
m_lastImportStatus.operation = operation;
|
||||
m_lastImportStatus.targetPath = targetPath;
|
||||
m_lastImportStatus.message = message;
|
||||
m_lastImportStatus.startedAtMs = startedAtMs;
|
||||
m_lastImportStatus.completedAtMs = 0;
|
||||
m_lastImportStatus.durationMs = 0;
|
||||
m_lastImportStatus.importedAssetCount = 0;
|
||||
m_lastImportStatus.removedArtifactCount = 0;
|
||||
}
|
||||
|
||||
void AssetImportService::FinishImportStatusLocked(const Containers::String& operation,
|
||||
const Containers::String& targetPath,
|
||||
bool success,
|
||||
const Containers::String& message,
|
||||
const AssetDatabase::MaintenanceStats& stats) {
|
||||
const Core::uint64 completedAtMs = GetCurrentSteadyTimeMs();
|
||||
const Core::uint64 startedAtMs = m_lastImportStatus.startedAtMs != 0
|
||||
? m_lastImportStatus.startedAtMs
|
||||
: completedAtMs;
|
||||
m_lastImportStatus.revision = m_nextImportStatusRevision++;
|
||||
m_lastImportStatus.inProgress = false;
|
||||
m_lastImportStatus.success = success;
|
||||
m_lastImportStatus.operation = operation;
|
||||
m_lastImportStatus.targetPath = targetPath;
|
||||
m_lastImportStatus.message = message;
|
||||
m_lastImportStatus.startedAtMs = startedAtMs;
|
||||
m_lastImportStatus.completedAtMs = completedAtMs;
|
||||
m_lastImportStatus.durationMs = completedAtMs >= startedAtMs
|
||||
? completedAtMs - startedAtMs
|
||||
: 0;
|
||||
m_lastImportStatus.importedAssetCount = stats.importedAssetCount;
|
||||
m_lastImportStatus.removedArtifactCount = stats.removedArtifactCount;
|
||||
|
||||
if (success) {
|
||||
Debug::Logger::Get().Info(Debug::LogCategory::FileSystem, Containers::String("[AssetImport] ") + message);
|
||||
} else {
|
||||
Debug::Logger::Get().Error(Debug::LogCategory::FileSystem, Containers::String("[AssetImport] ") + message);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
|
||||
@@ -76,17 +76,16 @@ void ProjectAssetIndex::ResetProjectRoot(const Containers::String& projectRoot)
|
||||
}
|
||||
|
||||
void ProjectAssetIndex::RefreshFrom(const AssetImportService& importService) {
|
||||
std::unordered_map<std::string, AssetGUID> pathToGuid;
|
||||
std::unordered_map<AssetGUID, Containers::String> guidToPath;
|
||||
AssetImportService::LookupSnapshot snapshot;
|
||||
const Containers::String projectRoot = importService.GetProjectRoot();
|
||||
if (!projectRoot.Empty()) {
|
||||
importService.BuildLookupSnapshot(pathToGuid, guidToPath);
|
||||
importService.BuildLookupSnapshot(snapshot);
|
||||
}
|
||||
|
||||
std::unique_lock<std::shared_mutex> lock(m_mutex);
|
||||
m_projectRoot = projectRoot;
|
||||
m_assetGuidByPathKey = std::move(pathToGuid);
|
||||
m_assetPathByGuid = std::move(guidToPath);
|
||||
m_assetGuidByPathKey = std::move(snapshot.assetGuidByPathKey);
|
||||
m_assetPathByGuid = std::move(snapshot.assetPathByGuid);
|
||||
}
|
||||
|
||||
bool ProjectAssetIndex::TryGetAssetRef(AssetImportService& importService,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
|
||||
#include <exception>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -45,6 +46,9 @@ MaterialLoader g_materialLoader;
|
||||
MeshLoader g_meshLoader;
|
||||
ShaderLoader g_shaderLoader;
|
||||
TextureLoader g_textureLoader;
|
||||
UIViewLoader g_uiViewLoader;
|
||||
UIThemeLoader g_uiThemeLoader;
|
||||
UISchemaLoader g_uiSchemaLoader;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -85,6 +89,9 @@ void ResourceManager::EnsureInitialized() {
|
||||
RegisterBuiltinLoader(*this, g_meshLoader);
|
||||
RegisterBuiltinLoader(*this, g_shaderLoader);
|
||||
RegisterBuiltinLoader(*this, g_textureLoader);
|
||||
RegisterBuiltinLoader(*this, g_uiViewLoader);
|
||||
RegisterBuiltinLoader(*this, g_uiThemeLoader);
|
||||
RegisterBuiltinLoader(*this, g_uiSchemaLoader);
|
||||
m_assetImportService.Initialize();
|
||||
|
||||
m_asyncLoader = std::move(asyncLoader);
|
||||
@@ -106,11 +113,12 @@ void ResourceManager::Shutdown() {
|
||||
}
|
||||
|
||||
void ResourceManager::SetResourceRoot(const Containers::String& rootPath) {
|
||||
EnsureInitialized();
|
||||
m_resourceRoot = rootPath;
|
||||
if (!m_resourceRoot.Empty()) {
|
||||
ResourceFileSystem::Get().Initialize(rootPath);
|
||||
m_assetImportService.SetProjectRoot(rootPath);
|
||||
m_projectAssetIndex.RefreshFrom(m_assetImportService);
|
||||
BootstrapProjectAssets();
|
||||
} else {
|
||||
m_assetImportService.SetProjectRoot(Containers::String());
|
||||
ResourceFileSystem::Get().Shutdown();
|
||||
@@ -122,6 +130,16 @@ const Containers::String& ResourceManager::GetResourceRoot() const {
|
||||
return m_resourceRoot;
|
||||
}
|
||||
|
||||
bool ResourceManager::BootstrapProjectAssets() {
|
||||
if (m_resourceRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bootstrapped = m_assetImportService.BootstrapProject();
|
||||
m_projectAssetIndex.RefreshFrom(m_assetImportService);
|
||||
return bootstrapped;
|
||||
}
|
||||
|
||||
void ResourceManager::AddRef(ResourceGUID guid) {
|
||||
std::lock_guard lock(m_mutex);
|
||||
|
||||
@@ -199,6 +217,7 @@ void ResourceManager::Unload(ResourceGUID guid) {
|
||||
if (it != nullptr) {
|
||||
resource = *it;
|
||||
m_resourceCache.Erase(guid);
|
||||
m_cache.Remove(guid);
|
||||
m_guidToPath.Erase(guid);
|
||||
m_memoryUsage -= resource->GetMemorySize();
|
||||
}
|
||||
@@ -223,6 +242,7 @@ void ResourceManager::UnloadAll() {
|
||||
}
|
||||
|
||||
m_resourceCache.Clear();
|
||||
m_cache.Clear();
|
||||
m_refCounts.Clear();
|
||||
m_guidToPath.Clear();
|
||||
m_memoryUsage = 0;
|
||||
@@ -346,6 +366,7 @@ void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids)
|
||||
if (it != nullptr) {
|
||||
IResource* resource = *it;
|
||||
m_resourceCache.Erase(guid);
|
||||
m_cache.Remove(guid);
|
||||
m_guidToPath.Erase(guid);
|
||||
m_memoryUsage -= resource->GetMemorySize();
|
||||
resourcesToRelease.PushBack(resource);
|
||||
@@ -360,13 +381,69 @@ void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids)
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceManager::RefreshAssetDatabase() {
|
||||
void ResourceManager::RefreshProjectAssets() {
|
||||
if (!m_resourceRoot.Empty()) {
|
||||
m_assetImportService.Refresh();
|
||||
m_projectAssetIndex.RefreshFrom(m_assetImportService);
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceManager::CanReimportProjectAsset(const Containers::String& path) const {
|
||||
if (m_resourceRoot.Empty() || path.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ResourceType importType = ResourceType::Unknown;
|
||||
return m_assetImportService.TryGetImportableResourceType(path, importType);
|
||||
}
|
||||
|
||||
bool ResourceManager::ReimportProjectAsset(const Containers::String& path) {
|
||||
if (m_resourceRoot.Empty() || path.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UnloadAll();
|
||||
|
||||
AssetImportService::ImportedAsset importedAsset;
|
||||
const bool reimported = m_assetImportService.ReimportAsset(path, importedAsset);
|
||||
m_projectAssetIndex.RefreshFrom(m_assetImportService);
|
||||
if (reimported && importedAsset.assetGuid.IsValid() && !importedAsset.relativePath.Empty()) {
|
||||
m_projectAssetIndex.RememberResolvedPath(importedAsset.assetGuid, importedAsset.relativePath);
|
||||
}
|
||||
|
||||
return reimported;
|
||||
}
|
||||
|
||||
bool ResourceManager::ClearProjectLibraryCache() {
|
||||
if (m_resourceRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UnloadAll();
|
||||
const bool cleared = m_assetImportService.ClearLibraryCache();
|
||||
m_projectAssetIndex.RefreshFrom(m_assetImportService);
|
||||
return cleared;
|
||||
}
|
||||
|
||||
bool ResourceManager::RebuildProjectAssetCache() {
|
||||
if (m_resourceRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UnloadAll();
|
||||
const bool rebuilt = m_assetImportService.RebuildLibraryCache();
|
||||
m_projectAssetIndex.RefreshFrom(m_assetImportService);
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
Containers::String ResourceManager::GetProjectLibraryRoot() const {
|
||||
return m_assetImportService.GetLibraryRoot();
|
||||
}
|
||||
|
||||
AssetImportService::ImportStatusSnapshot ResourceManager::GetProjectAssetImportStatus() const {
|
||||
return m_assetImportService.GetLastImportStatus();
|
||||
}
|
||||
|
||||
bool ResourceManager::TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const {
|
||||
const bool resolved = m_projectAssetIndex.TryGetAssetRef(m_assetImportService, path, resourceType, outRef);
|
||||
|
||||
@@ -510,12 +587,18 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
}
|
||||
|
||||
Containers::String loadPath = path;
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
if (!m_resourceRoot.Empty() &&
|
||||
AssetImportService::ImportedAsset resolvedAsset;
|
||||
ResourceType importableType = ResourceType::Unknown;
|
||||
const bool shouldUseProjectArtifact =
|
||||
!m_resourceRoot.Empty() &&
|
||||
m_assetImportService.TryGetImportableResourceType(path, importableType) &&
|
||||
importableType == type;
|
||||
|
||||
if (shouldUseProjectArtifact &&
|
||||
m_assetImportService.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath);
|
||||
loadPath = resolvedAsset.artifactMainPath;
|
||||
loadPath = resolvedAsset.runtimeLoadPath;
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
|
||||
@@ -307,7 +307,16 @@ void WritePackedMaterialProperty(Core::uint8* destination, const MaterialPropert
|
||||
|
||||
Material::Material() = default;
|
||||
|
||||
Material::~Material() = default;
|
||||
Material::~Material() {
|
||||
// Imported materials can own nested handles and container state; explicitly
|
||||
// resetting them here avoids teardown-order issues during destruction.
|
||||
m_shader.Reset();
|
||||
m_tags = Containers::Array<MaterialTagEntry>();
|
||||
m_properties = Containers::HashMap<Containers::String, MaterialProperty>();
|
||||
m_constantLayout = Containers::Array<MaterialConstantFieldDesc>();
|
||||
m_constantBufferData = Containers::Array<Core::uint8>();
|
||||
m_textureBindings = Containers::Array<MaterialTextureBinding>();
|
||||
}
|
||||
|
||||
void Material::Release() {
|
||||
m_shader.Reset();
|
||||
|
||||
@@ -2,19 +2,42 @@
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
#include <XCEngine/Resources/Texture/Texture.h>
|
||||
#include <cstring>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
void DestroyOwnedResources(Containers::Array<T*>& resources) {
|
||||
std::unordered_set<T*> releasedResources;
|
||||
for (T* resource : resources) {
|
||||
if (resource == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (releasedResources.insert(resource).second) {
|
||||
delete resource;
|
||||
}
|
||||
}
|
||||
|
||||
resources.Clear();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Mesh::Mesh() = default;
|
||||
Mesh::~Mesh() = default;
|
||||
Mesh::~Mesh() {
|
||||
Release();
|
||||
}
|
||||
|
||||
void Mesh::Release() {
|
||||
m_vertexData.Clear();
|
||||
m_indexData.Clear();
|
||||
m_sections.Clear();
|
||||
m_materials.Clear();
|
||||
m_textures.Clear();
|
||||
DestroyOwnedResources(m_materials);
|
||||
DestroyOwnedResources(m_textures);
|
||||
m_vertexCount = 0;
|
||||
m_vertexStride = 0;
|
||||
m_attributes = VertexAttribute::Position;
|
||||
|
||||
878
engine/src/Resources/UI/UIDocumentCompiler.cpp
Normal file
878
engine/src/Resources/UI/UIDocumentCompiler.cpp
Normal file
@@ -0,0 +1,878 @@
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
Containers::String ToContainersString(const std::string& value) {
|
||||
return Containers::String(value.c_str());
|
||||
}
|
||||
|
||||
std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
std::string ToLowerCopy(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
Containers::String NormalizePathString(const fs::path& path) {
|
||||
return Containers::String(path.lexically_normal().generic_string().c_str());
|
||||
}
|
||||
|
||||
Containers::String FormatDiagnosticMessage(const Containers::String& path,
|
||||
const UIDocumentSourceLocation& location,
|
||||
const Containers::String& message) {
|
||||
return path +
|
||||
":" +
|
||||
Containers::String(std::to_string(location.line).c_str()) +
|
||||
":" +
|
||||
Containers::String(std::to_string(location.column).c_str()) +
|
||||
": " +
|
||||
message;
|
||||
}
|
||||
|
||||
void WriteString(std::ofstream& stream, const Containers::String& value) {
|
||||
const Core::uint32 length = static_cast<Core::uint32>(value.Length());
|
||||
stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
|
||||
if (length > 0) {
|
||||
stream.write(value.CStr(), length);
|
||||
}
|
||||
}
|
||||
|
||||
bool ReadString(std::ifstream& stream, Containers::String& outValue) {
|
||||
Core::uint32 length = 0;
|
||||
stream.read(reinterpret_cast<char*>(&length), sizeof(length));
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
outValue.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string buffer(length, '\0');
|
||||
stream.read(buffer.data(), length);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outValue = ToContainersString(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteNode(std::ofstream& stream, const UIDocumentNode& node) {
|
||||
WriteString(stream, node.tagName);
|
||||
|
||||
UIDocumentArtifactNodeHeader header;
|
||||
header.attributeCount = static_cast<Core::uint32>(node.attributes.Size());
|
||||
header.childCount = static_cast<Core::uint32>(node.children.Size());
|
||||
header.line = node.location.line;
|
||||
header.column = node.location.column;
|
||||
header.selfClosing = node.selfClosing ? 1u : 0u;
|
||||
stream.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const UIDocumentAttribute& attribute : node.attributes) {
|
||||
WriteString(stream, attribute.name);
|
||||
WriteString(stream, attribute.value);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const UIDocumentNode& child : node.children) {
|
||||
if (!WriteNode(stream, child)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<bool>(stream);
|
||||
}
|
||||
|
||||
bool ReadNode(std::ifstream& stream, UIDocumentNode& outNode) {
|
||||
if (!ReadString(stream, outNode.tagName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentArtifactNodeHeader header;
|
||||
stream.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outNode.location.line = header.line;
|
||||
outNode.location.column = header.column;
|
||||
outNode.selfClosing = header.selfClosing != 0;
|
||||
|
||||
outNode.attributes.Clear();
|
||||
outNode.children.Clear();
|
||||
outNode.attributes.Reserve(header.attributeCount);
|
||||
outNode.children.Reserve(header.childCount);
|
||||
|
||||
for (Core::uint32 index = 0; index < header.attributeCount; ++index) {
|
||||
UIDocumentAttribute attribute;
|
||||
if (!ReadString(stream, attribute.name) ||
|
||||
!ReadString(stream, attribute.value)) {
|
||||
return false;
|
||||
}
|
||||
outNode.attributes.PushBack(std::move(attribute));
|
||||
}
|
||||
|
||||
for (Core::uint32 index = 0; index < header.childCount; ++index) {
|
||||
UIDocumentNode child;
|
||||
if (!ReadNode(stream, child)) {
|
||||
return false;
|
||||
}
|
||||
outNode.children.PushBack(std::move(child));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsNameStartChar(char ch) {
|
||||
return std::isalpha(static_cast<unsigned char>(ch)) != 0 ||
|
||||
ch == '_' ||
|
||||
ch == ':';
|
||||
}
|
||||
|
||||
bool IsNameChar(char ch) {
|
||||
return std::isalnum(static_cast<unsigned char>(ch)) != 0 ||
|
||||
ch == '_' ||
|
||||
ch == '-' ||
|
||||
ch == ':' ||
|
||||
ch == '.';
|
||||
}
|
||||
|
||||
bool LooksLikeUIDocumentReference(const Containers::String& value) {
|
||||
const std::string trimmed = ToLowerCopy(ToStdString(value.Trim()));
|
||||
return trimmed.size() > 5 &&
|
||||
(trimmed.size() >= 5 && trimmed.rfind(".xcui") == trimmed.size() - 5 ||
|
||||
trimmed.size() >= 8 && trimmed.rfind(".xctheme") == trimmed.size() - 8 ||
|
||||
trimmed.size() >= 9 && trimmed.rfind(".xcschema") == trimmed.size() - 9);
|
||||
}
|
||||
|
||||
void AppendUniqueDependency(const Containers::String& dependencyPath,
|
||||
std::unordered_set<std::string>& seenDependencies,
|
||||
Containers::Array<Containers::String>& outDependencies) {
|
||||
const std::string key = ToLowerCopy(ToStdString(dependencyPath));
|
||||
if (seenDependencies.insert(key).second) {
|
||||
outDependencies.PushBack(dependencyPath);
|
||||
}
|
||||
}
|
||||
|
||||
bool CollectUIDocumentDependencies(const fs::path& sourcePath,
|
||||
const UIDocumentNode& node,
|
||||
Containers::Array<Containers::String>& outDependencies,
|
||||
Containers::Array<UIDocumentDiagnostic>& inOutDiagnostics,
|
||||
Containers::String& outErrorMessage,
|
||||
std::unordered_set<std::string>& seenDependencies) {
|
||||
const fs::path sourceDirectory = sourcePath.parent_path();
|
||||
|
||||
for (const UIDocumentAttribute& attribute : node.attributes) {
|
||||
if (!LooksLikeUIDocumentReference(attribute.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Containers::String trimmedValue = attribute.value.Trim();
|
||||
fs::path dependencyPath(trimmedValue.CStr());
|
||||
if (!dependencyPath.is_absolute()) {
|
||||
dependencyPath = sourceDirectory / dependencyPath;
|
||||
}
|
||||
|
||||
dependencyPath = dependencyPath.lexically_normal();
|
||||
if (!fs::exists(dependencyPath) || fs::is_directory(dependencyPath)) {
|
||||
UIDocumentDiagnostic diagnostic;
|
||||
diagnostic.severity = UIDocumentDiagnosticSeverity::Error;
|
||||
diagnostic.location = node.location;
|
||||
diagnostic.message =
|
||||
Containers::String("Referenced UI document was not found: ") + trimmedValue;
|
||||
inOutDiagnostics.PushBack(diagnostic);
|
||||
outErrorMessage = FormatDiagnosticMessage(
|
||||
NormalizePathString(sourcePath),
|
||||
diagnostic.location,
|
||||
diagnostic.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
AppendUniqueDependency(
|
||||
NormalizePathString(dependencyPath),
|
||||
seenDependencies,
|
||||
outDependencies);
|
||||
}
|
||||
|
||||
for (const UIDocumentNode& child : node.children) {
|
||||
if (!CollectUIDocumentDependencies(
|
||||
sourcePath,
|
||||
child,
|
||||
outDependencies,
|
||||
inOutDiagnostics,
|
||||
outErrorMessage,
|
||||
seenDependencies)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct SourceFileReadResult {
|
||||
bool succeeded = false;
|
||||
fs::path resolvedPath;
|
||||
std::string content;
|
||||
Containers::String errorMessage;
|
||||
};
|
||||
|
||||
bool ReadUIDocumentSourceFile(const Containers::String& path, SourceFileReadResult& outResult) {
|
||||
outResult = SourceFileReadResult();
|
||||
if (path.Empty()) {
|
||||
outResult.errorMessage = "UI document path is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto tryReadFile = [&](const fs::path& filePath) -> bool {
|
||||
std::ifstream input(filePath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
outResult.succeeded = static_cast<bool>(input) || input.eof();
|
||||
outResult.resolvedPath = filePath.lexically_normal();
|
||||
outResult.content = buffer.str();
|
||||
return outResult.succeeded;
|
||||
};
|
||||
|
||||
const fs::path requestedPath(path.CStr());
|
||||
if (tryReadFile(requestedPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!requestedPath.is_absolute()) {
|
||||
const Containers::String resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
if (!resourceRoot.Empty() &&
|
||||
tryReadFile(fs::path(resourceRoot.CStr()) / requestedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
outResult.errorMessage = Containers::String("Unable to read UI document source file: ") + path;
|
||||
return false;
|
||||
}
|
||||
|
||||
class UIDocumentParser {
|
||||
public:
|
||||
UIDocumentParser(const std::string& source,
|
||||
const fs::path& resolvedPath,
|
||||
const Containers::String& expectedRootTag,
|
||||
UIDocumentKind kind)
|
||||
: m_source(source)
|
||||
, m_resolvedPath(resolvedPath)
|
||||
, m_expectedRootTag(expectedRootTag)
|
||||
, m_kind(kind) {
|
||||
}
|
||||
|
||||
bool Parse(UIDocumentCompileResult& outResult) {
|
||||
outResult = UIDocumentCompileResult();
|
||||
outResult.document.kind = m_kind;
|
||||
outResult.document.sourcePath = NormalizePathString(m_resolvedPath);
|
||||
|
||||
if (HasUtf8Bom()) {
|
||||
Advance();
|
||||
Advance();
|
||||
Advance();
|
||||
}
|
||||
|
||||
SkipWhitespaceAndTrivia();
|
||||
if (AtEnd()) {
|
||||
AddError(CurrentLocation(), "UI document is empty.");
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
if (Peek() != '<') {
|
||||
AddError(CurrentLocation(), "Expected '<' at the beginning of the UI document.");
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
UIDocumentNode rootNode;
|
||||
if (!ParseElement(rootNode)) {
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
SkipWhitespaceAndTrivia();
|
||||
if (!AtEnd()) {
|
||||
AddError(CurrentLocation(), "Unexpected trailing content after the root element.");
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
if (!m_expectedRootTag.Empty() &&
|
||||
rootNode.tagName != m_expectedRootTag) {
|
||||
AddError(
|
||||
rootNode.location,
|
||||
Containers::String("Expected root element <") +
|
||||
m_expectedRootTag +
|
||||
">, found <" +
|
||||
rootNode.tagName +
|
||||
">.");
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
outResult.document.rootNode = std::move(rootNode);
|
||||
if (const UIDocumentAttribute* nameAttribute = outResult.document.rootNode.FindAttribute("name");
|
||||
nameAttribute != nullptr && !nameAttribute->value.Empty()) {
|
||||
outResult.document.displayName = nameAttribute->value;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> seenDependencies;
|
||||
if (!CollectUIDocumentDependencies(
|
||||
m_resolvedPath,
|
||||
outResult.document.rootNode,
|
||||
outResult.document.dependencies,
|
||||
outResult.document.diagnostics,
|
||||
m_errorMessage,
|
||||
seenDependencies)) {
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
outResult.document.valid = true;
|
||||
outResult.succeeded = true;
|
||||
return Finalize(outResult, true);
|
||||
}
|
||||
|
||||
private:
|
||||
bool ParseElement(UIDocumentNode& outNode) {
|
||||
const UIDocumentSourceLocation elementLocation = CurrentLocation();
|
||||
if (!Consume('<', "Expected '<' to start an element.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(elementLocation, "Unexpected end of file after '<'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Peek() == '/' || Peek() == '!' || Peek() == '?') {
|
||||
AddError(elementLocation, "Unexpected token while parsing an element.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Containers::String tagName;
|
||||
if (!ParseName(tagName, "Expected an element tag name.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outNode.location = elementLocation;
|
||||
outNode.tagName = tagName;
|
||||
|
||||
while (!AtEnd()) {
|
||||
SkipWhitespace();
|
||||
if (StartsWith("/>")) {
|
||||
Advance();
|
||||
Advance();
|
||||
outNode.selfClosing = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Peek() == '>') {
|
||||
Advance();
|
||||
break;
|
||||
}
|
||||
|
||||
UIDocumentAttribute attribute;
|
||||
if (!ParseAttribute(attribute)) {
|
||||
return false;
|
||||
}
|
||||
outNode.attributes.PushBack(std::move(attribute));
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(elementLocation, "Unexpected end of file before element start tag was closed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
SkipWhitespaceAndTrivia();
|
||||
if (AtEnd()) {
|
||||
AddError(
|
||||
elementLocation,
|
||||
Containers::String("Unexpected end of file while parsing <") +
|
||||
outNode.tagName +
|
||||
">.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StartsWith("</")) {
|
||||
Advance();
|
||||
Advance();
|
||||
|
||||
Containers::String closeName;
|
||||
if (!ParseName(closeName, "Expected a closing element tag name.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SkipWhitespace();
|
||||
if (!Consume('>', "Expected '>' to finish the closing tag.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (closeName != outNode.tagName) {
|
||||
AddError(
|
||||
CurrentLocation(),
|
||||
Containers::String("Closing tag </") +
|
||||
closeName +
|
||||
"> does not match <" +
|
||||
outNode.tagName +
|
||||
">.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Peek() != '<') {
|
||||
AddError(CurrentLocation(), "Text nodes are not supported in XCUI documents.");
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentNode childNode;
|
||||
if (!ParseElement(childNode)) {
|
||||
return false;
|
||||
}
|
||||
outNode.children.PushBack(std::move(childNode));
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseAttribute(UIDocumentAttribute& outAttribute) {
|
||||
if (!ParseName(outAttribute.name, "Expected an attribute name.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SkipWhitespace();
|
||||
if (!Consume('=', "Expected '=' after the attribute name.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SkipWhitespace();
|
||||
return ParseQuotedString(outAttribute.value);
|
||||
}
|
||||
|
||||
bool ParseQuotedString(Containers::String& outValue) {
|
||||
if (AtEnd()) {
|
||||
AddError(CurrentLocation(), "Expected a quoted attribute value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const char quote = Peek();
|
||||
if (quote != '"' && quote != '\'') {
|
||||
AddError(CurrentLocation(), "Attribute values must use single or double quotes.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
std::string value;
|
||||
while (!AtEnd() && Peek() != quote) {
|
||||
value.push_back(Peek());
|
||||
Advance();
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(CurrentLocation(), "Unterminated attribute value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
outValue = ToContainersString(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseName(Containers::String& outName, const char* errorMessage) {
|
||||
if (AtEnd() || !IsNameStartChar(Peek())) {
|
||||
AddError(CurrentLocation(), errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
name.push_back(Peek());
|
||||
Advance();
|
||||
while (!AtEnd() && IsNameChar(Peek())) {
|
||||
name.push_back(Peek());
|
||||
Advance();
|
||||
}
|
||||
|
||||
outName = ToContainersString(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkipWhitespaceAndTrivia() {
|
||||
while (!AtEnd()) {
|
||||
SkipWhitespace();
|
||||
if (StartsWith("<!--")) {
|
||||
if (!SkipComment()) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (StartsWith("<?")) {
|
||||
if (!SkipProcessingInstruction()) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SkipWhitespace() {
|
||||
while (!AtEnd() &&
|
||||
std::isspace(static_cast<unsigned char>(Peek())) != 0) {
|
||||
Advance();
|
||||
}
|
||||
}
|
||||
|
||||
bool SkipComment() {
|
||||
const UIDocumentSourceLocation location = CurrentLocation();
|
||||
Advance();
|
||||
Advance();
|
||||
Advance();
|
||||
Advance();
|
||||
|
||||
while (!AtEnd() && !StartsWith("-->")) {
|
||||
Advance();
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(location, "Unterminated XML comment.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
Advance();
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SkipProcessingInstruction() {
|
||||
const UIDocumentSourceLocation location = CurrentLocation();
|
||||
Advance();
|
||||
Advance();
|
||||
|
||||
while (!AtEnd() && !StartsWith("?>")) {
|
||||
Advance();
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(location, "Unterminated processing instruction.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Consume(char expected, const char* errorMessage) {
|
||||
if (AtEnd() || Peek() != expected) {
|
||||
AddError(CurrentLocation(), errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AddError(const UIDocumentSourceLocation& location, const Containers::String& message) {
|
||||
UIDocumentDiagnostic diagnostic;
|
||||
diagnostic.severity = UIDocumentDiagnosticSeverity::Error;
|
||||
diagnostic.location = location;
|
||||
diagnostic.message = message;
|
||||
m_diagnostics.PushBack(diagnostic);
|
||||
|
||||
if (m_errorMessage.Empty()) {
|
||||
m_errorMessage = FormatDiagnosticMessage(NormalizePathString(m_resolvedPath), location, message);
|
||||
}
|
||||
}
|
||||
|
||||
bool Finalize(UIDocumentCompileResult& outResult, bool succeeded) {
|
||||
Containers::Array<UIDocumentDiagnostic> combinedDiagnostics;
|
||||
combinedDiagnostics.Reserve(
|
||||
m_diagnostics.Size() +
|
||||
outResult.document.diagnostics.Size());
|
||||
for (const UIDocumentDiagnostic& diagnostic : m_diagnostics) {
|
||||
combinedDiagnostics.PushBack(diagnostic);
|
||||
}
|
||||
for (const UIDocumentDiagnostic& diagnostic : outResult.document.diagnostics) {
|
||||
combinedDiagnostics.PushBack(diagnostic);
|
||||
}
|
||||
outResult.document.diagnostics = std::move(combinedDiagnostics);
|
||||
outResult.succeeded = succeeded;
|
||||
outResult.errorMessage = succeeded ? Containers::String() : m_errorMessage;
|
||||
if (!succeeded) {
|
||||
outResult.document.valid = false;
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool HasUtf8Bom() const {
|
||||
return m_source.size() >= 3 &&
|
||||
static_cast<unsigned char>(m_source[0]) == 0xEF &&
|
||||
static_cast<unsigned char>(m_source[1]) == 0xBB &&
|
||||
static_cast<unsigned char>(m_source[2]) == 0xBF;
|
||||
}
|
||||
|
||||
bool StartsWith(const char* text) const {
|
||||
const size_t length = std::strlen(text);
|
||||
return m_offset + length <= m_source.size() &&
|
||||
m_source.compare(m_offset, length, text) == 0;
|
||||
}
|
||||
|
||||
bool AtEnd() const {
|
||||
return m_offset >= m_source.size();
|
||||
}
|
||||
|
||||
char Peek() const {
|
||||
return AtEnd() ? '\0' : m_source[m_offset];
|
||||
}
|
||||
|
||||
void Advance() {
|
||||
if (AtEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_source[m_offset] == '\n') {
|
||||
++m_line;
|
||||
m_column = 1;
|
||||
} else {
|
||||
++m_column;
|
||||
}
|
||||
++m_offset;
|
||||
}
|
||||
|
||||
UIDocumentSourceLocation CurrentLocation() const {
|
||||
UIDocumentSourceLocation location;
|
||||
location.line = m_line;
|
||||
location.column = m_column;
|
||||
return location;
|
||||
}
|
||||
|
||||
const std::string& m_source;
|
||||
fs::path m_resolvedPath;
|
||||
Containers::String m_expectedRootTag;
|
||||
UIDocumentKind m_kind = UIDocumentKind::View;
|
||||
size_t m_offset = 0;
|
||||
Core::uint32 m_line = 1;
|
||||
Core::uint32 m_column = 1;
|
||||
Containers::Array<UIDocumentDiagnostic> m_diagnostics;
|
||||
Containers::String m_errorMessage;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CompileUIDocument(const UIDocumentCompileRequest& request,
|
||||
UIDocumentCompileResult& outResult) {
|
||||
outResult = UIDocumentCompileResult();
|
||||
|
||||
SourceFileReadResult readResult;
|
||||
if (!ReadUIDocumentSourceFile(request.path, readResult)) {
|
||||
outResult.errorMessage = readResult.errorMessage;
|
||||
outResult.succeeded = false;
|
||||
outResult.document.kind = request.kind;
|
||||
return false;
|
||||
}
|
||||
|
||||
const Containers::String expectedRootTag =
|
||||
request.expectedRootTag.Empty()
|
||||
? GetUIDocumentDefaultRootTag(request.kind)
|
||||
: request.expectedRootTag;
|
||||
|
||||
UIDocumentParser parser(
|
||||
readResult.content,
|
||||
readResult.resolvedPath,
|
||||
expectedRootTag,
|
||||
request.kind);
|
||||
return parser.Parse(outResult);
|
||||
}
|
||||
|
||||
bool WriteUIDocumentArtifact(const Containers::String& artifactPath,
|
||||
const UIDocumentCompileResult& compileResult,
|
||||
Containers::String* outErrorMessage) {
|
||||
if (!compileResult.succeeded || !compileResult.document.valid) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = "UI document compile result is invalid.";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream output(artifactPath.CStr(), std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Unable to write UI document artifact: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentArtifactFileHeader header;
|
||||
header.kind = static_cast<Core::uint32>(compileResult.document.kind);
|
||||
header.dependencyCount = static_cast<Core::uint32>(compileResult.document.dependencies.Size());
|
||||
header.diagnosticCount = static_cast<Core::uint32>(compileResult.document.diagnostics.Size());
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
if (!output) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write UI document artifact header: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteString(output, compileResult.document.sourcePath);
|
||||
WriteString(output, compileResult.document.displayName);
|
||||
if (!WriteNode(output, compileResult.document.rootNode)) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write UI document node tree: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const Containers::String& dependency : compileResult.document.dependencies) {
|
||||
WriteString(output, dependency);
|
||||
if (!output) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write UI document dependencies: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const UIDocumentDiagnostic& diagnostic : compileResult.document.diagnostics) {
|
||||
UIDocumentArtifactDiagnosticHeader diagnosticHeader;
|
||||
diagnosticHeader.severity = static_cast<Core::uint32>(diagnostic.severity);
|
||||
diagnosticHeader.line = diagnostic.location.line;
|
||||
diagnosticHeader.column = diagnostic.location.column;
|
||||
output.write(reinterpret_cast<const char*>(&diagnosticHeader), sizeof(diagnosticHeader));
|
||||
WriteString(output, diagnostic.message);
|
||||
if (!output) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write UI document diagnostics: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadUIDocumentArtifact(const Containers::String& artifactPath,
|
||||
UIDocumentKind expectedKind,
|
||||
UIDocumentCompileResult& outResult) {
|
||||
outResult = UIDocumentCompileResult();
|
||||
|
||||
std::ifstream input(artifactPath.CStr(), std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
outResult.errorMessage = Containers::String("Unable to open UI document artifact: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentArtifactFileHeader header;
|
||||
input.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!input) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact header: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIDocumentArtifactFileHeader expectedHeader;
|
||||
if (std::memcmp(header.magic, expectedHeader.magic, sizeof(header.magic)) != 0) {
|
||||
outResult.errorMessage = Containers::String("Invalid UI document artifact magic: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header.schemaVersion != kUIDocumentArtifactSchemaVersion) {
|
||||
outResult.errorMessage = Containers::String("Unsupported UI document artifact schema version: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header.kind != static_cast<Core::uint32>(expectedKind)) {
|
||||
outResult.errorMessage = Containers::String("UI document artifact kind mismatch: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
outResult.document.kind = expectedKind;
|
||||
if (!ReadString(input, outResult.document.sourcePath) ||
|
||||
!ReadString(input, outResult.document.displayName) ||
|
||||
!ReadNode(input, outResult.document.rootNode)) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact body: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
outResult.document.dependencies.Clear();
|
||||
outResult.document.dependencies.Reserve(header.dependencyCount);
|
||||
for (Core::uint32 index = 0; index < header.dependencyCount; ++index) {
|
||||
Containers::String dependency;
|
||||
if (!ReadString(input, dependency)) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact dependencies: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
outResult.document.dependencies.PushBack(std::move(dependency));
|
||||
}
|
||||
|
||||
outResult.document.diagnostics.Clear();
|
||||
outResult.document.diagnostics.Reserve(header.diagnosticCount);
|
||||
for (Core::uint32 index = 0; index < header.diagnosticCount; ++index) {
|
||||
UIDocumentArtifactDiagnosticHeader diagnosticHeader;
|
||||
input.read(reinterpret_cast<char*>(&diagnosticHeader), sizeof(diagnosticHeader));
|
||||
if (!input) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact diagnostic header: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentDiagnostic diagnostic;
|
||||
diagnostic.severity = static_cast<UIDocumentDiagnosticSeverity>(diagnosticHeader.severity);
|
||||
diagnostic.location.line = diagnosticHeader.line;
|
||||
diagnostic.location.column = diagnosticHeader.column;
|
||||
if (!ReadString(input, diagnostic.message)) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact diagnostic message: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
outResult.document.diagnostics.PushBack(std::move(diagnostic));
|
||||
}
|
||||
|
||||
outResult.document.valid = true;
|
||||
outResult.succeeded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Containers::String GetUIDocumentDefaultRootTag(UIDocumentKind kind) {
|
||||
switch (kind) {
|
||||
case UIDocumentKind::View:
|
||||
return "View";
|
||||
case UIDocumentKind::Theme:
|
||||
return "Theme";
|
||||
case UIDocumentKind::Schema:
|
||||
return "Schema";
|
||||
default:
|
||||
return "View";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
61
engine/src/Resources/UI/UIDocuments.cpp
Normal file
61
engine/src/Resources/UI/UIDocuments.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include <XCEngine/Resources/UI/UIDocuments.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
size_t MeasureNodeMemorySize(const UIDocumentNode& node) {
|
||||
size_t size = sizeof(UIDocumentNode) + node.tagName.Length();
|
||||
for (const UIDocumentAttribute& attribute : node.attributes) {
|
||||
size += sizeof(UIDocumentAttribute);
|
||||
size += attribute.name.Length();
|
||||
size += attribute.value.Length();
|
||||
}
|
||||
|
||||
for (const UIDocumentNode& child : node.children) {
|
||||
size += MeasureNodeMemorySize(child);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t MeasureDiagnosticMemorySize(const UIDocumentDiagnostic& diagnostic) {
|
||||
return sizeof(UIDocumentDiagnostic) + diagnostic.message.Length();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void UIDocumentResource::Release() {
|
||||
m_document.Clear();
|
||||
SetInvalid();
|
||||
m_memorySize = 0;
|
||||
}
|
||||
|
||||
void UIDocumentResource::SetDocumentModel(const UIDocumentModel& document) {
|
||||
m_document = document;
|
||||
RecalculateMemorySize();
|
||||
}
|
||||
|
||||
void UIDocumentResource::SetDocumentModel(UIDocumentModel&& document) {
|
||||
m_document = std::move(document);
|
||||
RecalculateMemorySize();
|
||||
}
|
||||
|
||||
void UIDocumentResource::RecalculateMemorySize() {
|
||||
size_t size = sizeof(*this);
|
||||
size += m_document.sourcePath.Length();
|
||||
size += m_document.displayName.Length();
|
||||
size += MeasureNodeMemorySize(m_document.rootNode);
|
||||
for (const Containers::String& dependency : m_document.dependencies) {
|
||||
size += sizeof(Containers::String) + dependency.Length();
|
||||
}
|
||||
for (const UIDocumentDiagnostic& diagnostic : m_document.diagnostics) {
|
||||
size += MeasureDiagnosticMemorySize(diagnostic);
|
||||
}
|
||||
|
||||
m_memorySize = size;
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -9,6 +9,26 @@ using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
struct TrackingMaterial final : Material {
|
||||
static int s_destructorCount;
|
||||
|
||||
~TrackingMaterial() override {
|
||||
++s_destructorCount;
|
||||
}
|
||||
};
|
||||
|
||||
int TrackingMaterial::s_destructorCount = 0;
|
||||
|
||||
struct TrackingTexture final : Texture {
|
||||
static int s_destructorCount;
|
||||
|
||||
~TrackingTexture() override {
|
||||
++s_destructorCount;
|
||||
}
|
||||
};
|
||||
|
||||
int TrackingTexture::s_destructorCount = 0;
|
||||
|
||||
TEST(Mesh, DefaultConstructor) {
|
||||
Mesh mesh;
|
||||
EXPECT_EQ(mesh.GetVertexCount(), 0u);
|
||||
@@ -88,6 +108,27 @@ TEST(Mesh, AddMaterial) {
|
||||
EXPECT_GT(mesh.GetMemorySize(), 0u);
|
||||
}
|
||||
|
||||
TEST(Mesh, ReleaseDeletesOwnedSubresourcesOnce) {
|
||||
TrackingMaterial::s_destructorCount = 0;
|
||||
TrackingTexture::s_destructorCount = 0;
|
||||
|
||||
auto* mesh = new Mesh();
|
||||
mesh->AddMaterial(new TrackingMaterial());
|
||||
mesh->AddTexture(new TrackingTexture());
|
||||
|
||||
mesh->Release();
|
||||
|
||||
EXPECT_EQ(TrackingMaterial::s_destructorCount, 1);
|
||||
EXPECT_EQ(TrackingTexture::s_destructorCount, 1);
|
||||
EXPECT_EQ(mesh->GetMaterials().Size(), 0u);
|
||||
EXPECT_EQ(mesh->GetTextures().Size(), 0u);
|
||||
|
||||
delete mesh;
|
||||
|
||||
EXPECT_EQ(TrackingMaterial::s_destructorCount, 1);
|
||||
EXPECT_EQ(TrackingTexture::s_destructorCount, 1);
|
||||
}
|
||||
|
||||
TEST(Mesh, SetBounds) {
|
||||
Mesh mesh;
|
||||
|
||||
|
||||
@@ -169,4 +169,58 @@ TEST(TextureLoader, ResourceManagerLoadsTextureByAssetRefFromProjectAssets) {
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(TextureLoader, ResourceManagerLoadsLibraryArtifactTextureWithoutReimportingIt) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_texture_library_artifact_direct_load_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path texturePath = assetsDir / "checker.bmp";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
fs::copy_file(GetTextureFixturePath("checker.bmp"), texturePath, fs::copy_options::overwrite_existing);
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
const AssetImportService::ImportStatusSnapshot bootstrapStatus = manager.GetProjectAssetImportStatus();
|
||||
EXPECT_TRUE(bootstrapStatus.HasValue());
|
||||
EXPECT_TRUE(bootstrapStatus.success);
|
||||
EXPECT_EQ(std::string(bootstrapStatus.operation.CStr()), "Bootstrap Project");
|
||||
EXPECT_GT(bootstrapStatus.startedAtMs, 0u);
|
||||
EXPECT_GE(bootstrapStatus.completedAtMs, bootstrapStatus.startedAtMs);
|
||||
EXPECT_EQ(bootstrapStatus.durationMs, bootstrapStatus.completedAtMs - bootstrapStatus.startedAtMs);
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/checker.bmp", ResourceType::Texture, resolvedAsset));
|
||||
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||
|
||||
std::error_code ec;
|
||||
const fs::path relativeArtifactPath =
|
||||
fs::relative(fs::path(resolvedAsset.artifactMainPath.CStr()), projectRoot, ec);
|
||||
ASSERT_FALSE(ec);
|
||||
ASSERT_FALSE(relativeArtifactPath.empty());
|
||||
|
||||
const auto textureHandle = manager.Load<Texture>(relativeArtifactPath.generic_string().c_str());
|
||||
ASSERT_TRUE(textureHandle.IsValid());
|
||||
EXPECT_EQ(textureHandle->GetWidth(), 2u);
|
||||
EXPECT_EQ(textureHandle->GetHeight(), 2u);
|
||||
EXPECT_EQ(textureHandle->GetPath(), relativeArtifactPath.generic_string().c_str());
|
||||
const AssetImportService::ImportStatusSnapshot postLoadStatus = manager.GetProjectAssetImportStatus();
|
||||
EXPECT_TRUE(postLoadStatus.HasValue());
|
||||
EXPECT_EQ(std::string(postLoadStatus.operation.CStr()), "Bootstrap Project");
|
||||
EXPECT_GT(postLoadStatus.startedAtMs, 0u);
|
||||
EXPECT_GE(postLoadStatus.completedAtMs, postLoadStatus.startedAtMs);
|
||||
EXPECT_EQ(postLoadStatus.durationMs, postLoadStatus.completedAtMs - postLoadStatus.startedAtMs);
|
||||
|
||||
database.Shutdown();
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
30
tests/Resources/UI/CMakeLists.txt
Normal file
30
tests/Resources/UI/CMakeLists.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
# ============================================================
|
||||
# UI Resource Tests
|
||||
# ============================================================
|
||||
|
||||
set(UI_RESOURCE_TEST_SOURCES
|
||||
test_ui_document_loader.cpp
|
||||
)
|
||||
|
||||
add_executable(ui_resource_tests ${UI_RESOURCE_TEST_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(ui_resource_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(ui_resource_tests
|
||||
PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(ui_resource_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/tests/Fixtures
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(ui_resource_tests)
|
||||
215
tests/Resources/UI/test_ui_document_loader.cpp
Normal file
215
tests/Resources/UI/test_ui_document_loader.cpp
Normal file
@@ -0,0 +1,215 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/AssetImportService.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
|
||||
#include <XCEngine/Resources/UI/UIDocuments.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
|
||||
namespace {
|
||||
|
||||
void WriteTextFile(const std::filesystem::path& path, const std::string& contents) {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
output << contents;
|
||||
ASSERT_TRUE(static_cast<bool>(output));
|
||||
}
|
||||
|
||||
bool ContainsExtension(const XCEngine::Containers::Array<XCEngine::Containers::String>& values,
|
||||
const char* expectedValue) {
|
||||
for (const auto& value : values) {
|
||||
if (value == expectedValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ContainsDependencyFile(const XCEngine::Containers::Array<XCEngine::Containers::String>& dependencies,
|
||||
const char* expectedFileName) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
for (const auto& dependency : dependencies) {
|
||||
if (fs::path(dependency.CStr()).filename().string() == expectedFileName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TEST(UIDocumentLoader, LoadersExposeExpectedTypesAndExtensions) {
|
||||
UIViewLoader viewLoader;
|
||||
UIThemeLoader themeLoader;
|
||||
UISchemaLoader schemaLoader;
|
||||
|
||||
EXPECT_EQ(viewLoader.GetResourceType(), ResourceType::UIView);
|
||||
EXPECT_EQ(themeLoader.GetResourceType(), ResourceType::UITheme);
|
||||
EXPECT_EQ(schemaLoader.GetResourceType(), ResourceType::UISchema);
|
||||
|
||||
const auto viewExtensions = viewLoader.GetSupportedExtensions();
|
||||
EXPECT_TRUE(ContainsExtension(viewExtensions, "xcui"));
|
||||
EXPECT_TRUE(ContainsExtension(viewExtensions, "xcuiasset"));
|
||||
EXPECT_TRUE(viewLoader.CanLoad("panel.xcui"));
|
||||
EXPECT_TRUE(viewLoader.CanLoad("panel.xcuiasset"));
|
||||
EXPECT_FALSE(viewLoader.CanLoad("panel.txt"));
|
||||
|
||||
const auto themeExtensions = themeLoader.GetSupportedExtensions();
|
||||
EXPECT_TRUE(ContainsExtension(themeExtensions, "xctheme"));
|
||||
EXPECT_TRUE(ContainsExtension(themeExtensions, "xcthemeasset"));
|
||||
EXPECT_TRUE(themeLoader.CanLoad("editor.xctheme"));
|
||||
EXPECT_TRUE(themeLoader.CanLoad("editor.xcthemeasset"));
|
||||
|
||||
const auto schemaExtensions = schemaLoader.GetSupportedExtensions();
|
||||
EXPECT_TRUE(ContainsExtension(schemaExtensions, "xcschema"));
|
||||
EXPECT_TRUE(ContainsExtension(schemaExtensions, "xcschemaasset"));
|
||||
EXPECT_TRUE(schemaLoader.CanLoad("entity.xcschema"));
|
||||
EXPECT_TRUE(schemaLoader.CanLoad("entity.xcschemaasset"));
|
||||
}
|
||||
|
||||
TEST(UIDocumentLoader, CompileAndLoadViewTracksDependencies) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path root = fs::temp_directory_path() / "xc_ui_document_compile_test";
|
||||
fs::remove_all(root);
|
||||
|
||||
WriteTextFile(root / "shared" / "toolbar.xcui", "<View name=\"Toolbar\" />\n");
|
||||
WriteTextFile(root / "themes" / "editor.xctheme", "<Theme name=\"EditorTheme\" />\n");
|
||||
WriteTextFile(root / "schemas" / "entity.xcschema", "<Schema name=\"EntitySchema\" />\n");
|
||||
WriteTextFile(
|
||||
root / "main.xcui",
|
||||
"<!-- root comment -->\n"
|
||||
"<View name=\"MainPanel\" theme=\"themes/editor.xctheme\">\n"
|
||||
" <Column>\n"
|
||||
" <Use view=\"shared/toolbar.xcui\" />\n"
|
||||
" <AutoForm schema=\"schemas/entity.xcschema\" />\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
|
||||
UIViewLoader loader;
|
||||
UIDocumentCompileResult compileResult = {};
|
||||
ASSERT_TRUE(loader.CompileDocument((root / "main.xcui").string().c_str(), compileResult));
|
||||
ASSERT_TRUE(compileResult.succeeded);
|
||||
ASSERT_TRUE(compileResult.document.valid);
|
||||
EXPECT_EQ(compileResult.document.rootNode.tagName, "View");
|
||||
EXPECT_EQ(compileResult.document.rootNode.children.Size(), 1u);
|
||||
EXPECT_EQ(compileResult.document.rootNode.children[0].tagName, "Column");
|
||||
EXPECT_EQ(compileResult.document.dependencies.Size(), 3u);
|
||||
EXPECT_TRUE(ContainsDependencyFile(compileResult.document.dependencies, "toolbar.xcui"));
|
||||
EXPECT_TRUE(ContainsDependencyFile(compileResult.document.dependencies, "editor.xctheme"));
|
||||
EXPECT_TRUE(ContainsDependencyFile(compileResult.document.dependencies, "entity.xcschema"));
|
||||
|
||||
LoadResult loadResult = loader.Load((root / "main.xcui").string().c_str());
|
||||
ASSERT_TRUE(loadResult);
|
||||
ASSERT_NE(loadResult.resource, nullptr);
|
||||
|
||||
auto* view = static_cast<UIView*>(loadResult.resource);
|
||||
ASSERT_NE(view, nullptr);
|
||||
EXPECT_TRUE(view->IsValid());
|
||||
EXPECT_EQ(view->GetName(), "MainPanel");
|
||||
EXPECT_EQ(view->GetRootNode().tagName, "View");
|
||||
EXPECT_EQ(view->GetDependencies().Size(), 3u);
|
||||
delete view;
|
||||
|
||||
fs::remove_all(root);
|
||||
}
|
||||
|
||||
TEST(UIDocumentLoader, AssetDatabaseImportsViewArtifactAndReimportsWhenDependencyChanges) {
|
||||
namespace fs = std::filesystem;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_ui_artifact_reimport_test";
|
||||
const fs::path assetsRoot = projectRoot / "Assets";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
|
||||
WriteTextFile(assetsRoot / "UI" / "Shared" / "toolbar.xcui", "<View name=\"Toolbar\" />\n");
|
||||
WriteTextFile(
|
||||
assetsRoot / "UI" / "Main.xcui",
|
||||
"<View name=\"Inspector\">\n"
|
||||
" <Use view=\"Shared/toolbar.xcui\" />\n"
|
||||
"</View>\n");
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset firstResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/UI/Main.xcui", ResourceType::UIView, firstResolve));
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().string(), ".xcuiasset");
|
||||
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
||||
|
||||
UIViewLoader loader;
|
||||
LoadResult firstLoad = loader.Load(firstResolve.artifactMainPath.CStr());
|
||||
ASSERT_TRUE(firstLoad);
|
||||
auto* firstView = static_cast<UIView*>(firstLoad.resource);
|
||||
ASSERT_NE(firstView, nullptr);
|
||||
EXPECT_EQ(firstView->GetRootNode().tagName, "View");
|
||||
EXPECT_EQ(firstView->GetDependencies().Size(), 1u);
|
||||
EXPECT_TRUE(ContainsDependencyFile(firstView->GetDependencies(), "toolbar.xcui"));
|
||||
delete firstView;
|
||||
|
||||
const XCEngine::Containers::String firstArtifactPath = firstResolve.artifactMainPath;
|
||||
database.Shutdown();
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
WriteTextFile(
|
||||
assetsRoot / "UI" / "Shared" / "toolbar.xcui",
|
||||
"<View name=\"Toolbar\">\n"
|
||||
" <Button id=\"refresh\" />\n"
|
||||
"</View>\n");
|
||||
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
AssetDatabase::ResolvedAsset secondResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/UI/Main.xcui", ResourceType::UIView, secondResolve));
|
||||
ASSERT_TRUE(secondResolve.artifactReady);
|
||||
EXPECT_NE(firstArtifactPath, secondResolve.artifactMainPath);
|
||||
EXPECT_TRUE(fs::exists(secondResolve.artifactMainPath.CStr()));
|
||||
database.Shutdown();
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(UIDocumentLoader, AssetImportServiceReportsDetailedDiagnosticsForMissingDependency) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_ui_import_error_test";
|
||||
const fs::path assetsRoot = projectRoot / "Assets";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
WriteTextFile(
|
||||
assetsRoot / "UI" / "Broken.xcui",
|
||||
"<View>\n"
|
||||
" <Use view=\"Shared/missing_toolbar.xcui\" />\n"
|
||||
"</View>\n");
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
importService.SetProjectRoot(projectRoot.string().c_str());
|
||||
|
||||
AssetImportService::ImportedAsset importedAsset;
|
||||
EXPECT_FALSE(importService.EnsureArtifact("Assets/UI/Broken.xcui", ResourceType::UIView, importedAsset));
|
||||
|
||||
const AssetImportService::ImportStatusSnapshot status = importService.GetLastImportStatus();
|
||||
EXPECT_TRUE(status.HasValue());
|
||||
EXPECT_FALSE(status.success);
|
||||
|
||||
const std::string message = status.message.CStr();
|
||||
EXPECT_NE(message.find("Failed to build asset artifact: Assets/UI/Broken.xcui"), std::string::npos);
|
||||
EXPECT_NE(message.find("Referenced UI document was not found"), std::string::npos);
|
||||
EXPECT_NE(message.find("missing_toolbar.xcui"), std::string::npos);
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -565,12 +565,12 @@ TEST_F(SceneTest, Save_ContainsHierarchyAndComponentEntries) {
|
||||
std::filesystem::remove(scenePath);
|
||||
}
|
||||
|
||||
TEST_F(SceneTest, SaveAndLoad_PreservesMeshComponentPaths) {
|
||||
TEST_F(SceneTest, SaveAndLoad_PreservesBuiltinMeshComponentPaths) {
|
||||
GameObject* meshObject = testScene->CreateGameObject("Backpack");
|
||||
auto* meshFilter = meshObject->AddComponent<MeshFilterComponent>();
|
||||
auto* meshRenderer = meshObject->AddComponent<MeshRendererComponent>();
|
||||
meshFilter->SetMeshPath("Assets/Models/backpack/backpack.obj");
|
||||
meshRenderer->SetMaterialPath(0, "Assets/Materials/backpack.mat");
|
||||
meshFilter->SetMeshPath("builtin://meshes/cube");
|
||||
meshRenderer->SetMaterialPath(0, "builtin://materials/default-primitive");
|
||||
meshRenderer->SetCastShadows(false);
|
||||
meshRenderer->SetReceiveShadows(true);
|
||||
meshRenderer->SetRenderLayer(4);
|
||||
@@ -589,9 +589,9 @@ TEST_F(SceneTest, SaveAndLoad_PreservesMeshComponentPaths) {
|
||||
ASSERT_NE(loadedMeshFilter, nullptr);
|
||||
ASSERT_NE(loadedMeshRenderer, nullptr);
|
||||
|
||||
EXPECT_EQ(loadedMeshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj");
|
||||
EXPECT_EQ(loadedMeshFilter->GetMeshPath(), "builtin://meshes/cube");
|
||||
ASSERT_EQ(loadedMeshRenderer->GetMaterialCount(), 1u);
|
||||
EXPECT_EQ(loadedMeshRenderer->GetMaterialPath(0), "Assets/Materials/backpack.mat");
|
||||
EXPECT_EQ(loadedMeshRenderer->GetMaterialPath(0), "builtin://materials/default-primitive");
|
||||
EXPECT_FALSE(loadedMeshRenderer->GetCastShadows());
|
||||
EXPECT_TRUE(loadedMeshRenderer->GetReceiveShadows());
|
||||
EXPECT_EQ(loadedMeshRenderer->GetRenderLayer(), 4u);
|
||||
@@ -699,7 +699,7 @@ TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) {
|
||||
|
||||
EXPECT_NE(loadedScene.Find("Camera"), nullptr);
|
||||
EXPECT_NE(loadedScene.Find("Light"), nullptr);
|
||||
EXPECT_NE(loadedScene.Find("Cube"), nullptr);
|
||||
EXPECT_NE(loadedScene.Find("Sphere"), nullptr);
|
||||
EXPECT_EQ(loadedScene.Find("BackpackMesh"), nullptr);
|
||||
EXPECT_EQ(FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj").size(), 0u);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
@@ -107,6 +108,45 @@ bool PumpAsyncLoadsUntil(ResourceManager& manager,
|
||||
return condition();
|
||||
}
|
||||
|
||||
bool DirectoryHasEntries(const std::filesystem::path& directoryPath) {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(directoryPath, ec) || !std::filesystem::is_directory(directoryPath, ec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::filesystem::directory_iterator(directoryPath) != std::filesystem::directory_iterator();
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> ListArtifactEntries(const std::filesystem::path& artifactsRoot) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::vector<fs::path> entries;
|
||||
std::error_code ec;
|
||||
if (!fs::exists(artifactsRoot, ec) || !fs::is_directory(artifactsRoot, ec)) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
for (const auto& shardEntry : fs::directory_iterator(artifactsRoot, ec)) {
|
||||
if (ec) {
|
||||
break;
|
||||
}
|
||||
if (!shardEntry.is_directory()) {
|
||||
entries.push_back(shardEntry.path());
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& artifactEntry : fs::directory_iterator(shardEntry.path(), ec)) {
|
||||
if (ec) {
|
||||
break;
|
||||
}
|
||||
entries.push_back(artifactEntry.path());
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(entries.begin(), entries.end());
|
||||
return entries;
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, ConcurrentAsyncLoadsCoalesceSameMeshPath) {
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
@@ -232,4 +272,313 @@ TEST(ProjectAssetIndex_Test, RefreshesSnapshotThroughImportServiceOnCacheMiss) {
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(AssetImportService_Test, BootstrapProjectBuildsLookupSnapshotAndReportsStatus) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_bootstrap_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
importService.SetProjectRoot(projectRoot.string().c_str());
|
||||
EXPECT_TRUE(importService.BootstrapProject());
|
||||
|
||||
AssetImportService::LookupSnapshot snapshot;
|
||||
importService.BuildLookupSnapshot(snapshot);
|
||||
EXPECT_GE(snapshot.assetGuidByPathKey.size(), 2u);
|
||||
EXPECT_GE(snapshot.assetPathByGuid.size(), 2u);
|
||||
EXPECT_NE(snapshot.assetGuidByPathKey.find("assets/runtime.material"), snapshot.assetGuidByPathKey.end());
|
||||
|
||||
AssetRef assetRef;
|
||||
EXPECT_TRUE(importService.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
const auto snapshotPathIt = snapshot.assetPathByGuid.find(assetRef.assetGuid);
|
||||
ASSERT_NE(snapshotPathIt, snapshot.assetPathByGuid.end());
|
||||
EXPECT_EQ(std::string(snapshotPathIt->second.CStr()), "Assets/runtime.material");
|
||||
|
||||
const AssetImportService::ImportStatusSnapshot status = importService.GetLastImportStatus();
|
||||
EXPECT_TRUE(status.HasValue());
|
||||
EXPECT_FALSE(status.inProgress);
|
||||
EXPECT_TRUE(status.success);
|
||||
EXPECT_EQ(std::string(status.operation.CStr()), "Bootstrap Project");
|
||||
EXPECT_EQ(std::string(status.targetPath.CStr()), projectRoot.string());
|
||||
EXPECT_GT(status.startedAtMs, 0u);
|
||||
EXPECT_GE(status.completedAtMs, status.startedAtMs);
|
||||
EXPECT_EQ(status.durationMs, status.completedAtMs - status.startedAtMs);
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(AssetImportService_Test, RebuildLibraryCacheKeepsStableAssetRefs) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_rebuild_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
importService.SetProjectRoot(projectRoot.string().c_str());
|
||||
ASSERT_TRUE(importService.BootstrapProject());
|
||||
|
||||
AssetRef firstAssetRef;
|
||||
ASSERT_TRUE(importService.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, firstAssetRef));
|
||||
ASSERT_TRUE(firstAssetRef.IsValid());
|
||||
|
||||
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "ArtifactDB" / "artifacts.db"));
|
||||
|
||||
EXPECT_TRUE(importService.RebuildLibraryCache());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "ArtifactDB" / "artifacts.db"));
|
||||
|
||||
AssetRef secondAssetRef;
|
||||
ASSERT_TRUE(importService.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, secondAssetRef));
|
||||
ASSERT_TRUE(secondAssetRef.IsValid());
|
||||
EXPECT_EQ(firstAssetRef.assetGuid, secondAssetRef.assetGuid);
|
||||
EXPECT_EQ(firstAssetRef.localID, secondAssetRef.localID);
|
||||
EXPECT_EQ(firstAssetRef.resourceType, secondAssetRef.resourceType);
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, RebuildProjectAssetCacheRefreshesLookupState) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_rebuild_cache_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
AssetRef firstAssetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, firstAssetRef));
|
||||
ASSERT_TRUE(firstAssetRef.IsValid());
|
||||
|
||||
const fs::path libraryRoot(manager.GetProjectLibraryRoot().CStr());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
|
||||
|
||||
EXPECT_TRUE(manager.RebuildProjectAssetCache());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
|
||||
|
||||
AssetRef secondAssetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, secondAssetRef));
|
||||
ASSERT_TRUE(secondAssetRef.IsValid());
|
||||
EXPECT_EQ(firstAssetRef.assetGuid, secondAssetRef.assetGuid);
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, SetResourceRootBootstrapsProjectAssetCache) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_bootstrap_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
AssetRef assetRef;
|
||||
EXPECT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
|
||||
const AssetImportService::ImportStatusSnapshot status = manager.GetProjectAssetImportStatus();
|
||||
EXPECT_TRUE(status.HasValue());
|
||||
EXPECT_FALSE(status.inProgress);
|
||||
EXPECT_TRUE(status.success);
|
||||
EXPECT_EQ(std::string(status.operation.CStr()), "Bootstrap Project");
|
||||
EXPECT_GT(status.startedAtMs, 0u);
|
||||
EXPECT_GE(status.completedAtMs, status.startedAtMs);
|
||||
EXPECT_EQ(status.durationMs, status.completedAtMs - status.startedAtMs);
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(AssetImportService_Test, ClearLibraryAndReimportAllAssetsManageArtifactsExplicitly) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_tooling_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
importService.SetProjectRoot(projectRoot.string().c_str());
|
||||
|
||||
ResourceType importType = ResourceType::Unknown;
|
||||
EXPECT_TRUE(importService.TryGetImportableResourceType("Assets/runtime.material", importType));
|
||||
EXPECT_EQ(importType, ResourceType::Material);
|
||||
|
||||
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
||||
EXPECT_TRUE(importService.ReimportAllAssets());
|
||||
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
||||
|
||||
EXPECT_TRUE(importService.ClearLibraryCache());
|
||||
EXPECT_FALSE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
||||
|
||||
EXPECT_TRUE(importService.ReimportAllAssets());
|
||||
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(AssetImportService_Test, ImportStatusTracksExplicitOperationsAndRefreshCleanup) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_status_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
importService.SetProjectRoot(projectRoot.string().c_str());
|
||||
EXPECT_FALSE(importService.GetLastImportStatus().HasValue());
|
||||
|
||||
EXPECT_TRUE(importService.ReimportAllAssets());
|
||||
const AssetImportService::ImportStatusSnapshot reimportStatus = importService.GetLastImportStatus();
|
||||
EXPECT_TRUE(reimportStatus.HasValue());
|
||||
EXPECT_FALSE(reimportStatus.inProgress);
|
||||
EXPECT_TRUE(reimportStatus.success);
|
||||
EXPECT_EQ(std::string(reimportStatus.operation.CStr()), "Reimport All Assets");
|
||||
EXPECT_EQ(reimportStatus.importedAssetCount, 1u);
|
||||
EXPECT_GT(reimportStatus.startedAtMs, 0u);
|
||||
EXPECT_GE(reimportStatus.completedAtMs, reimportStatus.startedAtMs);
|
||||
EXPECT_EQ(reimportStatus.durationMs, reimportStatus.completedAtMs - reimportStatus.startedAtMs);
|
||||
|
||||
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
||||
const std::vector<fs::path> artifactEntries = ListArtifactEntries(libraryRoot / "Artifacts");
|
||||
ASSERT_EQ(artifactEntries.size(), 1u);
|
||||
EXPECT_TRUE(fs::exists(artifactEntries.front()));
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove(materialPath, ec);
|
||||
ec.clear();
|
||||
fs::remove(fs::path(materialPath.string() + ".meta"), ec);
|
||||
|
||||
importService.Refresh();
|
||||
const AssetImportService::ImportStatusSnapshot refreshStatus = importService.GetLastImportStatus();
|
||||
EXPECT_TRUE(refreshStatus.HasValue());
|
||||
EXPECT_TRUE(refreshStatus.success);
|
||||
EXPECT_EQ(std::string(refreshStatus.operation.CStr()), "Refresh");
|
||||
EXPECT_GE(refreshStatus.removedArtifactCount, 1u);
|
||||
EXPECT_GT(refreshStatus.startedAtMs, 0u);
|
||||
EXPECT_GE(refreshStatus.completedAtMs, refreshStatus.startedAtMs);
|
||||
EXPECT_EQ(refreshStatus.durationMs, refreshStatus.completedAtMs - refreshStatus.startedAtMs);
|
||||
EXPECT_FALSE(fs::exists(artifactEntries.front()));
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, ReimportProjectAssetBuildsArtifactForSelectedPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_reimport_asset_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
EXPECT_TRUE(manager.CanReimportProjectAsset("Assets/runtime.material"));
|
||||
|
||||
const fs::path libraryRoot(manager.GetProjectLibraryRoot().CStr());
|
||||
EXPECT_TRUE(manager.ReimportProjectAsset("Assets/runtime.material"));
|
||||
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user