[5.8.3] Instanced Stereo: MegaLights (and Volumetric Fog local lights) use the LEFT eye's light grid in the right eye

Summary

With Instanced Stereo and MegaLights enabled, local lights in the right eye are missing or wrong per screen tile.
Example: a point light inside a carved pumpkin lights the pumpkin’s face and the ground in front of it in the left eye.
In the right eye the face stays dark and the light pool on the ground is missing. Shadows and fog lighting also differ
between the eyes. The result depends on the view, because each right-eye tile receives the light list of the same
tile in the left eye’s image.

What Type of Bug are you experiencing?

Rendering (Graphics / Niagara)

Steps to Reproduce

  1. Place a point light (MegaLights on) inside a closed mesh with an opening, e.g. a hollow sphere or box with a
    window cut into it. The light should only escape through the opening.
  2. Instanced Stereo ON. Package and run with -emulatestereo (or play in a headset).
  3. Compare the two halves of the window. In the right half the inside of the mesh and the light pool through the
    opening are dark or missing. Moving the view changes which right-eye areas are affected.

Expected Result

Both eyes should match

Observed Result

Both eyes do not match

Affects Versions

5.8

Platform(s)

OpenXR

Upload an image

Additional Notes

Root cause (5.8 source)

1. Light grid cells (main cause)

  • LightGridInjection.cpp: under single-pass stereo the secondary view writes its light-grid cells into the
    PRIMARY view’s NumCulledLightsGrid / CulledLightDataGrid buffers. Its cells are stored after the primary’s, at
    ForwardLightStruct.CulledBufferOffsetISR (= MaxNumCells).
  • LightGridCommon.ush ComputeLightGridCellIndex(..., EyeIndex) only adds CulledBufferOffsetISR when
    EyeIndex != 0.
  • These passes run separately per view, also for the secondary view, but hard-code EyeIndex = 0. So in the right
    eye they read the LEFT eye’s per-tile light lists:
    • MegaLights/MegaLightsSampling.usf (~line 316): const uint EyeIndex = 0; // TODO
    • MegaLights/MegaLightsVolumeSampling.usf (~line 270): const uint EyeIndex = 0;
    • StochasticLighting/StochasticLightingTileClassification.usf (~line 625): const uint EyeIndex = 0;
    • VolumetricFog.usf (~line 1055) and VolumetricCloud.usf (~line 1258): ComputeLightGridCellIndex(..., 0)
  • The Virtual Shadow Map shaders do this correctly: they use uint EyeIndex = View.StereoPassIndex;
    (VirtualShadowMapLightGrid.ush, VirtualShadowMapPageMarking.usf).
  • Note: with Instanced Stereo enabled, ShaderCompiler.cpp defines INSTANCED_STEREO=1 for every non-ray-tracing
    shader, global compute shaders included. So these shaders do compile the EyeIndex branch; they just always pass 0.

2. Light positions (secondary cause, small error)

  • LightGridInjection.cpp (~line 1682): the secondary view also reuses the primary’s ForwardLightBuffer. Its light
    positions are relative to the primary’s PreViewTranslation, with the difference stored in
    PreViewTranslationOffsetISR.
  • GetForwardLightData_Internal / GetLightViewData in LightGridCommon.ush only add that offset for
    EyeIndex != 0. MegaLights (GetForwardLightData(i, 0) everywhere), VolumetricFog.usf and
    TranslucentLightInjectionShaders.usf pass 0, so right-eye lights are shifted by the eye separation.

Suggested fix

Pass View.StereoPassIndex as EyeIndex in the per-view passes listed above, as the VSM shaders already do.
Alternatively, give the secondary view its own light grid and light buffer.

Workaround we verified

A shader header included right after LightGridCommon.ush in the affected shaders. It wraps
ComputeLightGridCellIndex, GetForwardLightData, GetLocalLightData and the *FromGrid variants. When a call
passes EyeIndex == 0 but View.StereoPassIndex != 0, it adds CulledBufferOffsetISR / PreViewTranslationOffsetISR.
This fixes the right eye in a packaged build (-emulatestereo) and in the headset, with no visible performance cost.

Here is a patch made by claude to fix this issue. Please note, this is an engine-wide patch.

UE5.8_StereoLightFix.zip (3.9 KB)