From ef7cbf0afc0ef7e8382d154cc59ad938496d7156 Mon Sep 17 00:00:00 2001 From: johnnyzhang-eng Date: Sat, 29 Aug 2026 08:48:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(client-bake):=20=E7=AC=AC=E4=B8=80=E5=B8=A7?= =?UTF-8?q?=E5=88=A4=E9=BB=91=E5=90=8E=E5=85=88=E9=87=8D=E8=AF=95,?= =?UTF-8?q?=E5=88=AB=E6=8A=8A=E8=83=BD=E8=87=AA=E6=84=88=E7=9A=84=E7=8A=B6?= =?UTF-8?q?=E5=86=B5=E5=8F=98=E6=88=90=E6=95=B4=E5=8D=95=E6=8A=A5=E5=BA=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #918 加的亮度闸把黑帧拦住了,但 compileAsync 那道预热在真实浏览器里不够 —— 它只保证着色器编译与**已解码**贴图的上传,不等图片本身解码。线上实测 4 单 (886/888/890/891)全部卡在第 0 帧,用户从「悄悄拿到一张黑帧」变成「整单失败」。 改成判黑后每 300ms 重渲一次、最多 10 次;3 秒内没好才失败。贴图解码是几百毫秒 的事,给到 3 秒是十倍余量。 Closes #924 --- .../src/features/client-bake/index.test.ts | 30 +++++++++++++++---- frontend/src/features/client-bake/index.ts | 22 ++++++++++++-- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/frontend/src/features/client-bake/index.test.ts b/frontend/src/features/client-bake/index.test.ts index 37b22312..7aaf131a 100644 --- a/frontend/src/features/client-bake/index.test.ts +++ b/frontend/src/features/client-bake/index.test.ts @@ -7,6 +7,7 @@ const stage = vi.hoisted(() => ({ clips: { walk: 1.0667 } as Record, coverage: 0.01, luma: 148, + lumaFn: null as null | (() => number), setups: [] as Array<[string, number, number]>, yaw: null as number | null, disposed: 0, @@ -37,7 +38,7 @@ vi.mock('./stage', async () => { return i * 0.1 }, coverage: () => stage.coverage, - subjectLuma: () => stage.luma, + subjectLuma: () => (stage.lumaFn ? stage.lumaFn() : stage.luma), rigInfo: () => ({ loader: 'gltf', rootBone: 'Hips', @@ -71,6 +72,7 @@ beforeEach(() => { stage.clips = { walk: 1.0667 } stage.coverage = 0.01 stage.luma = 148 + stage.lumaFn = null stage.setups = [] stage.yaw = null stage.disposed = 0 @@ -118,9 +120,25 @@ describe('浏览器出帧驱动', () => { expect(stage.disposed).toBe(1) }) - it('主体是纯黑时当场失败 —— 覆盖率那道闸拦不住它', async () => { - // 贴图还没传上 GPU 就渲的话,模型是个纯黑剪影,而它的 alpha 占比与正常帧 - // **一模一样**(线上实测 0.101 对 0.101)—— 只数 alpha 的闸放它过去。 + it('第一帧是黑的先等一下再看,好了就继续 —— 那是贴图还在解码,几百毫秒能自愈', async () => { + // compileAsync 只保证着色器编译与已解码贴图的上传,不等图片本身解码, + // 实测线上仍会在第 0 帧撞上。直接失败等于把一个能自愈的状况变成整单报废。 + let calls = 0 + stage.lumaFn = () => (++calls <= 2 ? 0 : 148) + const waits: number[] = [] + const apis = stubRender3DApis({}) + await runClientBake({ + job: bakeJob(), + apis, + sleep: async (ms) => { + waits.push(ms) + }, + }) + expect(waits.length).toBeGreaterThan(0) // 确实等过 + expect(calls).toBeGreaterThan(2) // 重试之后才拿到正常亮度 + }) + + it('等满了还是黑才失败,并且不把那一帧传上去', async () => { stage.luma = 0 const uploaded: number[] = [] let failed = '' @@ -134,7 +152,9 @@ describe('浏览器出帧驱动', () => { failed = reason }, }) - await expect(runClientBake({ job: bakeJob(), apis })).rejects.toThrow('纯黑') + await expect(runClientBake({ job: bakeJob(), apis, sleep: async () => {} })).rejects.toThrow( + '纯黑', + ) expect(uploaded).toEqual([]) expect(failed).toContain('纯黑') }) diff --git a/frontend/src/features/client-bake/index.ts b/frontend/src/features/client-bake/index.ts index 0e2dc6ce..23a2ddbf 100644 --- a/frontend/src/features/client-bake/index.ts +++ b/frontend/src/features/client-bake/index.ts @@ -25,6 +25,8 @@ export interface RunClientBakeOptions { apis: Render3DApis onProgress?: (progress: BakeProgress) => void signal?: AbortSignal + /** 判黑后的等待。测试注入,免得真等几秒。 */ + sleep?: (ms: number) => Promise } /** 出帧中途被放弃(用户离开页面 / 上层取消)。不当失败上报,任务留给期限兜底。 */ @@ -44,8 +46,13 @@ export class BakeAborted extends Error { /** 主体平均亮度下限。纯黑剪影量到 0.0,正常帧量到约 148 —— 取 20 只拦「全黑」。 */ const MIN_SUBJECT_LUMA = 20 +/** 判黑后重试几次、每次等多久。贴图解码是几百毫秒的事,给到 3 秒是十倍余量。 */ +const BLACK_FRAME_RETRIES = 10 +const BLACK_FRAME_WAIT_MS = 300 + export async function runClientBake(options: RunClientBakeOptions): Promise { const { job, apis, onProgress, signal } = options + const sleep = options.sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms))) const throwIfAborted = () => { if (signal?.aborted) throw new BakeAborted() } @@ -79,11 +86,20 @@ export async function runClientBake(options: RunClientBakeOptions): Promise= 0 && luma < MIN_SUBJECT_LUMA && k < BLACK_FRAME_RETRIES; k++) { + await sleep(BLACK_FRAME_WAIT_MS) + luma = stage.subjectLuma() + } if (luma >= 0 && luma < MIN_SUBJECT_LUMA) { throw new StageError( - `第 ${i} 帧主体是纯黑(平均亮度 ${luma.toFixed(1)} < ${MIN_SUBJECT_LUMA}),贴图可能还没就绪`, + `第 ${i} 帧主体是纯黑(平均亮度 ${luma.toFixed(1)} < ${MIN_SUBJECT_LUMA}),` + + `等了 ${(BLACK_FRAME_RETRIES * BLACK_FRAME_WAIT_MS) / 1000} 秒仍未就绪`, ) } await apis.putBakeFrame(job.taskId, i, await stage.grab())