☰
HarmonyOS 7 onContinue:3DGS跨设备接续与LOD降级
2026/10/8 22:04:16 网站建设 项目流程

一、接续成功了,画面却回到场景入口

SplatContinuityLab 的需求听起来很自然:用户在手机上查看一个 3DGS 车站模型,走到工位后把视角接续到大屏,继续看刚才的柱体和站台。第一版也确实能拉起目标设备,但画面总是回到默认相机;偶尔连续点两次接续,目标端还会创建两套场景,显存占用翻倍。

故障发生时,系统能力本身没有报错。问题在我们传输的状态:最初尝试把可见 Gaussian 缓冲和纹理一起序列化,快照又大又慢,目标端还无法复用源端 GPU 资源。后来只传相机矩阵,却缺少块水位和场景版本,目标端先显示默认首帧,再突然跳到正确位置。任务 CONT-1812 就从这两次失败里定下来:只传可验证的轻量快照,恢复只能提交一次,并按目标设备能力决定 LOD。

最终 Demo 页面叫 HandoffViewportPage,场景是station_loop_v3.splat。快照只有 1.6 KiB,包含 pose epoch 42、相机位姿、可见块 88/89/90、源端 LOD 2 和一次性 resume token。目标设备内存档位为 LOW,因此恢复时把 LOD 从 2 降到 1,148 ms 内进入 CONTINUED,重复启动被忽略 1 次。

二、16:04,先删掉所有不能跨设备复用的东西

第一次复盘时,我们把快照字段分成三类。业务身份可以传:场景 ID、版本、相机位姿和选中物;恢复提示可以传:可见块 ID、源端 LOD、曝光参数;进程资源绝不能传:文件描述符、mmap 地址、GPU buffer handle、ArkGraphics 3D 实体引用。后者在目标进程没有意义,序列化它只会制造错误安全感。

第一段代码生成纯数据快照,并给每次接续分配 token。浮点数保留六位,既控制体积,也避免矩阵在 JSON 往返后出现无意义尾差。可见块最多记录三个,目标端把它们当预热提示,而不是“必须存在”的真相。

// ContinuitySnapshot.etsexportinterfaceHandoffSnapshot{version:3;scene:string;poseEpoch:number;token:stringcamera:number[];visibleBlocks:number[];sourceLod:number}exportfunctionencodeHandoffSnapshot(state:ViewportState):string{constsnapshot:HandoffSnapshot={version:3,scene:'station_loop_v3.splat',poseEpoch:42,token:`CONT-1812-${Date.now()}`,camera:state.camera.map((v)=>Number(v.toFixed(6))),visibleBlocks:state.visibleBlocks.slice(0,3),sourceLod:2}returnJSON.stringify(snapshot)}

编码发生在用户确认接续之后、场景还处于前台时。页面离场后不会重新读取相机,避免动画结束回调把另一组位姿混进快照。若场景版本正在更新,接续按钮暂时不可用;我们宁可提示“资源校验中”,也不发送一个目标端无法验证的版本。

三、16:18,onContinue 只负责交付证据

源端 UIAbility 的onContinue不执行网络传输,也不等待目标设备加载模型。它只检查当前页面是否可接续,生成快照并写入wantParam。序列化失败、场景尚未就绪或快照超过预算时返回拒绝,页面仍保持原状态。

// EntryAbility.etsonContinue(wantParam:Record<string,Object>):AbilityConstant.OnContinueResult{conststate=ContinuityStore.current()if(!state||state.phase!=='VIEWING'){returnAbilityConstant.OnContinueResult.REJECT}constpayload=encodeHandoffSnapshot(state)if(newTextEncoder().encode(payload).byteLength>8*1024){returnAbilityConstant.OnContinueResult.REJECT}wantParam['continuityTask']='CONT-1812'wantParam['handoffSnapshot']=payload hilog.info(0x1812,'SplatContinue',`task=CONT-1812 snapshot=1.6KiB epoch=42 blocks=88,89,90`)returnAbilityConstant.OnContinueResult.AGREE}

这里的 8 KiB 是项目预算,不是系统通用上限。它逼着快照保持“恢复描述”而不是资源包。方法返回后,源端页面不会立刻销毁渲染资源;只有接到接续完成状态才进入 SUSPENDED,并按正常生命周期释放场景。若接续失败,用户仍可以在源端继续操作。

四、16:31,目标端必须先去重,再创建场景

