Skip to content

Add background blur support inside 3D rendering contexts.#1432

Merged
shlzxjp merged 11 commits into
mainfrom
feature/partyhuang_background
May 18, 2026
Merged

Add background blur support inside 3D rendering contexts.#1432
shlzxjp merged 11 commits into
mainfrom
feature/partyhuang_background

Conversation

@Hparty
Copy link
Copy Markdown
Collaborator

@Hparty Hparty commented May 14, 2026

在 preserve3D 子树内补齐 BackgroundBlur 等背景样式的支持,使 3D 子树中的图层可以正常使用 BackgroundBlurStyle。

主要改动:

  • 3D 合成两阶段流水线:将 Layer3DContext 改为 addLayer + finishAndDrawTo 两阶段——先收集图层、再统一完成并绘制到目标,便于分别处理几何收集和栅格合成。

  • 复用标准 Background 流水线:通过文件内私有的 Compositor3DBackgroundSource 让 3D 片段的栅格化复用 BackgroundCapturer 流水线,片段内分发以及嵌套 offscreen 都走 createSubHandler 同一条路径,去掉 3D 专用的 background 处理代码。

  • alpha 策略:DrawPolygon3D 暴露 rasterAlpha / compositeAlpha,由合成器与栅格 pass 各自施加一次 per-layer alpha,由 allowsGroupOpacity 控制启用。

  • snapshot map 多消费者:将读游标从 BackgroundSnapshotMap 移到 BackgroundConsumer 上,让同一份 snapshot map 可以被多个 consumer 共享(tiled 渲染、部分缓存 pass);同时在 3D 子树捕获阶段保留外层 canvas 已有的 zoom / translate。

  • 修复 Layer::draw 路径下背景捕获错位:drawRect 改用 device 像素空间(outerCanvas.mapRect(rectForDraw)),避免 globalMatrix 含 perspective 时 bgSurface 大小与位置错位、背景丢失。

  • 内部架构清理:将 3D collectNodes 共享遍历与 drawFunc 上移到 Layer3DContext 基类,subclass 通过 emitNode 钩子注册节点;修复 Render3DContext::rasterLayer 在跨片段复用 leafArgs 时残留的悬空 BackgroundHandler 指针。

测试:BackgroundBlurTest.BackgroundBlur3DLayer 覆盖 Preserve3D / Flat / Preserve3DFallback / DirectDraw / NestedOffscreen / Nested3D 等场景,截图基线已更新。

Based on #1384 by @StarryThrone.

Co-authored-by: StarryThrone 18690813+StarryThrone@users.noreply.github.com

Copy link
Copy Markdown
Collaborator

@shlzxjp shlzxjp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

代码评审反馈(P0 + P1):以下问题建议在合入前处理。每条已对照 PR head 源码核对。

  • P0:基线人工核对、primeCompositorFromOuterCanvas 静默降级、BackgroundConsumer::readCursors 越界静默 skip
  • P1Layer.h 新增 friend 暴露面、isCapturer() + static_cast 反向类型识别、Layer3DContext::_drawFunc 重复 addLayer 静默覆盖、关键 unit test 缺失

详情见各行级评论。

"ImageWithMipmap": "1ea63c49",
"ImageWithShadow": "c37a86c8",
"Layer3DTree": "9e2c8c8ee",
"Layer3DTree": "e581709f9",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0 / Major] 老 baseline key 被 bump 需要人工核对

本次基线变更不仅有 6 个新 BackgroundBlur3DLayer_* key,还把 4 个与 BackgroundBlur 无直接关联的老 key 一并 bump:

  • Layer3DTree(line 169)
  • Matrix_3D_2D_3D_Preserve3D(line 302)
  • PartialDrawLayerPartialDrawLayer_shapeLayer(line 306-307)

alpha 策略重构(rasterAlpha/compositeAlpha)+ BSP raster/composite 分离 + contentScale 矩阵改动很可能导致 2D/3D 的像素差异,可能是预期的精度提升,也可能是无意回归。

建议

  1. PR 描述里逐 key 解释像素差异原因;
  2. reviewer 人工对比 _base.webp 旧/新基线,确认无可见质量退化;
  3. 截图差异不可见时,在 PR 描述附「肉眼无差/可接受」结论。

if (primeImage != nullptr) {
_compositor->primeWithImage(primeImage);
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0 / Major] primeCompositorFromOuterCanvas 静默降级丢失 backdrop

本函数有 5 处早返回路径:

  • L241 outerSurface == nullptr(picture canvas / 无 surface 的 canvas)
  • L245 outerSnapshot == nullptr
  • L250 targetWidth/Height <= 0
  • L260 矩阵不可逆
  • L266 picture == nullptr

