HarmonyOS 7 SceneForge 3DGS端侧重建实录 04:ModelWriteInfo × SaveResultToFile:PLY/MP4结果保存、文件状态与异常收口【鸿蒙心迹】
03 最后停在:
progress=100%
stage=FINISHED
status=RECON_FINISHED
这一步只说明 3DGS 重建已经完成。
对用户来说,真正能用的东西还没有出现。
要进入后续渲染、分享或归档,必须把 Session 里的结果写成文件。
Spatial Recon Kit 当前 HMS_SpatialRecon_ModelWriteInfo 提供了结果写入配置:modelFile 是必填的输出文件,modelFormat 指定输出格式;当前 HMS_SpatialReconOutputFormat 包含 PLY 与 MP4。audioFile 是可选 MP3 路径,longitude / latitude 则用于地理参考。HMS_SpatialRecon_SaveResultToFile 是异步保存接口,保存过程中再调用 GetProgress 时,进度已经表示保存进度。
SceneForge 04 就围绕这个阶段展开。
这次不换 Scene,也不重建第二遍。
直接继续 03 已完成的:
sessionId:
sf_session_20261003_03
先保存 PLY,再串行保存 MP4。
本轮统一数据:
taskId:
recon_20261003_04
sessionId:
sf_session_20261003_03
scene:
tabletop_tea_set_02
stageBeforeSave:
FINISHED
resultDir:
/data/storage/el2/base/files/
sceneforge/recon_03/results/
plyFile:
tabletop_tea_set_02_3dgs.ply
plySize:
86.4MB
plySaveCost:
1280ms
mp4File:
tabletop_tea_set_02_orbit.mp4
mp4Size:
18.7MB
mp4SaveCost:
2460ms
audioFile:
nullptr
geoTag:
false
saveQueueDepth:
1
concurrentSaveBlocked:
1
saveCallbackSuccess:
2 / 2
saveProgressSamples:
9
manifestWritten:
true
finalFiles:
2
destroyAfterSave:
true
status:
RESULT_READY

