Files
XCEngine/docs/used/Library启动预热与运行时异步加载混合重构计划_过期归档_2026-04-11.md

22 KiB
Raw Blame History

Library启动预热与运行时异步加载混合重构计划

归档状态2026-04-11。本计划不再作为活跃主计划继续推进其未收口部分已被后续更细分的 NanoVDB 首帧阻塞修复、Shader 并入 Library、以及 editor/runtime 收口方案分别接管,因此转入 docs/used 作为历史归档。

文档日期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 正式的三段式生命周期

阶段 AProject Startup Bootstrap

项目打开时:

  • 恢复 Library 数据库
  • 建索引
  • 快速验证缓存状态
  • 后台预热 metadata

不恢复大资源 payload。

阶段 BScene Structural Load

打开 scene 时:

  • 只做场景结构反序列化
  • 组件恢复 AssetRef 和资源路径 hint
  • 不在主线程同步恢复大 mesh/material/texture

阶段 CRuntime 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 已改为持有稳定 ResourceGUIDUnloadAll/Shutdown 后不再因为悬空指针在析构期崩溃
  • ResourceManagerUnload/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 这类场景时,命中链路不应再接近首次导入成本

阶段 3Scene 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/SessionCacheLibrary/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 阶段边界清晰
  • AssetImportServiceProjectAssetIndexResourceManager 职责边界清晰
  • editor 主线程不再承担重资源恢复职责

7.4 观测层

  • 用户能区分 BootstrapImportScene 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 直载、依赖快速校验、误重导入路径问题均已修复
阶段 3Scene 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 主线程。