refactor(srp): align urp renderer feature ownership model

This commit is contained in:
2026-04-21 20:17:08 +08:00
parent 0d0919b276
commit e527ca4e3a
6 changed files with 201 additions and 76 deletions

View File

@@ -0,0 +1,103 @@
# SRP URP Renderer Feature Authoring Model Plan
Date: 2026-04-21
## Background
The last few SRP stages already cleaned up the following seams:
- shared native backend substrate ownership
- renderer feature lifecycle invalidation
- renderer feature runtime-state synchronization
However, the managed URP side still has one architecture mismatch against the
Unity-style direction:
- `ScriptableRendererData` still exposes two feature ownership paths:
- persistent configured field: `rendererFeatures`
- runtime factory seam: `CreateRendererFeatures()`
That dual model makes the current design harder to reason about:
- the renderer-data asset is not the single source of truth for feature config
- feature lifetime semantics are blurred between "configured object" and
"runtime-created helper"
- project probes and managed probes still encode a non-Unity authoring style
- future editor integration and SRP asset editing will inherit this ambiguity
If the engine is meant to converge toward a Unity-style `SRP + URP` model,
`ScriptableRendererData` should own a stable set of feature configuration
objects, and runtime invalidation should rebuild renderer runtime state from
that configured list rather than from an alternate feature factory seam.
## Goal
Make the managed URP renderer feature model closer to Unity:
- `rendererFeatures` becomes the single configured source of truth
- `ScriptableRendererData` no longer creates an alternate feature collection via
`CreateRendererFeatures()`
- renderer runtime rebuild continues to work by snapshotting and releasing the
configured feature instances correctly
- `ScriptableRendererFeature` is modeled as a managed engine object rather than
a plain helper class
- managed probes and project probes are migrated to explicit feature
configuration instead of runtime feature factory overrides
## Why Now
This is the right point to do it because:
- the lifecycle and dirty/version chain is already stable enough
- the next SRP stages need a clearer asset/config model, not more invalidation
patches
- future renderer authoring, editor exposure, and custom URP feature workflows
all depend on a clean ownership model
If this seam stays open, later SRP work will keep mixing:
- asset configuration logic
- runtime cache rebuild logic
- probe-only factory patterns
That would move the codebase away from the Unity-style architecture the project
is aiming for.
## Scope
This stage stays focused on managed SRP/URP ownership cleanup.
Included:
- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererData.cs`
- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs`
- affected managed probes under `managed/GameScripts/RenderPipelineApiProbe.cs`
- affected project probes under `project/Assets/Scripts/ProjectRenderPipelineProbe.cs`
Not included:
- deferred rendering
- native render backend changes
- new editor work
## Implementation Plan
1. Collapse renderer feature ownership onto `rendererFeatures`
2. Remove the `CreateRendererFeatures()` seam from `ScriptableRendererData`
3. Keep runtime rebuild safe by retaining the last runtime-bound feature
snapshot for disposal
4. Make `ScriptableRendererFeature` inherit from managed engine `Object`
5. Migrate probes to constructor/config-field based feature assignment
6. Rebuild `XCEditor`
7. Run old editor smoke for at least 10 seconds and verify a fresh
`SceneReady`
8. Archive the plan, commit, and push
## Expected Result
After this stage:
- renderer data is the authoritative owner of renderer feature configuration
- runtime invalidation semantics remain intact but are simpler to reason about
- custom URP-style feature authoring is closer to the Unity mental model
- the next SRP stages can build on a cleaner managed asset/config substrate

View File