一、为什么 03 不直接把 writeInfo 传给 StartSession
HMS_SpatialRecon_StartSession 当前支持传:
HMS_SpatialRecon_ModelWriteInfo*
如果不为空,重建完成后可以继续执行写入。
SceneForge 没这么做。
原因不是接口不能用,而是这个系列想把:
RECONSTRUCT
和:
SAVE
拆成两个可独立观察的阶段。
03 只验收重建生命周期。
04 才开始保存。
这样:
94.8s
明确是重建耗时,
1280ms
2460ms
明确是两个保存任务的耗时。
性能口径不会混在一起。
二、保存之前第一条判断必须是 stage=FINISHED
当前官方状态码里有:
SPATIAL_RECON_STATUS_STAGE_NOT_FINISHED
所以项目层不等接口报错才处理。
保存入口先检查 03 的终态。
bool ResultSaveCoordinator::CanSave() const
{
return session_ != nullptr &&
snapshot_.stage ==
SPATIAL_RECON_STAGE_FINISHED &&
!isSaving_;
}
只要还在:
BUILDING
PAUSED
SAVING
就不允许新建保存任务。
三、ModelWriteInfo 里最关键的是 modelFile 与 modelFormat
PLY 保存配置:
HMS_SpatialRecon_ModelWriteInfo BuildPlyInfo()
{
HMS_SpatialRecon_ModelWriteInfo info {};
info.modelFile =
"/data/storage/el2/base/files/"
"sceneforge/recon_03/results/"
"tabletop_tea_set_02_3dgs.ply";
info.modelFormat =
SPATIAL_RECON_OUTPUT_FORMAT_PLY;
info.audioFile = nullptr;
info.longitude = 0.0f;
info.latitude = 0.0f;
return info;
}
本轮没有音频,也不做地理标记:
audioFile=nullptr
geoTag=false
所以经纬度保留默认 0。
这两个字段不能拿来“随便填一个位置”。
没有业务需求就不要伪造地理信息。
四、PLY 和 MP4 是两种完全不同的结果用途
SceneForge 当前把它们定义成:
PLY
后续 3DGS 加载、渲染、编辑的核心模型结果;
MP4
给用户快速预览和分享的环绕视频。
当前官方输出枚举就是:
SPATIAL_RECON_OUTPUT_FORMAT_PLY
SPATIAL_RECON_OUTPUT_FORMAT_MP4
所以 04 不在文章里虚构第三种自定义格式。
后面 05 的 spatialRender.GSPlugin 主线会使用模型结果进入 ArkGraphics 3D 渲染。
五、保存入口主动串行,不让两个 Save 同时跑
我第一次写的时候,PLY 保存刚开始,就让用户又点了:
导出 MP4
工程上没必要冒这个险。
SceneForge 当前强制:
saveQueueDepth=1
保存 PLY 时,新的保存请求直接:
concurrentSaveBlocked++
等 PLY 回调成功后,再开始 MP4。
HMS_SpatialReconStatus SaveOne(
HMS_SpatialRecon_ModelWriteInfo* info,
HMS_SpatialReconCallbackFunc callback)
{
if (isSaving_) {
concurrentSaveBlocked_++;
return SPATIAL_RECON_STATUS_STAGE_BUILDING;
}
isSaving_ = true;
return HMS_SpatialRecon_SaveResultToFile(
session_,
info,
callback);
}
这里用项目状态先做互斥。
不依赖“多次并发保存究竟会返回什么”来保证正确性。
六、GetProgress 在保存阶段必须换标签
03 里:
progress=46.2%
表示重建进度。
04 调用 SaveResultToFile 后,官方文档明确说:
GetProgress
只反映当前文件保存进度。
所以 Snapshot 变成:
operation=SAVING_PLY
saveProgress=...
而不是继续写:
reconProgress
本轮 PLY 和 MP4 都最终采样到:
100%
累计:
saveProgressSamples=9
七、PLY 保存耗时 1280ms,文件 86.4MB
本轮第一份结果:
tabletop_tea_set_02_3dgs.ply
86.4MB
1280ms
回调:
SUCCESS
只有保存回调成功以后,Result Manifest 才把:
plyReady=true
写入。
不能看到文件名出现就提前宣布成功。
八、MP4 保存耗时更长,单独记录
第二份:
tabletop_tea_set_02_orbit.mp4
18.7MB
2460ms
文件更小,但耗时更长并不矛盾。
MP4 可能包含额外的路径生成和视频编码过程。
SceneForge 只记录实际耗时,不用文件大小直接推断时间。
这两条性能基线以后会进 06 回归。
九、保存回调要把“成功状态”和“文件证据”一起检查
只有回调 SUCCESS 还不够。
SceneForge 保存完成后继续验证:
文件存在
size > 0
扩展名正确
Manifest 路径一致
如果回调成功但文件找不到:
SAVE_RESULT_MISSING
不会进入 RESULT_READY。
项目不把一个 callback 当成全部证据。
十、结果清单必须和两个文件一起落盘
保存完成后生成:
manifest.json
示意:
{
"taskId": "recon_20261003_04",
"sessionId": "sf_session_20261003_03",
"scene": "tabletop_tea_set_02",
"files": [
{
"type": "PLY",
"name": "tabletop_tea_set_02_3dgs.ply",
"sizeMb": 86.4
},
{
"type": "MP4",
"name": "tabletop_tea_set_02_orbit.mp4",
"sizeMb": 18.7
}
]
}
这段 Manifest 不是 Spatial Recon Kit 官方格式。
它是 SceneForge 自己的结果索引。
05 加载模型时,不再到目录里“猜哪个 PLY 是最新的”,而是直接读取 Manifest。
十一、文件目录要按任务隔离,不让结果互相覆盖
当前结果目录:
/data/storage/el2/base/files/
sceneforge/recon_03/results/
里面只放当前 Scene 这次任务的结果。
如果下一轮又重建:
tabletop_tea_set_02
会创建新任务目录,而不是覆盖旧文件。
这样可以保留:
版本
性能数据
对比模型
也避免结果缓存和渲染加载拿错文件。
十二、保存过程中不能 DestroySession
HMS_SpatialRecon_DestroySession 当前说明非常明确:
销毁以后 Session 失效,未保存的数据会丢失;如果需要持久化,必须在销毁前先保存。
所以 04 的资源收口顺序固定:
FINISHED
→ Save PLY
→ PLY callback success
→ Save MP4
→ MP4 callback success
→ Manifest
→ DestroySession
不是:
Start Save
→ 立刻 Destroy
十三、Destroy 以后 session 指针立即视为不可用
SceneForge 的 Session Owner 在 Destroy 后:
session_ = nullptr
并把:
destroyAfterSave=true
写进最终 Snapshot。
从这一刻起:
GetProgress
Pause
Resume
SaveResult
全部禁止。
后续渲染只依赖文件,不再依赖重建 Session。
这个“从 Session 到文件”的切换就是 04 的真正完成点。
十四、DevEco 图重点看“先 PLY,再 MP4,再 Destroy”
开发图:

