Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions build/Stride.sln
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stride.Build.Sdk.Tests", "S
..\sources\sdk\Stride.Build.Sdk.Tests\Sdk\Sdk.targets = ..\sources\sdk\Stride.Build.Sdk.Tests\Sdk\Sdk.targets
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.Rendering.Tests", "..\sources\engine\Stride.Rendering.Tests\Stride.Rendering.Tests.csproj", "{42723C4C-D3CC-45B1-9AA9-625C17574E02}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1530,6 +1532,18 @@ Global
{35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Win32.ActiveCfg = Release|Any CPU
{35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Win32.Build.0 = Release|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Debug|Win32.ActiveCfg = Debug|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Debug|Win32.Build.0 = Debug|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Release|Any CPU.Build.0 = Release|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Release|Win32.ActiveCfg = Release|Any CPU
{42723C4C-D3CC-45B1-9AA9-625C17574E02}.Release|Win32.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1659,6 +1673,7 @@ Global
{D26186F8-7158-4A01-9524-EF4F53E0802C} = {0B81090E-4066-4723-A658-8AEDBEADE619}
{8D873BE7-8EF2-4478-B86A-249021D046EB} = {0B81090E-4066-4723-A658-8AEDBEADE619}
{E6B11A34-A1DB-41C2-B509-94DACA9D9BDE} = {0B81090E-4066-4723-A658-8AEDBEADE619}
{42723C4C-D3CC-45B1-9AA9-625C17574E02} = {A7ED9F01-7D78-4381-90A6-D50E51C17250}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FF877973-604D-4EA7-B5F5-A129961F9EF2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ RenderStages:
Name: GBuffer
EffectSlotName: GBuffer
SortMode: !FrontToBackSortMode {}
5b5e4f7ee8b5497f9d3b418d06823b14:
Id: 5b5e4f7e-e8b5-497f-9d3b-418d06823b14
Name: UIRenderStage
EffectSlotName: UI
SortMode: !BackToFrontSortMode {}
RenderFeatures:
d8fb80b0e7995140a46bca8dc36ee8a2: !Stride.Rendering.MeshRenderFeature,Stride.Rendering
RenderStageSelectors:
Expand Down Expand Up @@ -106,7 +111,7 @@ RenderFeatures:
93933ad00d0c357d4915ad462cbfd04c: !Stride.Rendering.UI.UIRenderFeature,Stride.UI
RenderStageSelectors:
14a071694411235038a102ac3794bb4d: !Stride.Rendering.SimpleGroupToRenderStageSelector,Stride.Rendering
RenderStage: ref!! 0fa30591-02ee-486d-9347-2b6aee83d035
RenderStage: ref!! 5b5e4f7e-e8b5-497f-9d3b-418d06823b14
EffectName: Test
9013eab3ea0ef6c98bf133b86c173d45: !Stride.Particles.Rendering.ParticleEmitterRenderFeature,Stride.Particles
RenderStageSelectors:
Expand All @@ -129,6 +134,7 @@ SharedRenderers:
87ff1d9cdd52418daf76385176a0e316: ref!! 555e84b4-b68a-4f38-ac3a-f0f563028ef0
5e059d4cc2db4ee8a1f28a40f4ac3ae8: ref!! b03a45c6-7a56-417c-8a80-69cc608671f1
GBufferRenderStage: ref!! ecab139e-5f55-42b5-a324-310c195a9c89
AfterPostEffectsRenderStage: ref!! 5b5e4f7e-e8b5-497f-9d3b-418d06823b14
PostEffects: !PostProcessingEffects ref!! 608bf04d-8640-469d-b710-1ecf6a46f794
LightShafts: null
VRSettings:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ RenderStages:
Name: ShadowMapCaster
EffectSlotName: ShadowMapCaster
SortMode: !FrontToBackSortMode {}
62af1128cc9c49a09d813f7f598a4b16:
Id: 62af1128-cc9c-49a0-9d81-3f7f598a4b16
Name: UIRenderStage
EffectSlotName: UI
SortMode: !BackToFrontSortMode {}
RenderFeatures:
d8fb80b0e7995140a46bca8dc36ee8a2: !Stride.Rendering.MeshRenderFeature,Stride.Rendering
RenderStageSelectors:
Expand Down Expand Up @@ -64,7 +69,7 @@ RenderFeatures:
93933ad00d0c357d4915ad462cbfd04c: !Stride.Rendering.UI.UIRenderFeature,Stride.UI
RenderStageSelectors:
14a071694411235038a102ac3794bb4d: !Stride.Rendering.SimpleGroupToRenderStageSelector,Stride.Rendering
RenderStage: ref!! 0fa30591-02ee-486d-9347-2b6aee83d035
RenderStage: ref!! 62af1128-cc9c-49a0-9d81-3f7f598a4b16
EffectName: Test
9013eab3ea0ef6c98bf133b86c173d45: !Stride.Particles.Rendering.ParticleEmitterRenderFeature,Stride.Particles
RenderStageSelectors:
Expand All @@ -85,6 +90,7 @@ SharedRenderers:
ShadowMapRenderStages:
fc4d1e0de5c2b0bbc27bcf96e9a848fd: ref!! c0524e55-4061-464d-84dd-7c4c70f70e0e
GBufferRenderStage: null
AfterPostEffectsRenderStage: ref!! 62af1128-cc9c-49a0-9d81-3f7f598a4b16
PostEffects: null
LightShafts: null
VRSettings:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ public partial class ForwardRenderer : SceneRendererBase, ISharedRenderer
/// </summary>
public RenderStage TransparentRenderStage { get; set; }

/// <summary>
/// A render stage drawn directly onto <see cref="viewOutputTarget"/> after
/// <see cref="PostEffects"/> have resolved. Geometry assigned to this stage is
/// never tone-mapped, bloomed, or otherwise affected by post-processing effects,
/// making it suitable for HUD and screen-space UI.
/// Leave null (the default) when no post-FX-immune UI is required.
/// </summary>
public RenderStage AfterPostEffectsRenderStage { get; set; }

/// <summary>
/// The shadow map render stages for shadow casters. No shadow rendering will happen if null.
/// </summary>
Expand Down Expand Up @@ -292,6 +301,11 @@ protected virtual void CollectView(RenderContext context)
{
context.RenderView.RenderStages.Add(GBufferRenderStage);
}

if (AfterPostEffectsRenderStage != null)
{
context.RenderView.RenderStages.Add(AfterPostEffectsRenderStage);
}
}

protected override unsafe void CollectCore(RenderContext context)
Expand Down Expand Up @@ -610,6 +624,18 @@ protected virtual void DrawView(RenderContext context, RenderDrawContext drawCon
}
}