@@ -521,19 +521,20 @@ namespace Gameplay
internal sealed class ManagedFeaturePassOrderProbeRendererData
: ProbeRendererData
{
protected override ScriptableRenderer CreateProbeRenderer()
public ManagedFeaturePassOrderProbeRendererData()
: base(false)
{
return new ManagedFeaturePassOrderProbeRenderer();
}
protected override ScriptableRendererFeature[] CreateRendererFeatures()
{
return new ScriptableRendererFeature[]
rendererFeatures = new ScriptableRendererFeature[]
{
new ManagedFeaturePassOrderCustomFeature("CustomA"),
new ManagedFeaturePassOrderCustomFeature("CustomB")
};
}
protected override ScriptableRenderer CreateProbeRenderer()
{
return new ManagedFeaturePassOrderProbeRenderer();
}
}
internal sealed class CameraDataObservationPass
@@ -978,6 +979,16 @@ namespace Gameplay
internal abstract class ProbeRendererData
: UniversalRendererData
{
protected ProbeRendererData()
{
}
protected ProbeRendererData(
bool initializeDefaultRendererFeatures)
: base(initializeDefaultRendererFeatures)
{
}
protected sealed override ScriptableRenderer CreateRenderer()
{
return CreateProbeRenderer();
@@ -1111,6 +1122,16 @@ namespace Gameplay
internal sealed class ManagedRendererInvalidationProbeRendererData
: ProbeRendererData
{
public ManagedRendererInvalidationProbeRendererData()
: base(false)
{
ManagedRendererInvalidationProbeState.CreateFeatureCallCount++;
rendererFeatures = new ScriptableRendererFeature[]
{
new ManagedRendererInvalidationProbeFeature()
};
}
protected override ScriptableRenderer CreateProbeRenderer()
{
ManagedRendererInvalidationProbeState.CreateRendererCallCount++;
@@ -1124,15 +1145,6 @@ namespace Gameplay
base.SetupRenderer(renderer);
}
protected override ScriptableRendererFeature[] CreateRendererFeatures()
{
ManagedRendererInvalidationProbeState.CreateFeatureCallCount++;
return new ScriptableRendererFeature[]
{
new ManagedRendererInvalidationProbeFeature()
};
}
public void InvalidateForTest()
{
ManagedRendererInvalidationProbeState.InvalidateRendererCallCount++;
@@ -1187,8 +1199,18 @@ namespace Gameplay
internal sealed class ManagedPersistentFeatureProbeRendererData
: ProbeRendererData
{
private readonly ManagedPersistentFeatureProbeRendererFeature m_feature =
new ManagedPersistentFeatureProbeRendererFeature();
private readonly ManagedPersistentFeatureProbeRendererFeature m_feature;
public ManagedPersistentFeatureProbeRendererData()
: base(false)
{
m_feature =
new ManagedPersistentFeatureProbeRendererFeature();
rendererFeatures = new ScriptableRendererFeature[]
{
m_feature
};
}
protected override ScriptableRenderer CreateProbeRenderer()
{
@@ -1197,14 +1219,6 @@ namespace Gameplay
return new ManagedPersistentFeatureProbeRenderer();
}
protected override ScriptableRendererFeature[] CreateRendererFeatures()
{
return new ScriptableRendererFeature[]
{
m_feature
};
}
public void InvalidateForTest()
{
ManagedPersistentFeatureProbeState
@@ -1243,18 +1257,19 @@ namespace Gameplay
internal sealed class ManagedFeaturePlannedPostProcessRendererData
: ProbeRendererData
{
protected override ScriptableRenderer CreateProbeRenderer()
public ManagedFeaturePlannedPostProcessRendererData()
: base(false)
{
return new ProbeSceneRenderer();
}
protected override ScriptableRendererFeature[] CreateRendererFeatures()
{
return new ScriptableRendererFeature[]
rendererFeatures = new ScriptableRendererFeature[]
{
new ManagedFeaturePlannedPostProcessRendererFeature()
};
}
protected override ScriptableRenderer CreateProbeRenderer()
{
return new ProbeSceneRenderer();
}
}
internal sealed class ManagedRenderContextCameraDataProbeRendererData
@@ -1335,6 +1350,16 @@ namespace Gameplay
internal sealed class ManagedLifecycleProbeRendererData
: ProbeRendererData
{
public ManagedLifecycleProbeRendererData()
: base(false)
{
ManagedLifecycleProbeState.CreateFeatureCallCount++;
rendererFeatures = new ScriptableRendererFeature[]
{
new ManagedLifecycleProbeRendererFeature()
};
}
protected override ScriptableRenderer CreateProbeRenderer()
{
ManagedLifecycleProbeState.CreateRendererCallCount++;
@@ -1348,15 +1373,6 @@ namespace Gameplay
base.SetupRenderer(renderer);
}
protected override ScriptableRendererFeature[] CreateRendererFeatures()
{
ManagedLifecycleProbeState.CreateFeatureCallCount++;
return new ScriptableRendererFeature[]
{
new ManagedLifecycleProbeRendererFeature()
};
}
protected override void ReleaseRuntimeResources()
{
ManagedLifecycleProbeState.ReleaseRendererDataRuntimeResourcesCallCount++;

View File

@@ -41,11 +41,6 @@ namespace XCEngine.Rendering.Universal
return m_rendererInstance;
}
internal ScriptableRendererFeature[] CreateRendererFeaturesInstance()
{
return GetRendererFeatures();
}
internal void SetupRendererInstance(
ScriptableRenderer renderer)
{
@@ -245,12 +240,6 @@ namespace XCEngine.Rendering.Universal
{
}
protected virtual ScriptableRendererFeature[] CreateRendererFeatures()
{
return rendererFeatures ??
Array.Empty<ScriptableRendererFeature>();
}
protected virtual void ReleaseRuntimeResources()
{
}
@@ -342,15 +331,21 @@ namespace XCEngine.Rendering.Universal
rendererFeatures =
CreateDefaultRendererFeatures() ??
Array.Empty<ScriptableRendererFeature>();
BindRendererFeatureOwners(rendererFeatures);
}
private ScriptableRendererFeature[] GetRendererFeatures()
{
if (m_rendererFeatures == null)
{
ScriptableRendererFeature[]
configuredRendererFeatures =
rendererFeatures ??
Array.Empty<ScriptableRendererFeature>();
rendererFeatures =
configuredRendererFeatures;
m_rendererFeatures =
CreateRendererFeatures() ??
Array.Empty<ScriptableRendererFeature>();
configuredRendererFeatures;
}
BindRendererFeatureOwners(m_rendererFeatures);

View File

@@ -4,6 +4,7 @@ using XCEngine.Rendering;
namespace XCEngine.Rendering.Universal
{
public abstract class ScriptableRendererFeature
: Object
{
private bool m_disposed;
private bool m_runtimeCreated;

View File

@@ -11,11 +11,20 @@ namespace XCEngine.Rendering.Universal
public DepthPrepassBlockData depthPrepass;
public UniversalRendererData()
: this(true)
{
}
protected UniversalRendererData(
bool initializeDefaultRendererFeatures)
{
mainScene = UniversalMainSceneData.CreateDefault();
shadowCaster = ShadowCasterBlockData.CreateDefault();
depthPrepass = DepthPrepassBlockData.CreateDefault();
ResetRendererFeaturesToDefault();
if (initializeDefaultRendererFeatures)
{
ResetRendererFeaturesToDefault();
}
}
protected override ScriptableRenderer CreateRenderer()

View File

@@ -302,6 +302,17 @@ namespace ProjectScripts
public sealed class ProjectRendererInvalidationProbeRendererData
: UniversalRendererData
{
public ProjectRendererInvalidationProbeRendererData()
: base(false)
{
ProjectRendererInvalidationProbeState
.CreateFeatureCallCount++;
rendererFeatures = new ScriptableRendererFeature[]
{
new ProjectRendererInvalidationProbeFeature()
};
}
protected override ScriptableRenderer CreateRenderer()
{
ProjectRendererInvalidationProbeState
@@ -317,17 +328,6 @@ namespace ProjectScripts
base.SetupRenderer(renderer);
}
protected override ScriptableRendererFeature[]
CreateRendererFeatures()
{
ProjectRendererInvalidationProbeState
.CreateFeatureCallCount++;
return new ScriptableRendererFeature[]
{
new ProjectRendererInvalidationProbeFeature()
};
}
public void InvalidateForTest()
{
ProjectRendererInvalidationProbeState
@@ -432,8 +432,18 @@ namespace ProjectScripts
: UniversalRendererData
{
private readonly ProjectPersistentFeatureProbeRendererFeature
m_feature;
public ProjectPersistentFeatureProbeRendererData()
: base(false)
{
m_feature =
new ProjectPersistentFeatureProbeRendererFeature();
rendererFeatures = new ScriptableRendererFeature[]
{
m_feature
};
}
protected override ScriptableRenderer CreateRenderer()
{
@@ -442,15 +452,6 @@ namespace ProjectScripts
return new ProjectPersistentFeatureProbeRenderer();
}
protected override ScriptableRendererFeature[]
CreateRendererFeatures()
{
return new ScriptableRendererFeature[]
{
m_feature
};
}
public void InvalidateForTest()
{
ProjectPersistentFeatureProbeState