重复恢复来自两个入口:冷启动走onCreate,目标 Ability 已存在时又可能收到新的 Want。如果两条路径都直接创建 ArkGraphics 3D 场景,就会得到两份 renderer 和两套块缓存。修复后的restoreFromWant先验证版本和 token,再查询已消费集合;同一 token 第二次到达只记录 ignored,不触碰资源。

// ContinuityRestorer.etsexportasyncfunctionrestoreFromWant(want:Want,tier:'LOW'|'MID'|'HIGH'):Promise<RestoreResult>{constraw=want.parameters?.['handoffSnapshot']asstringconstsnapshot=JSON.parse(raw)asHandoffSnapshotif(snapshot.version!==3||ResumeTokenStore.has(snapshot.token)){return{state:'IGNORED_DUPLICATE',duplicateIgnored:1}}ResumeTokenStore.reserve(snapshot.token)consttargetLod=tier==='LOW'?Math.min(snapshot.sourceLod,1):snapshot.sourceLodconstscene=awaitsceneLoader.open(snapshot.scene,targetLod)awaitscene.prewarm(snapshot.visibleBlocks)scene.camera.apply(snapshot.camera,snapshot.poseEpoch)ResumeTokenStore.commit(snapshot.token)return{state:'CONTINUED',duplicateIgnored:0,targetLod}}

token 先 reserve、成功后 commit,是为了区分“正在恢复”和“已经恢复”。恢复失败会释放 reserve,允许系统再次投递;已 commit 的 token 则保留到场景关闭。场景加载、块预热和相机提交都在同一个 generation 下执行,页面退出或新的接续到来会令旧 generation 失效,迟到回调只能释放自身资源,不能改写 UI。

DevEco Studio 的 HiLog 按同一任务记录:snapshot=1.6KiB epoch=42 blocks=88,89,90,目标端输出tier=LOW lod=2->1 restore=148ms state=CONTINUED duplicateIgnored=1。右侧模拟器显示的场景名、块号、LOD 与日志一致,方便判断接续看到的是不是同一份证据。

五、16:47,LOD 降级不是失败,而是能力协商结果

目标设备无法保证和源端拥有同等内存与 GPU 预算。最初代码强行按 sourceLod=2 恢复,在低档设备上虽然视角正确,却会因为块预热过多造成首帧停顿。现在目标端先读取能力档位:LOW 使用 LOD 1,只预热 88/89/90 的低精度块;MID/HIGH 可以保持 LOD 2。相机、选中物和曝光不降级,变化只发生在几何与 SH 精度。

这条规则也写进 UI,避免用户把清晰度变化误判为接续失败。结果页同时展示“源端 LOD 2 / 目标 LOD 1 / 原因 LOW 内存档位”。后台还会在稳定三秒后尝试升档,但升档属于新的渲染任务,不修改 CONT-1812 的恢复结果。

从点击接续到首帧稳定共 148 ms:解析与验证 6 ms,模型索引打开 39 ms,三块预热 71 ms,场景创建与相机提交 32 ms。这里没有把源端 GPU 数据传过来,速度来自目标端已有模型和精确预热提示,而不是绕过资源生命周期。

六、源端和目标端各自有一条释放顺序

接续完成后,源端不是立即destroy。它先停止相机手势采样,冻结 pose epoch,等待正在提交的帧结束,再释放 ArkGraphics 3D 实体和块租约。目标端如果进入后台,只暂停渲染并保留已提交 token;回到前台可以恢复同一场景,不会把系统重投的 Want 当成第二次接续。

取消也有明确边界。用户在目标端首帧前返回,当前 generation 失效,预热块逐一归还,场景对象释放,token 的 reserve 被清除;源端保持 VIEWING。只有目标端提交第一帧并写入 commit 后,源端才切到 SUSPENDED。这样不会出现两边都认为自己已交接、结果两边都释放的空窗。

七、最终状态比“拉起成功”更重要

CONT-1812 最终的验收条件并不复杂:快照 1.6 KiB;场景与版本一致;pose epoch 42;预热块 88/89/90;目标档位 LOW;LOD 2→1;148 ms 首帧;重复恢复忽略 1 次;状态 CONTINUED。任何一项不一致,页面都会保持 VERIFYING,而不是先展示默认场景再补救。

这次复盘让我重新确认了跨设备接续的核心:它不是把源进程搬到目标进程,而是交付一份足够小、足够明确、可以验证的恢复意图。资源在各自设备重新创建,生命周期各自闭环,token 把重复入口收敛成一次提交。做到这些以后,用户感知到的才是“继续看刚才的位置”,而不是一次带着偶然性的远端启动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询