Skip to content

Add DepthStencilTextureView to share one stencil attachment across same-spec render targets. - #1514

Open
StarryThrone wants to merge 12 commits into
mainfrom
feature/jasonrjchen_stencil_cache_split
Open

Add DepthStencilTextureView to share one stencil attachment across same-spec render targets.#1514
StarryThrone wants to merge 12 commits into
mainfrom
feature/jasonrjchen_stencil_cache_split

Conversation

@StarryThrone

Copy link
Copy Markdown
Collaborator

背景:此前 stencil 由每个 RenderTargetProxy 私有创建并持有,同规格的多个 RenderTarget 各自占用一块 depth/stencil 显存,无法复用。

改动:新增 DepthStencilTextureView 可缓存资源,按规格(width/height/sampleCount)派生 UniqueKey 纳入 ResourceCache 统一管理;RenderTargetProxy::getStencil 经 DepthStencilTextureView::Make 获取共享 attachment(维持 getStencil + getOrAllocateStencil 双层结构,TextureRenderTargetProxy 继续以 backingStore 尺寸定制),OpsRenderTask 通过 getStencil 取用并以 getTexture 填入 render pass descriptor。stencil 仅作为 unique 资源复用,不挂 scratch key、不参与 unique→scratch 降级;共享安全性依赖各 render pass 的 LoadAction::Clear,内容不跨 pass 携带。

效果:同规格的多个 RenderTarget 共享同一块物理 stencil,显存占用从每 proxy 一块降为每规格一块。

测试:RenderTargetProxyGetStencil 覆盖 proxy 内复用、跨 proxy 共享与 spec 隔离三个场景;Dispatch_StencilReuseAcrossPasses 以多 render pass 复用加 EvenOdd 重绘验证 per-pass 清零(新增基准 StencilCoverPath/ReuseOverpasses 与 ReuseOverpasses_Redraw)。OpenGL 后端全量 594 个用例通过。

…me-spec render targets.

将 RenderTargetProxy 私有创建的 stencil 改为经 ResourceCache 按 spec(width/height/sampleCount)派生 unique key 命中共享的 DepthStencilTextureView,OpsRenderTask 经 getStencil 取用共享 attachment;补充 proxy 内复用、跨 proxy 共享、spec 隔离及多 render pass 复用清理的测试。

@Hparty Hparty left a comment

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.

Reviewed by CodeBuddy: two follow-ups, no blocking issues.

ASSERT_TRUE(proxy != nullptr);
auto stencil = proxy->getStencil(1);
ASSERT_TRUE(stencil != nullptr);
EXPECT_EQ(stencil.get(), first.get());

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.

测试盲区:Case 2 断言 stencil.get() == first.get() 时,first 仍持有强引用,只覆盖了"资源被引用期间的共享"路径。全局缓存的核心场景是:所有代理释放后附件进入 purgeableResources,新代理再通过 refResource 重新激活同一实例。建议在 Case 2 前保存裸指针并释放 first(如 auto* shared = first.get(); first.reset();),再断言 stencil.get() == shared,以覆盖 purgeable -> 重新激活的完整生命周期(注意 Case 3 的 EXPECT_NE 需改用该裸指针比较)。另 Dispatch_StencilReuseAcrossPasses 全程复用同一 surface,也未触发跨代理共享。

if (stencilAttachment != nullptr) {
// The cached entry must match the requested sample count; otherwise the caller is mixing
// MSAA configurations on the same proxy, which would silently violate the "all attachments
// share one sampleCount" rule that every backend enforces.

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.

sampleCount 不匹配仅由 DEBUG_ASSERT 防护,release 构建下同一代理混用 getStencil(1)/getStencil(4) 会静默返回错误 sampleCount 的附件。契约已在头文件注释中声明且当前调用方不可触发,仅建议加固:release 下返回 nullptr(OpsRenderTask.cpp 已能处理 null 并跳过 stencil op),使契约在 release 下也安全。

@Hparty

Hparty commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

测试盲区 G1(混合 pass 回归):OpsRenderTask 会把同一 flush 内的所有 op 打包进一个 render pass,只要其中有 op needsStencil(),整个 pass 就会挂上 depth-stencil attachment(LoadAction::Clear / StoreAction::DontCare),其余普通 op 照常执行。此时普通 op(StandardDrawOp)的 pipeline 必须声明与 attachment 一致的 depth-stencil format——这正是本 PR 中 StandardDrawOp::bindStandardPipeline 通过 RenderPass::depthStencilFormat() 继承 format、以及 StencilCoverPathDrawOp 的 stencil/cover descriptor 不再硬编码 DEPTH24_STENCIL8 的原因。现有测试只覆盖了纯 stencil 场景(如 Dispatch_StencilReuseAcrossPasses 全程只画 stencil op),没有覆盖"stencil op + 普通 op 共享一个 pass"的组合;在 Metal 上若普通 op 的 pipeline 不声明该 format,会被拒绝创建并崩溃(API validation 报 render pipeline's pixelFormat (MTLPixelFormatInvalid) does not match the framebuffer's pixelFormat)。建议在 Dispatch_StencilReuseAcrossPasses 之后补充以下回归测试:

// Regression for the mixed-pass scenario: a stencil-cover path op and a non-stencil draw op
// sharing one render pass. Both ops are queued in a single flush, so OpsRenderTask attaches a
// depth-stencil texture to the render pass for every op in the task. The plain rect's
// pipeline therefore runs against a pass that carries a depth-stencil attachment, which is
// exactly the case StandardDrawOp::bindStandardPipeline handles by inheriting
// RenderPass::depthStencilFormat() instead of leaving the depth-stencil format unknown, and
// the stencil/cover descriptors follow the same format instead of hardcoding
// DEPTH24_STENCIL8. On Metal a pipeline whose declared depth-stencil format does not match the
// attachment is rejected at creation time, so this combination used to crash before the fix;
// the assertions below pin down that both the stencil-filled pentagon and the plain rect
// still rasterize correctly when they share the pass.
TGFX_TEST(StencilCoverPathTest, Dispatch_StencilCoverAndRectShareRenderPass) {
  ContextScope scope;
  auto context = scope.getContext();
  ASSERT_TRUE(context != nullptr);

  ScopedStencilCoverCaps capsGuard(context, true);

  constexpr int kSize = 64;
  auto surface = Surface::Make(context, kSize, kSize);
  ASSERT_TRUE(surface != nullptr);
  auto canvas = surface->getCanvas();
  canvas->clear(Color{0.f, 0.f, 0.f, 1.f});

  Paint paint;
  paint.setAntiAlias(false);
  paint.setColor(Color{1.f, 0.f, 0.f, 1.f});

  // Op A: a stencil-cover pentagon in the left half. It is a non-rect shape without AA, so it
  // is dispatched through the stencil-and-cover pipeline (pendingStencilCoverShapes).
  Path path = BuildCellPentagon(16, 32);
  path.setFillType(PathFillType::Winding);
  canvas->drawPath(path, paint);

  // Op B: a plain filled rect in the right half, in the same flush so both ops end up in one
  // OpsRenderTask and therefore share one render pass. The rect never touches the stencil
  // buffer, but its pipeline must still declare the pass's depth-stencil format on backends
  // that reject a format-less pipeline against a depth-stencil-bearing pass.
  canvas->drawRect(Rect::MakeXYWH(40, 20, 20, 24), paint);

  context->flushAndSubmit();

  // Inside pentagon A: stencil+cover fill, must be red.
  EXPECT_EQ(ReadRedChannel(surface.get(), 16, 32), 0xFF)
      << "Pentagon interior (stencil op) should be red in the mixed pass";
  // Outside pentagon A (top-left corner): stencil-culled, must stay black.
  EXPECT_EQ(ReadRedChannel(surface.get(), 4, 4), 0x00)
      << "Outside pentagon A should stay black in the mixed pass";
  // Inside rect B: plain fill sharing the stencil-bearing pass, must be red.
  EXPECT_EQ(ReadRedChannel(surface.get(), 50, 32), 0xFF)
      << "Rect interior (non-stencil op) should be red in the mixed pass";
  // Outside everything (bottom-right corner): must stay black.
  EXPECT_EQ(ReadRedChannel(surface.get(), 62, 62), 0x00)
      << "Outside both shapes should stay black in the mixed pass";
}

验证情况:该用例在 OpenGL 与 Metal 均通过;Metal 下 METAL_DEVICE_WRAPPER_TYPE=1 开启 API validation 时,revert 本 PR 的修复 commit 可复现原始崩溃(exit 134,报上述 format 不匹配),恢复修复后通过,确认该测试能真实捕获原始 bug。新增的 Dispatch_StencilCoverAndRectShareRenderPass 用例本身在两个后端均不依赖已有 baseline(纯像素断言),不会受基线缺失影响。

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.

2 participants