HiLog:
taskId=
recon_20261003_04
sessionId=
sf_session_20261003_03
stage=
FINISHED
saveQueueDepth=
1
SaveResultToFile PLY
progress=
100%
ply=
86.4MB
cost=
1280ms
block concurrent save=
1
SaveResultToFile MP4
progress=
100%
mp4=
18.7MB
cost=
2460ms
callbacks=
2/2
manifestWritten=
true
finalFiles=
2
destroySession=
true
status=
RESULT_READY
整个保存链一眼就能看清。
十五、手机运行图只展示“可以用的结果”,不再展示 Session 细节
最终运行图:

顶部:
tabletop_tea_set_02
RESULT_READY
两个文件:
PLY
86.4MB
100%
MP4
18.7MB
100%
底部检查:
manifestWritten=true
save callback=2/2
destroyAfterSave=true
finalFiles=2
到这一篇,SceneForge 第一次真正拥有了:
可加载
可预览
可分享
的输出资产。
十六、为什么 MP4 也放在 04,而不是渲染篇
MP4 在这里是:
重建结果保存格式
05 的渲染主线则是:
把 3DGS 模型真正加载进 ArkGraphics 3D
并让用户交互浏览。
两个概念不能混。
一个是“导出视频文件”,另一个是“实时 3D 渲染”。
所以 04 保存 MP4,05 不再围绕视频播放,而会进入 PLY / GS 模型的实时加载。
十七、保存失败时不要马上 Destroy,允许项目层重试
如果 PLY 保存失败:
Session 仍然 FINISHED
结果仍在内存 / 工作目录上下文里
SceneForge 会:
保持 Session
标记 SAVE_FAILED
允许用户重试
只有:
用户主动放弃
或
保存成功
才 Destroy。
如果第一次失败就把 Session 销毁,恢复机会也一起丢了。
十八、04 最后固定八组保存测试
第一组,FINISHED 后保存 PLY 成功。
第二组,PLY 保存中重复点保存,项目层阻断。
第三组,PLY 完成后保存 MP4 成功。
第四组,保存阶段 GetProgress 口径切成 save progress。
第五组,modelFile 路径无效,状态不进入 RESULT_READY。
第六组,回调成功但文件不存在,结果校验失败。
第七组,两个文件都完成后 Manifest 正确。
第八组,DestroySession 后任何 Session API 都不再执行。
全部通过以后:
RESULT_READY
才成立。
十九、下一篇:文件有了,真正的 3D 浏览还没开始
04 最终拿到:
PLY
MP4
Manifest
但用户要的不是“结果目录”。
下一篇会继续:
spatialRender
GSPlugin
ArkGraphics 3D Scene
GSNode
Camera
RenderContext
把真正的 3DGS 结果加载到页面里。
也就是说,05 会正式从:
Spatial Recon
切到:
Spatial Render
但仍然使用同一个 SceneForge 项目和 04 生成的结果资产。
二十、保存前要先做磁盘空间预检
PLY 最终:
86.4MB
MP4:
18.7MB
真实保存过程还需要临时空间。
如果结果目录只剩:
20MB
直接调用 Save 没有意义。
SceneForge 在进入保存队列前先做:
availableBytes
检查。
项目不会写一个固定:
至少 200MB
的系统结论。
而是按当前任务估算:
预计 PLY
预计 MP4
安全余量
判断。
空间不足:
RESULT_STORAGE_NOT_ENOUGH
保持 Session,不 Destroy,让用户清理空间后重试。
二十一、保存进度要和当前文件绑定
PLY 与 MP4 都会触发:
stage=SAVING
progress=0~100%
如果 Snapshot 只有:
saveProgress=72%
UI 不知道这是哪个文件。
所以 04 保存状态增加:
saveTarget=PLY
或
saveTarget=MP4
本轮流程:
PLY 0 → 100
然后
MP4 0 → 100
而不是一条:
0 → 200%
二十二、第二次保存开始前必须确认第一次已经真正结束
项目层不只看:
isSaving=false
还要确认:
PLY callback success
PLY 文件验证通过
Manifest 内 PLY 状态 ready
三项都满足以后才启动 MP4。
否则第一份结果可能还在文件系统收口,第二个 Save 已经进入 Session。
本轮:
saveQueueDepth=1
concurrentSaveBlocked=1
就是为了验证这个边界。
二十三、ModelWriteInfo 的路径也要绑定 taskId
如果固定输出:
result.ply
下一次重建很容易覆盖上一版。
SceneForge 路径层级:
sceneforge/
recon_03/
results/
文件名再包含:
scene
结果类型
例如:
tabletop_tea_set_02_3dgs.ply
Manifest 则额外保存:
taskId
sessionId
scene
文件路径和业务身份有两层绑定。
二十四、audioFile 为 nullptr 也要明确记录
ModelWriteInfo 当前支持可选:
audioFile
要求是 MP3 路径。
SceneForge 这次没有空间音频或讲解音轨,因此明确:
audioFile=nullptr
而不是:
传一个不存在的空字符串路径
项目层把“没有音频”作为合法状态。
未来如果文旅场景要加讲解音轨,再重新验证:
音频文件存在
扩展名
生命周期
二十五、longitude / latitude 不应该用设备当前位置自动填充
ModelWriteInfo 还支持:
longitude
latitude
但 SceneForge 的茶具桌面场景和地理位置无关。
所以:
geoTag=false
经纬度保留默认。
即使应用拿到了 Location 权限,也不能因为“字段有位置”就自动把用户位置写进模型结果。
只有明确业务场景、用户预期和隐私设计都成立时,才应该启用地理标记。
二十六、结果文件最好再记录 hash
文件:
86.4MB
18.7MB
保存完成后,SceneForge 的后台索引还会计算:
fileSize
hash
createdAt
Manifest 只写:
路径 + size
已经够完成本篇验收。
真实产品则建议再保存完整 hash。
这样 05 加载模型时可以确认:
文件没有被替换
Manifest 没指错版本
二十七、MP4 失败不应该把已经成功的 PLY 删除
保存策略必须允许“部分成功”。
假设:
PLY success
MP4 failed
SceneForge 结果状态应该是:
PLY_READY
MP4_SAVE_FAILED
RESULT_PARTIAL
而不是回滚删除 86.4MB PLY。
因为 PLY 已经是有价值的核心模型资产。
用户可以稍后只重试 MP4。
本轮两个回调都成功,所以:
RESULT_READY
二十八、保存重试必须复用 FINISHED Session,而不是重新重建
保存失败以后,最昂贵的错误处理是:
重新 Push 54 帧
重新重建 94.8s
实际上只要 Session 仍然存在且 Stage 允许,项目应该重试:
SaveResultToFile
而不是重新构建模型。
这也是为什么保存失败时暂不 Destroy Session。
真正 Destroy 是一个不可逆边界。
二十九、DestroySession 需要放在统一 Owner 里
如果:
页面关闭
保存完成回调
用户取消
应用退出
四个地方都能直接 Destroy,很容易重复销毁。
SceneForge 继续沿用一个 Native Owner:
SpatialReconSessionManager
只有 Manager 可以执行:
DestroySession
其他模块只提交:
DESTROY_REQUEST
Manager 判断:
是否正在保存
是否还有待重试结果
是否已经 Destroy
再真正释放。
三十、Manifest 写入失败也不能静默宣布 RESULT_READY
如果:
PLY / MP4 都成功
但 manifest.json 写失败,
页面如果仍然显示:
RESULT_READY
下次 05 加载模型就找不到可靠索引。
所以本轮最终状态条件是:
PLY ready
MP4 ready
Manifest ready
Session destroyed
四项全部完成。
当前:
manifestWritten=true
destroyAfterSave=true
因此最终文件状态是完整的。
三十一、保存结果最好和重建性能日志分文件归档
03 的日志关注:
build cost
pause
mode switch
progress
04 的日志关注:
save progress
file size
save cost
callback
manifest
如果全部混在一个巨大文本里,后续很难做性能对比。
SceneForge 会生成:
recon_metrics.json
result_manifest.json
两个文件。
06 最终回归再把它们汇总到一份 Acceptance Snapshot。
三十二、04 真正完成的是“Session 资产化”
在 03 结束时,3DGS 结果只存在于:
HMS_SpatialRecon_Session
它是运行时状态。
04 结束以后,结果变成:
PLY
MP4
Manifest
这三种可以持久化、索引、分享、加载的资产。
从这一刻起,SceneForge 的业务重心才真正从:
Recon Session
切到:
Scene Asset
下一篇的 spatialRender 也正是建立在这个边界上。
三十三、结果目录也要防止“同名文件半覆盖”
如果 final path 已经存在,SceneForge 不会直接:
open + truncate
而是先判断:
Manifest 是否属于同一 taskId;
旧结果是否允许覆盖;
当前是否正在被渲染页使用。
默认策略是给新任务使用新的 task 目录。
这样即使同一个 scene 连续重建两次,也不会因为文件名相同把旧结果覆盖掉。
结果可追溯性比“目录看起来干净”更重要。
三十四、RESULT_READY 之后的第一消费方不是分享,而是 05 的渲染器
04 最终得到的 PLY 并不会立刻上传或分享。
下一篇 spatialRender.GSPlugin 会先用 Manifest 取到模型 URI,加载进 ArkGraphics 3D Scene,再验证:
GSNode 能创建;
相机可围绕观察;
模型缩放和中心点合理;
RenderContext 资源可释放。
只有“保存出的模型真的能被加载”以后,结果文件才完成从重建资产到交互资产的转换。
所以 04 的 RESULT_READY 是“文件层就绪”,05 才会进入“渲染层就绪”。这两个 Ready 状态继续保持清晰边界。
参考资料
HMS_SpatialRecon_ModelWriteInfo
https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/api/capi-spatialrecon-hms-spatialrecon-modelwriteinfospatial_recon_interface.h
https://developer.huawei.com/consumer/cn/doc/harmonyos-references/capi-spatial-recon-interface-h- Spatial Recon Kit 空间计算能力
https://developer.huawei.com/consumer/cn/features/spatialization
更多推荐



所有评论(0)