Skip to content

Commit ed32e15

Browse files
authored
Merge pull request #3287 from LazyWorksZA/fix/sslr-transparent-attachment-mismatch
fix: Local Reflections causes Vulkan device loss (surplus MRT targets bound in transparent stage)
2 parents 18786dd + 1ad580a commit ed32e15

5 files changed

Lines changed: 164 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
4+
using Xunit;
5+
6+
using Stride.Graphics;
7+
using Stride.Graphics.Regression;
8+
using Stride.Rendering;
9+
using Stride.Rendering.Compositing;
10+
11+
namespace Stride.Engine.Tests;
12+
13+
public class ForwardRendererTransparentTargetsTest : GameTestBase
14+
{
15+
/// <summary>
16+
/// Verifies the ForwardRenderer binds only as many render targets as the transparent stage declares it
17+
/// outputs, dropping surplus opaque MRT targets (e.g. those Local Reflections adds). See #3251.
18+
/// </summary>
19+
[Fact]
20+
public void BindsOnlyTransparentStageDeclaredTargets()
21+
{
22+
PerformDrawTest((game, context) =>
23+
{
24+
var device = game.GraphicsDevice;
25+
var commandList = context.CommandList;
26+
27+
var transparentStage = new RenderStage("Transparent", "Main")
28+
{
29+
Output = new RenderOutputDescription(PixelFormat.R8G8B8A8_UNorm, PixelFormat.D24_UNorm_S8_UInt),
30+
};
31+
var forwardRenderer = new ForwardRenderer { TransparentRenderStage = transparentStage };
32+
33+
// Formats are irrelevant here (only the target count matters); use a widely supported one.
34+
var color = Texture.New2D(device, 16, 16, PixelFormat.R8G8B8A8_UNorm, TextureFlags.RenderTarget);
35+
var normal = Texture.New2D(device, 16, 16, PixelFormat.R8G8B8A8_UNorm, TextureFlags.RenderTarget);
36+
var specular = Texture.New2D(device, 16, 16, PixelFormat.R8G8B8A8_UNorm, TextureFlags.RenderTarget);
37+
var depth = Texture.New2D(device, 16, 16, PixelFormat.D24_UNorm_S8_UInt, TextureFlags.DepthStencil);
38+
39+
// Opaque stage left 3 color targets + depth bound, as SSLR would.
40+
commandList.SetRenderTargets(depth, color, normal, specular);
41+
Assert.Equal(3, commandList.RenderTargetCount);
42+
43+
forwardRenderer.SetTransparentStageRenderTargets(context);
44+
45+
Assert.Equal(1, commandList.RenderTargetCount);
46+
Assert.Equal(color, commandList.RenderTargets[0]);
47+
Assert.Equal(depth, commandList.DepthStencilBuffer);
48+
49+
// Already matching the declared count: no change.
50+
forwardRenderer.SetTransparentStageRenderTargets(context);
51+
Assert.Equal(1, commandList.RenderTargetCount);
52+
53+
color.Dispose();
54+
normal.Dispose();
55+
specular.Dispose();
56+
depth.Dispose();
57+
}, takeSnapshot: false);
58+
}
59+
}

sources/engine/Stride.Engine.Tests/Stride.Engine.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<Compile Include="XunitAttributes.cs" />
2323
<Compile Include="Build\TestBuilder.cs" />
2424
<Compile Include="EngineTestBase.cs" />
25+
<Compile Include="ForwardRendererTransparentTargetsTest.cs" />
2526
<Compile Include="ParameterCollectionUpdateEngineTest.cs" />
2627
<Compile Include="EntityUpdateEngineTest.cs" />
2728
<Compile Include="AnimatedModelTests.cs" />

sources/engine/Stride.Engine/Rendering/Compositing/ForwardRenderer.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,8 @@ protected virtual void DrawView(RenderContext context, RenderDrawContext drawCon
555555

556556
var renderTargetSRV = ResolveRenderTargetAsSRV(drawContext);
557557

558+
SetTransparentStageRenderTargets(drawContext);
559+
558560
renderSystem.Draw(drawContext, context.RenderView, TransparentRenderStage);
559561

560562
Context.Allocator.ReleaseReference(renderTargetSRV);
@@ -807,6 +809,21 @@ private Texture ResolveDepthAsSRV(RenderDrawContext context)
807809
return depthStencilSRV;
808810
}
809811

