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(