// Draw UI directly onto the resolved output target, after post-processing.
// This stage is never tone-mapped or bloomed — UI stays crisp regardless
// of which PostEffects are active.
if (AfterPostEffectsRenderStage != null)
{
using (drawContext.PushRenderTargetsAndRestore())
{
drawContext.CommandList.SetRenderTarget(viewDepthStencil, viewOutputTarget);
renderSystem.Draw(drawContext, context.RenderView, AfterPostEffectsRenderStage);
}
}

// Free the depth texture since we won't need it anymore
if (depthStencilSRV != null)
{
Expand Down
104 changes: 104 additions & 0 deletions sources/engine/Stride.Rendering.Tests/ForwardRenderCompositorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Linq;
using Stride.Rendering;
using Stride.Rendering.Compositing;
using Stride.Rendering.UI;
using Xunit;
using Xunit.Abstractions;

namespace Stride.Rendering.Tests.Compositing
{
public class ForwardRendererCompositorTests
{
private readonly ITestOutputHelper _output;

public ForwardRendererCompositorTests(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public void ForwardRenderer_PostEffectsUIRenderStage_ExistsAndIsDrawnAfterPostFx()
{
var renderer = new ForwardRenderer();
var prop = renderer.GetType().GetProperty("PostEffectsUIRenderStage");
Assert.NotNull(prop);
// Assign and read back
var stage = new RenderStage("UIStage", "UI");
prop.SetValue(renderer, stage);
Assert.Same(stage, prop.GetValue(renderer));
}

[Fact]
public void UIRenderFeature_HasRenderStageSelectors_Collection()
{
var feature = new UIRenderFeature();

Assert.NotNull(feature.RenderStageSelectors);
}

[Fact]
public void UIRenderFeature_CanAddGroup31SelectorForUiStage()
{
var uiStage = new RenderStage("UiStage", "UI");
var feature = new UIRenderFeature();

feature.RenderStageSelectors.Add(new SimpleGroupToRenderStageSelector
{
RenderGroup = RenderGroupMask.Group31,
RenderStage = uiStage,
});

var selector = feature.RenderStageSelectors
.OfType<SimpleGroupToRenderStageSelector>()
.FirstOrDefault(s => s.RenderGroup == RenderGroupMask.Group31);

Assert.NotNull(selector);
Assert.Same(uiStage, selector.RenderStage);
}

[Fact]
public void IsAlreadyPatched_ReturnsFalse_WhenUiStageAbsent()
{
var compositor = new GraphicsCompositor();

var hasUiStage = compositor.RenderStages
.Any(s => s.Name == "UiStage");

Assert.False(hasUiStage);
}

[Fact]
public void IsAlreadyPatched_ReturnsTrue_WhenStageAndSelectorPresent()
{
var compositor = new GraphicsCompositor();
var uiStage = new RenderStage("UiStage", "UI");
compositor.RenderStages.Add(uiStage);

var feature = new UIRenderFeature();
feature.RenderStageSelectors.Add(new SimpleGroupToRenderStageSelector
{
RenderGroup = RenderGroupMask.Group31,
RenderStage = uiStage,
});
compositor.RenderFeatures.Add(feature);

var hasStage = compositor.RenderStages
.Any(s => s.Name == "UiStage");

var hasSelector = compositor.RenderFeatures
.OfType<UIRenderFeature>()
.SelectMany(f => f.RenderStageSelectors.OfType<SimpleGroupToRenderStageSelector>())
.Any(s => s.RenderGroup == RenderGroupMask.Group31
&& s.RenderStage?.Name == "UiStage");

Assert.True(hasStage);
Assert.True(hasSelector);
}

// Minimal stand-in; the real UIComponent requires engine services to construct.
private sealed class FakeUIComponent
{
public RenderGroup RenderGroup { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), 'Directory.Build.props'))/sdk/Stride.Build.Sdk.Tests/Sdk/Sdk.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<AssemblyName>Stride.Rendering.Tests</AssemblyName>
<RootNamespace>Stride.Rendering.Tests</RootNamespace>
<StrideCompileAssets>false</StrideCompileAssets>
<StrideGraphicsApiDependent>false</StrideGraphicsApiDependent>
<StrideGraphicsRegression>false</StrideGraphicsRegression>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\tests\xunit.runner.stride\xunit.runner.stride.csproj" />
<ProjectReference Include="..\Stride.Engine\Stride.Engine.csproj" />
<ProjectReference Include="..\Stride.Rendering\Stride.Rendering.csproj" />
<ProjectReference Include="..\Stride.UI\Stride.UI.csproj" />
</ItemGroup>
<Import Project="$(StrideRoot)sources/sdk/Stride.Build.Sdk.Tests/Sdk/Sdk.targets" />
</Project>