812+
/// <summary>
813+
/// Binds only the render targets the transparent stage outputs, dropping any surplus opaque targets left
814+
/// bound by post-effects (e.g. Local Reflections) so the framebuffer matches the transparent render pass.
815+
/// </summary>
816+
internal void SetTransparentStageRenderTargets(RenderDrawContext drawContext)
817+
{
818+
if (TransparentRenderStage == null)
819+
return;
820+
821+
var commandList = drawContext.CommandList;
822+
var declaredCount = TransparentRenderStage.Output.RenderTargetCount;
823+
if (declaredCount >= 1 && commandList.RenderTargetCount > declaredCount)
824+
commandList.SetRenderTargets(commandList.DepthStencilBuffer, commandList.RenderTargets.Slice(0, declaredCount));
825+
}
826+
810827
private Texture ResolveRenderTargetAsSRV(RenderDrawContext drawContext)
811828
{
812829
if (!BindOpaqueAsResourceDuringTransparentRendering)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
4+
using System;
5+
6+
using Xunit;
7+
8+
using Stride.Core.Mathematics;
9+
using Stride.Rendering;
10+
11+
namespace Stride.Graphics.Tests;
12+
13+
public class TestFramebufferAttachmentGuard : GraphicTestGameBase
14+
{
15+
private struct Vertex
16+
{
17+
public Vector3 Position;
18+
public Vector2 TexCoords;
19+
}
20+
21+
/// <summary>
22+
/// The Vulkan backend's debug guard turns a framebuffer/render-pass attachment-count mismatch into a clear
23+
/// exception instead of a device loss. Verify it fires when more render targets are bound than the active
24+
/// pipeline's render pass declares.
25+
/// </summary>
26+
[SkippableFact]
27+
public void ThrowsWhenBoundTargetsExceedPipelineRenderPass()
28+
{
29+
PerformTest(game =>
30+
{
31+
// The guard lives in the Vulkan backend (and needs the debug device, which GraphicTestGameBase sets).
32+
Skip.IfNot(GraphicsDevice.Platform == GraphicsPlatform.Vulkan, "Attachment guard is Vulkan-only.");
33+
34+
var device = game.GraphicsDevice;
35+
var commandList = game.GraphicsContext.CommandList;
36+
37+
var backBuffer = device.Presenter.BackBuffer;
38+
var depth = device.Presenter.DepthStencilBuffer;
39+
var extraTarget = Texture.New2D(device, backBuffer.Width, backBuffer.Height, backBuffer.Format, TextureFlags.RenderTarget);
40+
41+
var declaration = new VertexDeclaration(VertexElement.Position<Vector3>(), VertexElement.TextureCoordinate<Vector2>());
42+
var vertexBuffer = Buffer.Vertex.New(device, new Vertex[3], GraphicsResourceUsage.Default);
43+
var sampledTexture = Texture.New2D(device, 4, 4, PixelFormat.R8G8B8A8_UNorm, TextureFlags.ShaderResource);
44+
45+
var effect = new EffectInstance(new Effect(device, SpriteEffect.Bytecode));
46+
effect.Parameters.Set(TexturingKeys.Texture0, sampledTexture);
47+
effect.Parameters.Set(TexturingKeys.Sampler, device.SamplerStates.LinearClamp);
48+
effect.UpdateEffect(device);
49+
50+
var pipelineState = new MutablePipelineState(device);
51+
pipelineState.State.SetDefaults();
52+
pipelineState.State.RootSignature = effect.RootSignature;
53+
pipelineState.State.EffectBytecode = effect.Effect.Bytecode;
54+
pipelineState.State.InputElements = declaration.CreateInputElements();
55+
pipelineState.State.PrimitiveType = PrimitiveType.TriangleList;
56+
57+
// Capture the pipeline Output with a single color + depth bound: its render pass declares 2 attachments.
58+
commandList.SetRenderTargetAndViewport(depth, backBuffer);
59+
pipelineState.State.Output.CaptureState(commandList);
60+
pipelineState.Update();
61+
62+
// Now bind two color targets + depth: the framebuffer would have 3 attachments, mismatching the pass.
63+
commandList.SetRenderTargets(depth, backBuffer, extraTarget);
64+
commandList.SetPipelineState(pipelineState.CurrentState);
65+
commandList.SetVertexBuffer(0, vertexBuffer, 0, declaration.VertexStride);
66+
effect.Apply(game.GraphicsContext);
67+
68+
var exception = Assert.Throws<InvalidOperationException>(() => commandList.Draw(3));
69+
Assert.Contains("render pass", exception.Message);
70+
71+
sampledTexture.Dispose();
72+
extraTarget.Dispose();
73+
});
74+
}
75+
}

sources/engine/Stride.Graphics/Vulkan/CommandList.Vulkan.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,18 @@ private unsafe void EnsureRenderPass()
17601760

17611761
if (framebufferDirty)
17621762
{
1763+
// A framebuffer/render-pass attachment mismatch is undefined behavior that loses the device on
1764+
// strict drivers; fail loud in debug (only, to not break drivers that tolerate it) instead.
1765+
if (GraphicsDevice.IsDebugMode)
1766+
{
1767+
var output = activePipeline.Description.Output;
1768+
var expectedAttachmentCount = output.RenderTargetCount + (output.DepthStencilFormat != PixelFormat.None ? 1 : 0);
1769+
if (framebufferAttachmentCount != expectedAttachmentCount)
1770+
throw new InvalidOperationException(
1771+
$"Bound render targets ({framebufferAttachmentCount}) do not match the active pipeline's render pass " +
1772+
$"({expectedAttachmentCount} attachments). The render targets bound on the command list must match the pipeline's Output description.");
1773+
}
1774+
17631775
// Create new frame buffer
17641776
fixed (VkImageView* attachmentsPointer = &framebufferAttachments[0])
17651777
{

0 commit comments

Comments
 (0)