全部静默 return;,但 finishAndDrawTo L119-128 的 compositorSource 仍会被构造、leafCapturer 仍写 entry,consume 端读出未 prime 过的 compositor 截图——blur 看到的是仅含 BSP 累积内容的「半 backdrop」,体感为偶发深色边/缺失。

这与项目规范 .codebuddy/rules/Code.md 「执行已确认的方案时…禁止静默降级为简化方案」直接冲突。

建议

  1. 让函数返回 bool;上层据此跳过 compositorSource 的构造,让 blur 走显式 fallback 而非读「半 backdrop」;
  2. 失败原因加 LOGW 一次(picture canvas 路径属预期,可走静默 + 文档化);
  3. picture canvas + 3D + BackgroundBlur 在 PR 描述 / 类头注释中显式声明为「已知不支持场景」。

auto& cursor = readCursors[key];
if (cursor >= it->second.size()) {
return;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0 / Major] cursor 越界静默 skip 隐藏 capture/consume 不一致

auto& cursor = readCursors[key];
if (cursor >= it->second.size()) {
  return;
}

配合 BackgroundCapturer::drawBackgroundStyle 改为 push_back(L235),capture/consume 必须严格 N:N。一旦发散即静默丢 entry,肉眼难察觉。

典型发散场景:Render3DContext::rasterLayerRender3DContext.cpp:202-228)中 Surface::Make / createFromSurface 失败时,不写 snapshot entry,但 consume 端仍按 fragment 数循环;从失败片段起所有后续片段都从「错位」cursor 读 entry,blur 偶发缺失或张冠李戴。

建议(双管齐下):

  1. 此处加 DEBUG_ASSERT(cursor < it->second.size() && "capture/consume push-pop count mismatch"); 让发散立即暴露;
  2. Render3DContext::rasterLayer 失败路径仍 push_back 一个 image=nullptr 占位 entry(消费端识别为「该片段无 backdrop」而非偏移所有后续 cursor)。

friend class LayerProperty;
friend class LayerSerialization;
friend class OffscreenRenderer;
friend class Layer3DContext;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 / Minor] 新增 friend 在公共头放大暴露面

Layer3DContext::collectNodessrc/layers/compositing3d/Layer3DContext.cpp:55-82)通过此 friend 直接访问:

  • _children_alphamaskOwner(L66)
  • canPreserve3D()hasBackgroundStyle()computeContentBounds()getMatrixWithScrollRect()(私有方法)

这是本 PR 唯一在公共头里放大暴露面的改动;3D 上下文反向窥视 Layer 内部,让未来 Layer 内部演进必须连带改 Layer3DContext。

建议:在 Layer 内新增私有 helper(如 forEach3DDescendant(visitor)),让 Layer3DContext 通过窄接口拿数据,去掉 friend;ROI 较高,几乎不增加代码量。

bool capturePass = args.backgroundHandler != nullptr && args.backgroundHandler->isCapturer();
if (_subtreeNeedsBackdrop && capturePass) {
auto* outerCapturer = static_cast<BackgroundCapturer*>(args.backgroundHandler);
outerSnapshots = outerCapturer->snapshotMap();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 / Minor] static_cast<BackgroundCapturer*> + isCapturer() 是手工反向类型识别

bool capturePass = args.backgroundHandler != nullptr && args.backgroundHandler->isCapturer();
if (_subtreeNeedsBackdrop && capturePass) {
  auto* outerCapturer = static_cast<BackgroundCapturer*>(args.backgroundHandler);
  outerSnapshots = outerCapturer->snapshotMap();

项目规范禁用 dynamic_cast,当前用 isCapturer() 虚函数 + static_cast 是其等价替代品。能工作的前提是「仅 BackgroundCapturer 返回 true」——一旦未来新增 capture 系子类(如 SubBackgroundCapturer)违反此约定,static_cast 即 UB。同时 isCapturer()snapshotMap() 仅供 3D 模块使用却作为 public 虚函数暴露在基类。

建议:基类增 virtual BackgroundCapturer* asCapturer() { return nullptr; }BackgroundCapturer override 返回 this;调用点改为:

if (auto* outerCapturer = args.backgroundHandler ? args.backgroundHandler->asCapturer() : nullptr) { ... }

这能同时去掉 isCapturer()snapshotMap() 两个 public 接口,让类型契约由编译器守护。

return _transformStack.empty() ? Matrix3D::I() : _transformStack.top().transform;
void Layer3DContext::addLayer(Layer* layer, const Matrix3D& transform, float alpha,
LayerDrawFunc drawFunc) {
_drawFunc = drawFunc;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 / Minor] _drawFunc 共享状态被二次 addLayer 静默覆盖

void Layer3DContext::addLayer(Layer* layer, const Matrix3D& transform, float alpha,
                              LayerDrawFunc drawFunc) {
  _drawFunc = drawFunc;  // 第二次调用会无声覆盖第一次的 drawFunc
  collectNodes(layer, transform, alpha, /*depth=*/0);
}

当前调用约定是「每个 Layer3DContext 实例只 addLayer 一次再 finish」,但接口未阻止重复调用,第二次调用静默覆盖第一次。未来若新增 「同一 3D 上下文中混合 drawLayer / drawContour 节点」 这类扩展,bug 难以察觉。

建议(任选其一):

  1. DEBUG_ASSERT(_drawFunc == nullptr || _drawFunc == drawFunc);(最小修改);
  2. drawFunc 下沉为 PendingNode 字段,实现「不同节点不同 drawFunc」(更彻底)。


displayList->render(surface.get());
EXPECT_TRUE(Baseline::Compare(surface, "BackgroundBlurTest/BackgroundBlur3DLayer_Nested3D"));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 / Minor] 关键 unit test 覆盖缺失

