Skip to content

修复渲染 Y 轴镜像 + 补字体守护日志(#193 #199)#203

Merged
appergb merged 2 commits into
betafrom
fix/193-render-y-flip
Jul 6, 2026
Merged

修复渲染 Y 轴镜像 + 补字体守护日志(#193 #199)#203
appergb merged 2 commits into
betafrom
fix/193-render-y-flip

Conversation

@appergb

@appergb appergb commented Jul 6, 2026

Copy link
Copy Markdown
Owner

根因

#193(CRITICAL 全局 Y 轴镜像):预览/播放/导出/inspect 四路共用的 wgpu 合成 shader(crates/opentake-render/src/gpu/shader.wgsl)顶点段,把 affine_transform 产出的 canvas 像素坐标(CG 语义:原点左下角、y 朝上,crates/opentake-render/src/plan/affine.rs 逐行移植自上游 CompositionBuilder.affineTransform,验证与上游完全一致)直接当 wgpu NDC 用:

let ndc = vec2<f32>(
    px.x / canvas.x * 2.0 - 1.0,
    px.y / canvas.y * 2.0 - 1.0,   // 漏掉 y 翻转
);

但 wgpu/WebGPU 的 NDC 虽然 y 朝上(+1=顶),其光栅化目标的视口/帧缓冲坐标系是 y 朝下(第 0 行=顶)。这一行没有做这个转换,导致 centerY 小的 clip(应在画布顶部附近)渲染到帧缓冲底部附近,反之亦然——渲染行数 ≈ (1 - centerY) × H,与真机实测「centerY=0.2 渲染在 ~0.78」「centerY=0.1 → ~0.85」完全吻合。

级联发现canvas_uv(mask 用的归一化画布坐标)原写作 1.0 - px.y / canvas.y——这不是独立于 NDC 的计算,而是手动补偿了上面这行的镜像 bug,让 mask 覆盖范围与(同样镜像的)帧缓冲保持配对一致。只改 NDC 一行会让这个补偿失配,mask 相对于它裁剪的内容整体镜像。用真实 GPU 渲染验证到这一步:初次只翻 NDC 后新增的 mask 回归测试当场失败(白色区域出现在错误的行),排查后确认两行必须同步翻转。

修复

shader.wgsl 两处协同改动:

  1. NDC 行翻转:1.0 - px.y / canvas.y * 2.0(帧缓冲行与 CG 像素 y 正确配对)。
  2. canvas_uv 同步去掉补偿翻转,改为 px.y / canvas.y(与 opentake_domain::grade::Mask::coverageTransform.center_y 的原点左上角/y 朝下语义直接对齐,不再依赖与旧 bug 的巧合抵消)。

affine.rsflip_vertical 分支、旋转中心 cy 等)全部是 CG 像素空间内的纯代数,不依赖 wgpu NDC 约定,未改动;其自带单测(如 rotate_90_maps_x_axis_to_y_axis)本身就是自洽的 CG 语义验证,与本次 shader 修复无关。

新增回归测试

crates/opentake-render/tests/gpu_y_orientation.rs(真实 GPU 渲染,无 GPU 环境按现有约定 skip):

  • clip_authored_near_top_renders_in_top_half — centerY=0.2 的实色 clip,非黑像素质心必须落在画布上半区
  • clip_authored_near_bottom_renders_in_bottom_half — centerY=0.8 → 下半区
  • top_and_bottom_authored_clips_render_on_opposite_halves — 交叉校验两者相对次序
  • off_center_mask_clips_to_authored_screen_region_not_mirrored — mask 侧配套检查:圆形 mask 中心 (0.5, 0.2) 必须裁出画布顶部附近的内容,而不是镜像位置(专门捕捉"只翻一行导致 mask 与内容错配"的回归)

4 个测试在修复前对着旧 shader 全部失败(用实际渲染结果验证过:质心/白色像素位置与理论推导的镜像值几乎完全吻合),修复后全部转绿。

没有任何既有测试需要修改预期opentake-render 原有全部用例(含 gpu_effects.rs 的两个 mask 测试)用的都是满画布 clip 或正中心 (0.5, 0.5) 的 mask,两者对 Y 方向镜像天然不敏感,这正是这个 CRITICAL bug 此前从未被测试捕捉到的原因。

#199(顺带,小)

CosmicTextRasterizer 在宿主机找不到任何系统字体时,静默把文字 clip 渲染成全透明,没有任何提示。在三个生产构造点各加一次 has_fonts() 检查:

  • src-tauri/src/render.rs(预览合成,GpuContext 首次构造时)
  • src-tauri/src/mcp.rscomposite_frames_jpeg,inspect_media/inspect_timeline 用)
  • src-tauri/src/export.rsrun_export_with_control,导出用)

falseeprintln!("[render] no system fonts discovered; text clips will render blank"),风格对齐现有 mcp.rs 内其余 eprintln!("[mcp] ...") 约定。不做字体 fallback(后续 issue 的事)。

验证

cargo fmt --all -- --check      # 通过
cargo clippy --workspace --all-targets -- -D warnings   # 通过,零告警
cargo test -p opentake-render -p opentake-tauri         # 全绿
  • opentake-render:72 测试全过(54 单测 + 18 GPU 集成测试,含新增 4 个)
  • opentake-tauri:156 测试全过(147 单测 + 9 集成测试),含直接命中 mcp.rs 改动路径的 inspect_timeline_composites_real_frames_when_gpu_available

Fixes #193
Fixes #199

baiqing added 2 commits July 6, 2026 21:08
…193)

The vertex shader mapped canvas-pixel Y (origin bottom-left, y up, per
affine_transform's CG convention) straight onto wgpu NDC without
accounting for wgpu's y-down viewport/framebuffer convention (NDC +1 =
top row, -1 = bottom row). Every clip rendered at row (1 - center_y)
instead of center_y, mirroring the whole canvas vertically across
preview/playback/export/inspect (all four share this compositor).

canvas_uv (mask evaluation) carried a compensating flip that had
silently canceled out with the buggy NDC line, keeping mask coverage
paired with the (also mirrored) framebuffer. Fixing the NDC line alone
would have mirrored every mask relative to the content it clips, so
both lines flip together.

New regression tests (crates/opentake-render/tests/gpu_y_orientation.rs)
render real GPU frames and check where content actually lands:
clip_authored_near_top_renders_in_top_half,
clip_authored_near_bottom_renders_in_bottom_half,
top_and_bottom_authored_clips_render_on_opposite_halves, and
off_center_mask_clips_to_authored_screen_region_not_mirrored (the
mask/NDC pairing check). All 4 fail against the pre-fix shader and pass
after.

No existing opentake-render test needed an expectation change: every
prior assertion used either a full-canvas clip or a dead-centered
(0.5, 0.5) mask, both Y-symmetric and therefore blind to the mirror.
CosmicTextRasterizer silently renders text clips fully transparent
when the host has no discoverable system fonts, with no signal that
this happened. Add a has_fonts() check right after construction at
each of the three production call sites (preview composite in
render.rs, MCP inspect_media/inspect_timeline compositing in mcp.rs,
and video export in export.rs) and eprintln a warning so a blank-text
render is diagnosable instead of silent. No fallback font behavior —
that's separate follow-up work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant