Skip to content

Bugfix: Exclude PostFx from UI by default (Issue #1154)#3128

Open
yuechen-li-dev wants to merge 8 commits intostride3d:masterfrom
yuechen-li-dev:RenderTest
Open

Bugfix: Exclude PostFx from UI by default (Issue #1154)#3128
yuechen-li-dev wants to merge 8 commits intostride3d:masterfrom
yuechen-li-dev:RenderTest

Conversation

@yuechen-li-dev
Copy link
Copy Markdown

PR Details

The blurry, washed out UI stemming from the PostFx being applied on UI elements has been a bug since 2021, and the RenderGroup 31 bypass is well known, and I've even made a script that is specifically designed to work around it.

So, I've decided to fix it today.

Details:

ForwardRenderer.cs
Added a PostEffectsUIRenderStage property. When set, this stage is drawn onto the final resolved output target after PostEffects has already run, meaning UI geometry assigned to it is never touched by tone-mapping, bloom, or any other post-processing effect.

DefaultGraphicsCompositorLevel10/9.sdgfxcomp
Added UIRenderStage as a new render stage and pointed the UIRenderFeature selector at it instead of Transparent. Wired PostEffectsUIRenderStage on the ForwardRenderer to this new stage. The result is that all UI renders after post-processing by default, with no configuration required from the user.

Added a separate unit test in new group: Stride.Rendering.Tests to guard against regression.

Related Issue

Fixes #1154

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

Copy link
Copy Markdown
Contributor

@Ethereal77 Ethereal77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, but a few comments:

  • If this PR is a proper fix that improves the defaults to not need the post-FX Clean UI "hack", why mention it all the time in comments and tests? Just do your thing and verify it is working!

  • Naming is ambiguous in my opinion. Look at the comment I left.

  • Some of the comments and logs seem like a WIP. Maybe a little cleanup is needed.

I'll massage the tests a bit to check the rendering is working and the render stage is being invoked in the correct order, excluding any details or specific behavior that may come from the Clean UI patch. However were it implemented it were a "hacky" solution. If this PR is the proper fix, forget about that solution.

Comment thread build/Stride.sln
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR should not touch the solution file

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new xunit test project was added to the solution, which is why the .sln file was changed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I missed that. Sorry

Comment on lines +1 to +5
// ForwardRendererCompositorTests.cs
// Place at: sources/engine/Stride.Rendering.Tests/Compositing/ForwardRendererCompositorTests.cs
//
// Structural tests confirming the UI-before-PostFx ordering bug.
// No GPU required — pure object-graph assertions.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these comments?

Comment on lines +37 to +41
// ----------------------------------------------------------------
// 2. REGRESSION GUARD (currently expected to fail — uncomment after fix):
// Once the fix is in, ForwardRenderer should expose a
// PostEffectsUIRenderStage property that is drawn AFTER PostEffects.
// ----------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like in-progress comments. Can be removed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment on lines +54 to +58
// ----------------------------------------------------------------
// 3. CONFIRM: UIRenderFeature inherits RenderStageSelectors from
// RootRenderFeature — the collection used by CleanUiPostFxPatch
// to route Group31 to a separate stage.
// ----------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above


Assert.NotNull(feature.RenderStageSelectors);

_output.WriteLine($"UIRenderFeature.RenderStageSelectors is present (type: {feature.RenderStageSelectors.GetType().Name}).");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit weird to write to the console inside a test. Imho tests should not talk, just verify things work as intended

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. However, I'd like to note that writing console output inside unit tests does significantly reduce tendency for LLMs to hallucinate while generating code, in case Stride does decide to use AI coding more extensively in the future.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to add comments inside the tests detailing what they are trying to test than to write the results to the console.

However, it hadn’t occurred to me that LLMs could understand better the context from console output. Makes sense, I have to try it 😁


Assert.True(hasStage);
Assert.True(hasSelector);
_output.WriteLine("IsAlreadyPatched: correctly returns true when stage and selector both exist.");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: logging in tests

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.

Comment on lines +146 to +152
// ----------------------------------------------------------------
// 7. DOCUMENT: The null-conditional assignment bug in the original
// CleanUiPostFxPatch.MoveUiRecursive.
// `ui?.RenderGroup = value` is a compile error in C# — it cannot
// assign through a null-conditional. The correct form is guarded
// with an explicit null check.
// ----------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment on lines +159 to +164
// WRONG (compile error if used on real UIComponent):
// fakeUi?.RenderGroup = RenderGroup.Group31;
//
// CORRECT:
if (fakeUi != null)
fakeUi.RenderGroup = RenderGroup.Group31;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this wrong? In theory, C# 14 allows null-conditional assignments, so both should be equivalent, no?

Comment on lines +167 to +168
_output.WriteLine("Null-conditional assignment bug documented.");
_output.WriteLine(" Correct pattern: if (ui != null) ui.RenderGroup = RenderGroup.Group31;");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: logging in tests

/// making it suitable for HUD and screen-space UI.
/// Leave null (the default) when no post-FX-immune UI is required.
/// </summary>
public RenderStage PostEffectsUIRenderStage { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostEffectsUIRenderStage naming is a bit ambiguous imho. It is a render stage that draws UI after post-FX, but he naming does not implies it occurs after post-FX, but in post-FX.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to "AfterPostEffectUIRenderStage" locally, before I commit it, would that be a good name?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it is. I'm bad myself at naming things, but sounds right.

Is there something in your logic that makes this render-stage UI-specific? I ask because I'm thinking about other effects that may benefit from no-post-fx (for example, debug rendering, outlines, etc). Nevermind if this is specific for UI rendering.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something in your logic that makes this render-stage UI-specific?
Nothing in particular, I just wanted to close this bug. I've done some research on Stride's graphic compositor in the past, and I think it could be better if there is more of a code-first approach to writing a custom graphics compositor, maybe with an interface or JSON profile or something, as it is a bit difficult to tinker with it in GameStudio the last time I've tried.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know. I think the graphics compositor editor is/was a half-way solution to a more complete version Silicon had planned that never actually ended happening. There are several places and features like that in the codebase. A pity.

Anyway, my question was that if there is no UI-specific limitation to the use of this render stage, maybe just calling it AfterPostEffectRenderStage is good enough. That way it can be used for other things without implying it is for UI.

@yuechen-li-dev
Copy link
Copy Markdown
Author

Looks good overall, but a few comments:

  • If this PR is a proper fix that improves the defaults to not need the post-FX Clean UI "hack", why mention it all the time in comments and tests? Just do your thing and verify it is working!
  • Naming is ambiguous in my opinion. Look at the comment I left.
  • Some of the comments and logs seem like a WIP. Maybe a little cleanup is needed.

I'll massage the tests a bit to check the rendering is working and the render stage is being invoked in the correct order, excluding any details or specific behavior that may come from the Clean UI patch. However were it implemented it were a "hacky" solution. If this PR is the proper fix, forget about that solution.

Unit tests were generated by Claude. It's very good at debugging if you use it properly, but it gets weirdly hung up about random stuff and it has a tendency to be confidently wrong, so you always have to check its answers. I built GameStudio on my own VS locally and confirmed that it works though.

My own CleanUI patch pretty much automated the RenderGroup 31 bypass documented by others which was the "hacky" solution. This PR should be the more proper fix of the root cause and does not use the same method as the Clean UI patch.

@Ethereal77
Copy link
Copy Markdown
Contributor

it has a tendency to be confidently wrong

Yeah, I can relate. I find it both funny and infuriating how stubborn LLMs can get about things sometimes 😁

@yuechen-li-dev
Copy link
Copy Markdown
Author

Yeah, I think this is pretty much done. It's a simple patch, so I think it's ready if there are no other concerns.

Copy link
Copy Markdown
Contributor

@Ethereal77 Ethereal77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in good shape overall. I left a few comments

87ff1d9cdd52418daf76385176a0e316: ref!! 555e84b4-b68a-4f38-ac3a-f0f563028ef0
5e059d4cc2db4ee8a1f28a40f4ac3ae8: ref!! b03a45c6-7a56-417c-8a80-69cc608671f1
GBufferRenderStage: ref!! ecab139e-5f55-42b5-a324-310c195a9c89
PostEffectsUIRenderStage: ref!! 5b5e4f7e-e8b5-497f-9d3b-418d06823b14
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it still has the old name. It should be AfterPostEffectsUIRenderStage.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to “AfterPostEffectsRenderStage" now.

ShadowMapRenderStages:
fc4d1e0de5c2b0bbc27bcf96e9a848fd: ref!! c0524e55-4061-464d-84dd-7c4c70f70e0e
GBufferRenderStage: null
PostEffectsUIRenderStage: ref!! 62af1128-cc9c-49a0-9d81-3f7f598a4b16
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it still has the old name. It should be AfterPostEffectsUIRenderStage.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to “AfterPostEffectsRenderStage" now.

Comment on lines +70 to +78
/// <summary>
/// A UI 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 AfterPostEffectsUIRenderStage { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just my opinion, but I'd remove UI from the name. The more I rhink of it, the more I believe it would be useful for more than just UI (gizmos, debug rendering, outlines, maybe even some custom material unaffected by post-FX).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Changed to “AfterPostEffectsRenderStage" now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: some double blank lines between methods and blank lines before closing braces

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be corrected in latest PR as well.

@Ethereal77
Copy link
Copy Markdown
Contributor

Btw, Stride.Graphics.Tests projects (for GraphicsProfile 10 and 11) and I believe also Stride.Engine.Tests already include some tests that cover the rendering logic (i.e., materials, layering, lighting and shadowing, etc.)

Your new Stride.Rendering.Tests is a bit alone currently with just the few tests for post-FX render stage. Maybe these should be moved to those other test projects, or the pure rendering tests should be moved here (a bigger change).

@xen2 You have been refactoring tests lately. What do you think?

…rStage", fix spacing inconsistencies in unit test.
@yuechen-li-dev
Copy link
Copy Markdown
Author

Btw, Stride.Graphics.Tests projects (for GraphicsProfile 10 and 11) and I believe also Stride.Engine.Tests already include some tests that cover the rendering logic (i.e., materials, layering, lighting and shadowing, etc.)

Your new Stride.Rendering.Tests is a bit alone currently with just the few tests for post-FX render stage. Maybe these should be moved to those other test projects, or the pure rendering tests should be moved here (a bigger change).

@xen2 You have been refactoring tests lately. What do you think?

Strides.Graphics.Test project is very heavy to run, since it has what I assumed to be image generation checks via DX11. So, that's why I would like to keep a separate project to keep things that doesn't require those image checks separate and maybe move some other tests over as well, if you guys would like, for separation of test concerns.

@Ethereal77
Copy link
Copy Markdown
Contributor

Ethereal77 commented Apr 13, 2026

LGTM. Thanks for your work. A much needed feature imho (and one that took a lot longer than needed).

Strides.Graphics.Test project is very heavy to run, since it has what I assumed to be image generation checks via DX11. So, that's why I would like to keep a separate project to keep things that doesn't require those image checks separate and maybe move some other tests over as well, if you guys would like, for separation of test concerns.

Wrt to that I'll wait for other mantainers' take on this. If it eases the workflow of testing the rendering without drawbacks, why not?
(anyway, that would go in a different PR, I suppose)

@xen2 xen2 force-pushed the master branch 2 times, most recently from ab329f3 to 482bd28 Compare April 16, 2026 07:56
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.

Exclude UI from PostFx by default

2 participants