本 PR 同时删除了 LayerTest.Layer3DContextAPI(原覆盖 stack 模型 begin/end recording 嵌套)但未补回新两阶段 API 的等价测试。综合复核后建议补 4 类:

  1. Layer3DContext 两阶段 API unit testaddLayer / finishAndDrawTo / emitNode 的调用顺序、空 _pendingNodes 早返回、嵌套 3D 上下文 render3DContext 设置/恢复等。
  2. BackgroundConsumer 多游标行为:直接构造 BackgroundSnapshotMap + 同 key 多次 push_back,串行喂两个 BackgroundConsumer,验证游标互不干扰。这是本 PR 新增的核心机制,目前只有端到端截图间接覆盖。
  3. GroupOpacity on/off 双 alpha 策略:3D 子树内显式切换 setAllowsGroupOpacity(true/false) 验证 rasterAlpha(baked 进 image)vs compositeAlpha(顶点色)两条路径分别生效。
  4. picture canvas + 3D + BackgroundBlur fallback:当前所有 case 都走 GPU 路径,需补 picture canvas 上 3D + BackgroundBlur 不崩溃且行为可预期(与上文 P0「primeCompositorFromOuterCanvas 静默降级」配套)。

建议加在 BackgroundBlur3DLayer 之后,或独立放回 LayerTest.cppLayer3DContextAPI 位置。

layerHasBackgroundStyle.emplace(node.layer, node.hasBackgroundStyle);
}
DrawArgs leafArgs = args;
leafArgs.opaqueContext = nullptr;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这一行 leafArgs.opaqueContext = nullptr; 是死代码:Render3DContext 只在 Create3DContext()opaqueMode == false(即 args.opaqueContext == nullptr)时才会被构造,因此 args.opaqueContext 进入这里时一定是 null,重置没有意义。建议删除这一行。

if (_pendingNodes.empty()) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这一行 leafArgs.opaqueContext = nullptr; 是死代码:循环里每个节点都会用 nodeArgs = leafArgs 拷贝一次后立即 nodeArgs.opaqueContext = &opaqueContext; 覆盖,初值永远不会被任何 drawWithFunc 读到。建议删除这一行。

BackgroundSnapshotKeyHash> {
struct BackgroundSnapshotMap {
std::unordered_map<BackgroundSnapshotKey, std::vector<BackgroundSnapshotEntry>,
BackgroundSnapshotKeyHash>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议在这里的注释中再点一句:消费者(BackgroundConsumer)现在会通过自己的 readCursors 状态读取此 map,外部只读 snapshots 字段。这样读者一眼就能理解 DisplayList::drawTileTask / drawRootLayer 的签名为何从 const BackgroundSnapshotMap* 改成非 const —— 不是某个调用点真的要写入 snapshots,而是消费者一侧的 cursor 状态绑定到了 consumer 自己。

@Hparty
Copy link
Copy Markdown
Collaborator Author

Hparty commented May 15, 2026

@shlzxjp Re: [P1 / Minor] 新增 friend 在公共头放大暴露面 — Layer3DContext 属于 Layer 合成子系统的一部分(与已有的 BackgroundCapturer/BackgroundConsumer friend 同层),collectNodes 访问的 _children / _alpha / maskOwner 等都是 Layer 内部合成路径的数据,friend 关系合理,不需要额外窄接口。

// Functor adapter so BspTree::traverseBackToFront — which expects a callable taking
// const DrawPolygon3D* — can collect fragments into a vector without a lambda.
struct CollectFragmentsAction {
std::vector<DrawPolygon3D*>* output = nullptr;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个地方应该统一起来,使用std::vector<const DrawPolygon3D*>* output = nullptr;
operator的参数是const,预期不可修改,push进output时强制解除了const限定,语义上是冲突的,统一下

@shlzxjp shlzxjp merged commit 7b69a2e into main May 18, 2026
10 checks passed
@shlzxjp shlzxjp deleted the feature/partyhuang_background branch May 18, 2026 13:58
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.

3 participants