
一、接续成功了画面却回到场景入口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 降到 1148 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:18onContinue 只负责交付证据源端 UIAbility 的onContinue不执行网络传输也不等待目标设备加载模型。它只检查当前页面是否可接续生成快照并写入wantParam。序列化失败、场景尚未就绪或快照超过预算时返回拒绝页面仍保持原状态。// EntryAbility.etsonContinue(wantParam:Recordstring,Object):AbilityConstant.OnContinueResult{conststateContinuityStore.current()if(!state||state.phase!VIEWING){returnAbilityConstant.OnContinueResult.REJECT}constpayloadencodeHandoffSnapshot(state)if(newTextEncoder().encode(payload).byteLength8*1024){returnAbilityConstant.OnContinueResult.REJECT}wantParam[continuityTask]CONT-1812wantParam[handoffSnapshot]payload hilog.info(0x1812,SplatContinue,taskCONT-1812 snapshot1.6KiB epoch42 blocks88,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):PromiseRestoreResult{constrawwant.parameters?.[handoffSnapshot]asstringconstsnapshotJSON.parse(raw)asHandoffSnapshotif(snapshot.version!3||ResumeTokenStore.has(snapshot.token)){return{state:IGNORED_DUPLICATE,duplicateIgnored:1}}ResumeTokenStore.reserve(snapshot.token)consttargetLodtierLOW?Math.min(snapshot.sourceLod,1):snapshot.sourceLodconstsceneawaitsceneLoader.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 按同一任务记录snapshot1.6KiB epoch42 blocks88,89,90目标端输出tierLOW lod2-1 restore148ms stateCONTINUED duplicateIgnored1。右侧模拟器显示的场景名、块号、LOD 与日志一致方便判断接续看到的是不是同一份证据。五、16:47LOD 降级不是失败而是能力协商结果目标设备无法保证和源端拥有同等内存与 GPU 预算。最初代码强行按 sourceLod2 恢复在低档设备上虽然视角正确却会因为块预热过多造成首帧停顿。现在目标端先读取能力档位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目标档位 LOWLOD 2→1148 ms 首帧重复恢复忽略 1 次状态 CONTINUED。任何一项不一致页面都会保持 VERIFYING而不是先展示默认场景再补救。这次复盘让我重新确认了跨设备接续的核心它不是把源进程搬到目标进程而是交付一份足够小、足够明确、可以验证的恢复意图。资源在各自设备重新创建生命周期各自闭环token 把重复入口收敛成一次提交。做到这些以后用户感知到的才是“继续看刚才的位置”而不是一次带着偶然性的远端启动。