[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:
parent
4d65ff6d51
commit
2d6afda90b
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -963,7 +963,7 @@ public static partial class Gen5SpirvTranslator
|
|||
SpirvDecoration.Location,
|
||||
binding.HostLocation);
|
||||
_pixelOutputs.Add(
|
||||
binding.GuestSlot,
|
||||
binding.ExportTarget,
|
||||
new SpirvPixelOutput(
|
||||
variable,
|
||||
outputType,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue