[Gpu] Route packed pixel color exports to the slots CB_SHADER_MASK enables

Shader compilers drop the MRTs a pixel program does not write, and the
hardware then delivers the Nth color export to the Nth target that
CB_SHADER_MASK enables — the export number and the target slot are not the
same thing. Unreal's base pass is the clear case: it exports MRT0 and MRT1 but
means targets 0 and 4 (SPI_SHADER_COL_FORMAT=0x54, CB_SHADER_MASK=0x000F000F).

Mapping each export to the target of the same number therefore wrote the
fourth G-buffer's data into the first, which showed up as a scene rendered in
world normals. Exports are now paired with the enabled slots in order, the
compiled pixel program keys its outputs by the export that feeds the slot
rather than by the slot, and a bound slot that no export reaches gets a zero
color write mask so it keeps its contents. When the two registers do not pair
up — different counts, or no formats at all — each export keeps going to the
slot of the same number, as before.
This commit is contained in:
kuba 2026-09-26 01:16:30 +02:00
parent 4d65ff6d51
commit 2d6afda90b
5 changed files with 85 additions and 6 deletions

View File

@ -0,0 +1,57 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
namespace SharpEmu.Libs.Gpu.Pipelines;
// Color exports reach the targets in order: the Nth export that SPI_SHADER_COL_FORMAT gives a
// format goes to the Nth target that CB_SHADER_MASK enables. Compilers drop MRTs a pixel program
// does not write, so the export number and the target slot can differ. Unreal's base pass, for
// example, exports MRT0 and MRT1 to targets 0 and 4 with CB_SHADER_MASK=0x000F000F.
internal static class PixelExportRouting
{
private const int TargetCount = 8;
// The export that feeds the slot, or -1 when no export reaches it. When the registers do not
// pair up, each export goes to the slot of the same number.
public static int ExportForSlot(ShaderInterfaceRegisters registers, uint slot)
{
if (slot >= TargetCount)
{
return -1;
}
Span<int> exports = stackalloc int[TargetCount];
Span<int> slots = stackalloc int[TargetCount];
var exportCount = 0;
var slotCount = 0;
for (var index = 0; index < TargetCount; index++)
{
if (index < registers.TargetOutputModes.Length && (registers.TargetOutputModes[index] & 0xF) != 0)
{
exports[exportCount++] = index;
}
if (((registers.ColorShaderMask >> (index * 4)) & 0xFu) != 0)
{
slots[slotCount++] = index;
}
}
if (exportCount == 0 || exportCount != slotCount)
{
return (int)slot;
}
for (var index = 0; index < slotCount; index++)
{
if (slots[index] == slot)
{
return exports[index];
}
}
return -1;
}
}

View File

@ -224,6 +224,7 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
outputModes = new byte[PixelInputInfo.TargetCount];
outputMappings = new ColorComponentMap[PixelInputInfo.TargetCount];
var outputs = new List<Gen5PixelOutputBinding>(ContextRegisters.ColorTargetCount);
var location = 0u;
for (var slot = 0u; slot < ContextRegisters.ColorTargetCount; slot++)
{
if (slot != 0 && (context.RenderTargetMaskForSlot(slot) == 0 || context.ColorTargets[slot].BaseAddress == 0))
@ -242,9 +243,16 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
throw SubmissionScheduler.Fatal($"The color target format has no pixel output kind: slot={slot} layout={(uint)words.Layout} numberType={(uint)words.NumberType} order={(uint)words.Order}.");
}
outputModes[slot] = (byte)((uint)kind + 1);
// A bound slot that no export reaches keeps its contents: its color write mask is zero.
var exportTarget = PixelExportRouting.ExportForSlot(context.ShaderInterface, slot);
// The high nibble keys the compiled program by the export that feeds the slot.
outputModes[slot] = (byte)(((uint)kind + 1) | (uint)((exportTarget + 1) << 4));
outputMappings[slot] = new ColorComponentMap(mapping.Packed);
outputs.Add(new Gen5PixelOutputBinding(slot, (uint)outputs.Count, kind, mapping));
// No EXP target reaches 8 and above, so an unfed slot is declared but never written.
outputs.Add(new Gen5PixelOutputBinding(slot, location++, kind, mapping)
{
ExportTarget = exportTarget >= 0 ? (uint)exportTarget : ContextRegisters.ColorTargetCount + slot,
});
}
return outputs.ToArray();
@ -443,13 +451,16 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
// A target the pixel program never exports keeps its contents, as on hardware; the
// host output would otherwise write an undefined value (e.g. depth-only passes that
// leave a color target bound and export only to the null target).
var exported = pixelStage is null || ((pixelStage.PixelColorExportMasks >> (int)(color.Slot * 4)) & 0xFu) != 0;
var exportTarget = PixelExportRouting.ExportForSlot(context.ShaderInterface, color.Slot);
var exported = pixelStage is null ||
(exportTarget >= 0 && ((pixelStage.PixelColorExportMasks >> (exportTarget * 4)) & 0xFu) != 0);
var colorMask = exported ? color.Resolution.ExportMapping.ApplyMask(context.RenderTargetMaskForSlot(color.Slot)) : 0;
parameters.SetColorMask(index, colorMask);
if (RenderTrace.Enabled && RenderTrace.Pipeline())
{
RenderTrace.Write(
$"PipelineCache output slot={color.Slot} guestMask=0x{context.RenderTargetMaskForSlot(color.Slot):X} " +
$"PipelineCache output slot={color.Slot} export={exportTarget} " +
$"guestMask=0x{context.RenderTargetMaskForSlot(color.Slot):X} " +
$"exported={(exported ? 1 : 0)} shaderMask=0x{pixelStage?.PixelColorExportMasks ?? 0:X8} " +
$"mapping=0x{color.Resolution.ExportMapping.Packed:X2} hostMask=0x{colorMask:X} format={(int)format}");
}

View File

@ -518,7 +518,7 @@ public static partial class Gen5MslTranslator
Gen5PixelOutputBinding? binding = null;
foreach (var candidate in _pixelOutputBindings)
{
if (candidate.GuestSlot == export.Target)
if (candidate.ExportTarget == export.Target)
{
binding = candidate;
break;

View File

@ -963,7 +963,7 @@ public static partial class Gen5SpirvTranslator
SpirvDecoration.Location,
binding.HostLocation);
_pixelOutputs.Add(
binding.GuestSlot,
binding.ExportTarget,
new SpirvPixelOutput(
variable,
outputType,

View File

@ -132,6 +132,17 @@ public readonly record struct Gen5PixelOutputBinding(
: this(guestSlot, hostLocation, kind, Gen5ColorComponentMapping.Identity)
{
}
private readonly uint? _exportTarget;
// The EXP MRT target that feeds this slot. It differs from the slot when the pixel
// program skips targets, because the hardware packs color exports into the slots
// that CB_SHADER_MASK enables.
public uint ExportTarget
{
get => _exportTarget ?? GuestSlot;
init => _exportTarget = value;
}
}
public readonly record struct Gen5ComputeSystemRegisters(