Split screen being clipped

Hi,

The detailed steps and images should be enough to describe the problem and pinpoint the code on your side.

I suppose that without TAA, the final presentation in the viewport is applied using an incorrect destination rectangle.

Let me know if you need any clarifications.

Also, I briefly tested within Unreal Engine 5.8.1 and the behavior seems to remain the same.

Thanks,

[Attachment Removed]

Steps to Reproduce
Hi,

Within our application, we can define the viewport layout and are rendering views considering external parameters. We are managing this as player split screens.

The rendering seems to occur correctly but the presentation is, as far as I can tell, not done correctly.

I found a couple case (including one from our team which seems close but not exactly the same):

[Content removed]

[Content removed]

In details :

In the below shot, we expect the rendering to cover half (0.5) the viewport in both x and y directions and start at a quarter in x and origin in y.

We are looking straight to an entity:

[Image Removed]

We can see that the scene is rendering but is not presented at the desired location leading to the first half be clipped by the presentation rectangle.

IF I enable temporal antialising :

[Image Removed]

THEN, presentation in correct :

[Image Removed]

We are defining presentation areas using the viewport method as an override from the GameViewPortClient ( virtual void LayoutPlayers() override; ) in a similar way. Should not matter since the viewport is correctly considered.

Is this a defect or are we missing something ?

Thanks,

[Attachment Removed]

Hello,

I wasn’t able to reproduce this error in latest using traditional split screen and by modifying UGameViewportClient::UGameViewportClient so that the first splitscreen layout was the offset one:

---SplitscreenInfo[ESplitScreenType::TwoPlayer_Horizontal].PlayerData.Add(FPerPlayerSplitscreenData(1.0f, 0.5f, 0.0f, 0.0f));
+++SplitscreenInfo[ESplitScreenType::TwoPlayer_Horizontal].PlayerData.Add(FPerPlayerSplitscreenData(0.5f, 0.5f, 0.25f, 0.0f));

After entering splitscreen mode I tried toggling r.AntiAliasingMethod and various screen percentages to try and emulate the behavior you are seeing but the viewrect was unaffected.

It’s possible this was an issue was fixed, or that I’m not reproducing the issue in the same way somehow, possibly you are using some kind of upscale or dynamic resolution settings I don’t have. I’m in the process of trying to reproduce in 5.6, but if you cannot easily provide a sample project that reproduces the issue in vanilla UE, I recommend logging the following per-view before PostProcessing:

View.ViewRect

View.UnscaledViewRect

View.GetSecondaryViewRectSize()

View.GetSecondaryViewCropRect()

View.Family->SecondaryViewFraction

View.PrimaryScreenPercentageMethod

ViewFamilyTexture->Desc.Extent

Also take RenderDoc captures or DumpGPU with r.AntiAliasingMethod 0 and 1 and compare the upscaling passes to see if the render-target viewport in the upscaling or tonemap passes are different.

[Attachment Removed]

Hi Alex,

I went ahead and generated GPU dumps as suggested and it seems the rendering is not placed in the same location of the viewfamilytexture according to the anti aliasing method:

[Image Removed]

I am attaching as well the dumps.

I am not certain of the expected workflow but I guess the issue comes from the copy from tone map into the view family texture does not account for the offset.

Let me know if this is enough for you to investigate ?

Note I will be travelling next week and have limited response capabilities.

Thanks,

[Attachment Removed]

Hello,

The AA GPU dump uses r.AntiAliasingMethod=1 and the NoAA GPU dump uses r.AntiAliasingMethod=2, is the error occuring for you only when using TAA ( r.AntiAliasingMethod=2) or only when using no AA (r.AntiAliasingMethod=0)?

[Attachment Removed]

Hi,

Attaching extra dump with r.AntiAliasingMethod 0.

You are correct I toggled between 0 and 1 when noticing the behavior and forgot that it starts with 2 as default in the application.

No AA is failing as well.

Only r.AntiAliasingMethod=1 is fine.

I hope this helps,

Basile

[Attachment Removed]

Unfortunately the GPU dump doesn’t provide enough information about the rects used to verify which pass is at fault.

I have been unable to reproduce the error using the following code:

void UMyGameViewportClient::LayoutPlayers()
{
    Super::LayoutPlayers();
 
    int CurrentPlayer = 0;
    for (ULocalPlayer* LocalPlayer : GetGameInstance()->GetLocalPlayers())
    {
        if (LocalPlayer == nullptr)
        {
            continue;
        }
 
        if (CurrentPlayer == 0)
        {
			LocalPlayer->Size = FVector2D(0.5f, 0.5f);
			LocalPlayer->Origin = FVector2D(0.25f, 0.0f);
        }
        else 
        {
			LocalPlayer->Size = FVector2D(0.5f, 0.5f);
			LocalPlayer->Origin = FVector2D(0.25f, 0.5f);
        }
        CurrentPlayer++;
    }
}

[Image Removed]We’ll need a RenderDoc capture with r.AntiAliasingMethod 0 to verify the pass causing the error, the relevant code in LayoutPlayers() or ideally a sample project in vanilla UE that shows the issue.

[Attachment Removed]

Hi Alex,

I am uploading renderdocs captures for antialiasing methods 0, 1 and 2.

I hope this can help. I had a quick look and both scissors and viewport seem correct to me ! :confused:

From your screen shot, the difference I can think of is that you probably have multi players. While we are using player layout to define / restrict the desired area, we have a single player in the game.

Let me know if this can help.

Thanks,

Basile

[Attachment Removed]

Thanks for the RenderDoc captures, it does look like the CopyTexture pass is not using the correct viewport settings.

[Image Removed]

Can you also provide the relevant code you are using in your LayoutPlayers() override?

[Attachment Removed]

Sounds like a reasonable lead.

Here is our code for the viewport :

void UTigerViewport::LayoutPlayers()

{

if (ViewService)

{

TArray\<ULocalPlayer \*\> const \& PlayerList \= GetOuterUEngine()\-\>GetGamePlayers(this);



FVector2D ViewportSize;

GetViewportSize(ViewportSize);

FVector2D PixelSize;

PixelSize.X \= (1\.0 / ViewportSize.X) \* 2\.0;

PixelSize.Y \= (1\.0 / ViewportSize.Y) \* 2\.0;



for (ULocalPlayer \* Player : PlayerList)

{

  APlayerController \* PlayerController \= Player\-\>GetPlayerController(GetWorld());

  if (IsValid(PlayerController) \=\= false)

  {

    Player\-\>Origin \= { 0, 0 };

    Player\-\>Size \= PixelSize;

    continue;

  }



  ATigerView \* View \= Cast\<ATigerView\>(PlayerController\-\>GetPawn());

  if (IsValid(View) \=\= false)

  {

    Player\-\>Origin \= { 0, 0 };

    Player\-\>Size \= PixelSize;

    continue;

  }



  Player\-\>Origin \= View\-\>Display.PositionScreenUnit;

  Player\-\>Size \= View\-\>Display.SizeScreenUnit;

}

}

else

{

Super::LayoutPlayers();

}

Assuming everything is valid, it applies computed screen position and size for the view.

If anything is wrong, it falls back to full coverage.

Screen position and size is computed there:

void ATigerView::OnViewportResized(FViewport * Viewport, uint32 Unused)

{

FVector2D ViewportSize = Viewport->GetSizeXY();

if (Display.bUseScreenUnit)

{

FVector2D Position \= Display.PositionScreenUnit \* ViewportSize;

Display.Position.X \= Position.X;

Display.Position.Y \= Position.Y;



FVector2D Size \= Display.SizeScreenUnit \* ViewportSize;

Display.Size.X \= Size.X;

Display.Size.Y \= Size.Y;

}

else

{

Display.PositionScreenUnit \= FVector2D{ Display.Position } / ViewportSize;

Display.SizeScreenUnit \= FVector2D{ Display.Size } / ViewportSize;

}

That being said, all those parameters are static in the test case and fully independant from the desired antialiasing mode.

[Attachment Removed]

Unfortunately, I’ve been unable to reproduce the issue using the code posted above in vanilla UE 5.6. Is it possible you have other engine changes? There may also be other project-specific render settings affecting this too, but I’ve tried to match what you have as best I can without success. Somehow the ToneMap and CopyTexture RSSetViewports in your case is using 0,0 for the origin instead of the offset calculated with Player->Origin.

If you can find a way to attach a vanilla UE project that reproduces this issue with the same settings and minimal code you are using that will make it much easier to debug.

[Attachment Removed]

Hi Alex,

With regards to Engine changes, we were hoping to stick to a vanilla version. Still, we reached a point where we had to add performance traces (cpu scopes to understand bottlenecks), and backported two bug fixes (a stability one in the renderer and a performance one in the garbage collector). Nothing that could (well should) be leading to such an anomaly. Also, I had the issue within vanilla UE 5.8.1.

I am still to try in 5.8.2 but I expect the same results.

I talked with another person on our side and while we have a couple extensions, none should be interacting with AA and this.

We will find a test case that can be brought into the engine. Maybe we will find the trigger on the road to it.

I have my hands tied with a performance issue at the moment but I will get back to this as soon as I have a chance.

Keeping you posted,

Thanks,

[Attachment Removed]

OK, thanks for the additional info.

[Attachment Removed]

Hi Alex,

It appears that enabling an optimization on my side (disabling an extension when it is not required), it actually hides the problem. Speaking again with the developer from this extension, he remembered this support case :

[Content removed]

The extension in question is triggering a compute shader and all code path are returning the untouched flag:

void FTigerHistogramViewExtension::SubscribeToPostProcessingPass(EPostProcessingPass Pass, const FSceneView & InView, FPostProcessingPassDelegateArray & InOutPassCallbacks, bool bIsPassEnabled)

{

if (Pass == EPostProcessingPass::Tonemap)

{

InOutPassCallbacks.Add(FAfterPassCallbackDelegate::CreateRaw(this, &FTigerHistogramViewExtension::AfterPostProcessPass_RenderThread));

}

}

FScreenPassTexture FTigerHistogramViewExtension::AfterPostProcessPass_RenderThread(FRDGBuilder & GraphBuilder, const FSceneView & View, const FPostProcessMaterialInputs & Inputs)

{

…

{

return Inputs.ReturnUntouchedSceneColorForPostProcessing(GraphBuilder);

}

}

Unfortunately, subscribing to a different pass as initially accepted is not an option (incorrect color space for our histogram, …) and this time, playing with the showflags as workaround is not helping.

I hope this will be enough for you to reproduce this time.

Thanks,

Basile

[Attachment Removed]

ReturnUntouchedSceneColorForPostProcessing uses FScreenPassTexture::CopyFromSlice and that’s likely where the Dest Rect information is lost.

You should be able to fix the issue by replacing

return Inputs.ReturnUntouchedSceneColorForPostProcessing(GraphBuilder);

with

FScreenPassTextureSlice SceneColorSlice =
    Inputs.GetInput(EPostProcessMaterialInput::SceneColor);
 
if (!Inputs.OverrideOutput.IsValid())
{
    return FScreenPassTexture::CopyFromSlice(GraphBuilder, SceneColorSlice);
}
 
FRDGTextureRef SourceTexture = SceneColorSlice.TextureSRV->Desc.Texture;
const FIntRect SourceRect = SceneColorSlice.ViewRect;
const FIntRect DestRect = Inputs.OverrideOutput.ViewRect;
 
FRHICopyTextureInfo CopyInfo;
CopyInfo.SourceSliceIndex = SceneColorSlice.TextureSRV->Desc.FirstArraySlice;
CopyInfo.NumMips = SourceTexture->Desc.NumMips;
CopyInfo.SourcePosition = FIntVector(SourceRect.Min.X, SourceRect.Min.Y, 0);
CopyInfo.DestPosition = FIntVector(DestRect.Min.X, DestRect.Min.Y, 0);
CopyInfo.Size = FIntVector(SourceRect.Width(), SourceRect.Height(), 1);
 
AddCopyTexturePass(
    GraphBuilder,
    SourceTexture,
    Inputs.OverrideOutput.Texture,
    CopyInfo);
 
return Inputs.OverrideOutput;

Or by modifying the engine ScreenPass.cpp FScreenPassTexture::CopyFromSlice

FScreenPassTexture FScreenPassTexture::CopyFromSlice(FRDGBuilder& GraphBuilder, const FScreenPassTextureSlice& ScreenTextureSlice, FScreenPassTexture OverrideOutput)
{
...
--- CopyInfo.SourcePosition = CopyInfo.DestPosition = FIntVector(ScreenTextureSlice.ViewRect.Min.X, ScreenTextureSlice.ViewRect.Min.Y, 0);
+++CopyInfo.SourcePosition = FIntVector(ScreenTextureSlice.ViewRect.Min.X, ScreenTextureSlice.ViewRect.Min.Y, 0);
+++CopyInfo.DestPosition = OverrideOutput.IsValid()
    ? FIntVector(OverrideOutput.ViewRect.Min.X, OverrideOutput.ViewRect.Min.Y, 0)
    : CopyInfo.SourcePosition;
...
---return FScreenPassTexture(OutputTexture, ScreenTextureSlice.ViewRect);
+++return FScreenPassTexture(OutputTexture, OverrideOutput.IsValid() ? OverrideOutput.ViewRect : ScreenTextureSlice.ViewRect)
}

The engine change maybe something we’ll implement as a fix if we determine it won’t have side effects.

I’ve created https://issues.unrealengine.com/issue/UE\-397385 for tracking this change which should be visible publicly soon.

[Attachment Removed]

Thanks Alex.

Good to have this one formally identified.

The workaround seems indeed to do the job. I tested this in editor only but I assume this will be fine after packaging.

Best

[Attachment Removed]

Yes, this should be fine after packaging also. If this issue is now resolved, I’ll go ahead and close it and the Jira above can be used for tracking the engine-side fix. Let me know if you have any additional questions.

[Attachment Removed]

This is fine. You can go ahead and get this one closed as resolved / explained.

I have plenty of questions but none related to this item. I will keep filing them as separate tickets when I have time to focus on them.

Thanks again,

Basile

[Attachment Removed]