From 2d6afda90b852bb618bce28dc5e6249a6a149f5e Mon Sep 17 00:00:00 2001 From: kuba Date: Sat, 26 Sep 2026 01:16:30 +0200 Subject: [PATCH] [Gpu] Route packed pixel color exports to the slots CB_SHADER_MASK enables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Gpu/Pipelines/PixelExportRouting.cs | 57 +++++++++++++++++++ .../Gpu/Pipelines/ShaderPipelineCache.cs | 19 +++++-- .../Gen5MslTranslator.Pixel.cs | 2 +- .../Gen5SpirvTranslator.cs | 2 +- src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs | 11 ++++ 5 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 src/SharpEmu.Libs/Gpu/Pipelines/PixelExportRouting.cs diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/PixelExportRouting.cs b/src/SharpEmu.Libs/Gpu/Pipelines/PixelExportRouting.cs new file mode 100644 index 0000000..0fd07e8 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/Pipelines/PixelExportRouting.cs @@ -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 exports = stackalloc int[TargetCount]; + Span 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; + } +} diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs index 43a8a09..496f6ee 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs @@ -224,6 +224,7 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider outputModes = new byte[PixelInputInfo.TargetCount]; outputMappings = new ColorComponentMap[PixelInputInfo.TargetCount]; var outputs = new List(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}"); } diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Pixel.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Pixel.cs index 7aad2cd..279f2b4 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Pixel.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Pixel.cs @@ -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; diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 4c4a7b9..b40b80a 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -963,7 +963,7 @@ public static partial class Gen5SpirvTranslator SpirvDecoration.Location, binding.HostLocation); _pixelOutputs.Add( - binding.GuestSlot, + binding.ExportTarget, new SpirvPixelOutput( variable, outputType, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs index fb48f84..ebf5d7e 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs @@ -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(