Skip to content

perf(pipeline): extend per-frame cross-camera RT lease beyond internal RT (follow-up to #3015) #3056

Description

@GuoLei1990

Background

PR #3015 introduced per-frame cross-camera pool leasing for BasicRenderPipeline._internalColorTarget / _copyBackgroundTexture: each camera acquires the internal RT from RenderTargetPool at the start of render() and returns it at the end, so cameras rendering sequentially in the same frame reuse one RT instead of each holding its own full-screen MSAA target.

This pattern currently covers only the internal color target. Several other pipeline pass RTs are still self-held across frames (kept on the pass instance via recreateRenderTargetIfNeeded, released only when the pass is disabled), so in a multi-camera scene each camera pins its own copy.

This issue tracks evaluating whether to extend the same per-frame lease to those passes.

Memory payoff (4 cameras @ 1920×1080, estimated)

RT ~Size Status
internal RT (full-res MSAA color) ~33 MB ✅ done in #3015 (~47% of total achievable saving)
PostProcess swap / output ~16.6 MB ×2 already per-frame freed (_releaseSwapRenderTarget / _releaseOutputRenderTarget)
FinalPass _srgbRenderTarget ~8.3 MB ⬜ self-held — best follow-up candidate
Bloom mip up/down chain ~85 MB (largest!) ⬜ self-held — high payoff, complex nested lifetimes
DepthOnly RT ~8.3 MB do not convert (see below)
SAO + blur pair ~6–12 MB ⬜ low payoff, needs lifetime re-check

The internal RT is the single biggest line item but only ~47% of the total achievable cross-camera saving — the long tail of full-res non-MSAA targets collectively rivals it. It is not an 80/20 win.

Recommended scope (priority order)

  1. FinalPass _srgbRenderTarget — cleanest: purely internal, never exposed to camera.shaderData, allocated/used/released within pass scope. Lowest risk, ~8 MB.
  2. Bloom mip chain — largest payoff (~85 MB) but nested/overlapping lifetimes; needs a careful release-timing audit before converting.
  3. SAO pair — small payoff; its texture is bound into camera.shaderData (camera_AOTexture) but last sampled in the opaque pass (transparent doesn't read AO), so likely safe to release at render end — needs confirmation that nothing samples it after the proposed release point.

Explicitly out of scope

  • DepthOnly RT must NOT be converted. Its depth texture is published to camera.shaderData via Camera._cameraDepthTextureProperty (DepthOnlyPass.onRender) and that reference outlives render() (accessible to materials/scripts as the camera depth texture). Returning the underlying RT to a shared pool would let the next camera's lease overwrite live depth data → use-after-free-style corruption. Keep it per-camera self-held.

Caveats / risks

  • Each conversion needs a per-RT release-timing audit: the release point must come strictly after the last same-frame sampler of that RT. The internal RT was the easy case (used-then-discarded within render()); the tail RTs have interleaved lifetimes and some publish texture refs into camera.shaderData.
  • No temporal/history dependencies were found in the current pipeline (no TAA/motion-vector history), so cross-frame content reuse is not a blocker today — but any future temporal effect would change this calculus.

Acceptance

  • Convert FinalPass _srgbRenderTarget to per-frame lease (or document why not)
  • Lifetime audit + decision for Bloom mip chain
  • Lifetime audit + decision for SAO pair
  • Confirm DepthOnly stays self-held with a code comment explaining the camera.shaderData lifetime constraint

Follow-up to #3015.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions