From 6b1abc1a385253884bde4acde5d8a0bdc8092c23 Mon Sep 17 00:00:00 2001 From: 999sian <71638559+999sian@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:17:39 +0100 Subject: [PATCH] perf(shader): cache pixel export masks (#288) --- src/SharpEmu.Libs/Agc/AgcExports.cs | 36 +++++----------- src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs | 22 ++++++++++ .../Agc/Gen5ShaderProgramTests.cs | 41 +++++++++++++++++++ 3 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderProgramTests.cs diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 1a54e9f..3764641 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -5937,13 +5937,14 @@ public static partial class AgcExports // Every bound color target the shader exports to. Deferred renderers // draw a multi-render-target G-buffer (up to eight slots) in one pass. // Fall back to slot 0 if we cannot match any export to a bound target. + var pixelColorExportMasks = pixelState.Program.PixelColorExportMasks; var allBoundTargets = GetRenderTargets(state.CxRegisters); // At most 8 slots; a manual filter avoids the per-draw LINQ iterator/ // closure allocations. Slots are distinct, so sorting by slot is stable. var selectedTargets = new List(allBoundTargets.Count); foreach (var target in allBoundTargets) { - if (HasPixelColorExport(pixelState, target.Slot)) + if (GetPixelColorExportMask(pixelColorExportMasks, target.Slot) != 0) { selectedTargets.Add(target); } @@ -5966,7 +5967,7 @@ public static partial class AgcExports { TraceAgcShader( $"agc.mrt_filter ps=0x{pixelShaderAddress:X16} " + - $"bound=[{string.Join(",", allBoundTargets.Select(t => $"s{t.Slot}:0x{t.Address:X}:exp{(HasPixelColorExport(pixelState, t.Slot) ? 1 : 0)}"))}] " + + $"bound=[{string.Join(",", allBoundTargets.Select(t => $"s{t.Slot}:0x{t.Address:X}:exp{(GetPixelColorExportMask(pixelColorExportMasks, t.Slot) != 0 ? 1 : 0)}"))}] " + $"kept={renderTargets.Length}"); } @@ -6166,7 +6167,7 @@ public static partial class AgcExports DecodeDepthTarget(state.CxRegisters), guestTargets, ApplyTransparentPremultipliedFillClear( - CreateRenderState(state.CxRegisters, renderTargets, pixelState), + CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks), textures, vertexInputs, pixelEvaluation.InitialScalarRegisters), @@ -6410,26 +6411,10 @@ public static partial class AgcExports return true; } - private static bool HasPixelColorExport(Gen5ShaderState state, uint target) => - GetPixelColorExportMask(state, target) != 0; - - private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) - { - // Called per render target (twice per draw via CreateRenderState + - // HasPixelColorExport); a manual scan avoids the per-call LINQ iterator - // and closure allocations. Same result as the previous - // Select/OfType/Where/Aggregate chain. - var mask = 0u; - foreach (var instruction in state.Program.Instructions) - { - if (instruction.Control is Gen5ExportControl export && export.Target == target) - { - mask |= export.EnableMask & 0xFu; - } - } - - return mask; - } + private static uint GetPixelColorExportMask(uint packedMasks, uint target) => + target < ColorTargetCount + ? (packedMasks >> (int)(target * 4)) & 0xFu + : 0; private static uint GetInterpolatedAttributeCount(Gen5ShaderState state) { @@ -6627,7 +6612,7 @@ public static partial class AgcExports private static GuestRenderState CreateRenderState( IReadOnlyDictionary registers, IReadOnlyList targets, - Gen5ShaderState pixelState) + uint pixelColorExportMasks) { if (targets.Count == 0) { @@ -6642,7 +6627,8 @@ public static partial class AgcExports var blend = DecodeBlendState(registers, target.Slot); return blend with { - WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot), + WriteMask = blend.WriteMask & + GetPixelColorExportMask(pixelColorExportMasks, target.Slot), }; }).ToArray(), scissor, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs index 01734ed..8d26be7 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs @@ -325,9 +325,31 @@ public sealed record Gen5ShaderProgram( ulong Address, IReadOnlyList Instructions) { + private const uint PixelColorTargetCount = 8; + private const int PixelColorMaskBits = 4; + private readonly uint _pixelColorExportMasks = ComputePixelColorExportMasks(Instructions); private const int ScalarRegisterCount = 256; private IReadOnlySet? _runtimeScalarRegisters; + public uint PixelColorExportMasks => _pixelColorExportMasks; + + private static uint ComputePixelColorExportMasks( + IReadOnlyList instructions) + { + var masks = 0u; + foreach (var instruction in instructions) + { + if (instruction.Control is Gen5ExportControl export && + export.Target < PixelColorTargetCount) + { + masks |= (export.EnableMask & 0xFu) << + (int)(export.Target * PixelColorMaskBits); + } + } + + return masks; + } + public IEnumerable ImageResources => Instructions .Select(instruction => instruction.Control) diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderProgramTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderProgramTests.cs new file mode 100644 index 0000000..de0368b --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderProgramTests.cs @@ -0,0 +1,41 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.ShaderCompiler; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class Gen5ShaderProgramTests +{ + [Fact] + public void PixelColorExportMasksPackAllColorTargets() + { + var program = new Gen5ShaderProgram( + 0, + [ + Export(0, 0x1), + Export(0, 0x4), + Export(1, 0x2), + Export(2, 0x3), + Export(3, 0x4), + Export(4, 0x5), + Export(5, 0x6), + Export(6, 0x7), + Export(7, 0x8), + Export(8, 0xF), + ]); + + Assert.Equal(0x8765_4325u, program.PixelColorExportMasks); + Assert.Equal(0x8765_4325u, program.PixelColorExportMasks); + } + + private static Gen5ShaderInstruction Export(uint target, uint enableMask) => new( + 0, + Gen5ShaderEncoding.Exp, + "exp", + [], + [], + [], + new Gen5ExportControl(target, enableMask, false, false, false)); +}