From f8c85b6168589944df0c91dc9e6cce85b47bf608 Mon Sep 17 00:00:00 2001 From: kuba Date: Sat, 26 Sep 2026 07:56:25 +0200 Subject: [PATCH] [Agc] Emit direct draws for every draw modifier sceAgcDcbDrawIndex and sceAgcDcbDrawIndexAuto returned without writing a packet unless the modifier was exactly 0x40000000. The modifier names the shader stage and user registers of the bound pipeline, so any other pipeline lost its draws without a trace. Unreal bakes its color-grading LUT through a geometry-stage pipeline, which left the LUT unwritten and tinted every 3D frame of Poppy Playtime blue. Both draws now emit their packets for any modifier. Modifiers other than the vertex-stage default are logged once each. (cherry picked from commit 8d46a6068b7d1a999662eeac1ee17f9e6d87c2eb) --- .../Agc/AgcExports.DrawPackets.cs | 26 +++++- .../Agc/AgcDirectDrawModifierTests.cs | 90 +++++++++++++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Agc/AgcDirectDrawModifierTests.cs diff --git a/src/SharpEmu.Libs/Agc/AgcExports.DrawPackets.cs b/src/SharpEmu.Libs/Agc/AgcExports.DrawPackets.cs index dfb8c60..0317c42 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.DrawPackets.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.DrawPackets.cs @@ -1,6 +1,7 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Collections.Concurrent; using SharpEmu.HLE; using SharpEmu.Libs.Kernel; @@ -133,13 +134,15 @@ public static partial class AgcExports var commandBufferAddress = ctx[CpuRegister.Rdi]; var indexCount = (uint)ctx[CpuRegister.Rsi]; var indexAddress = ctx[CpuRegister.Rdx]; - var modifier = (uint)ctx[CpuRegister.Rcx]; + var modifier = ctx[CpuRegister.Rcx]; - if (commandBufferAddress == 0 || modifier != 0x4000_0000) + if (commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } + NoteDrawModifier("sceAgcDcbDrawIndex", modifier); + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) || !TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) || !TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) || @@ -239,11 +242,13 @@ public static partial class AgcExports var commandBufferAddress = ctx[CpuRegister.Rdi]; var indexCount = (uint)ctx[CpuRegister.Rsi]; var modifier = ctx[CpuRegister.Rdx]; - if (commandBufferAddress == 0 || modifier != 0x4000_0000) + if (commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } + NoteDrawModifier("sceAgcDcbDrawIndexAuto", modifier); + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) || !TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) || !TryWriteUInt32(ctx, commandAddress + 4, indexCount) || @@ -457,4 +462,19 @@ public static partial class AgcExports #pragma warning restore SHEM006 private static long _indirectDrawEmitRejectCount; + + private static readonly ConcurrentDictionary<(string, ulong), byte> _reportedDrawModifiers = new(); + + // The modifier names the shader stage that receives the draw and the user registers the + // command processor fills for indirect draws. A direct draw emits the same packet for every + // modifier; rejecting the unfamiliar ones dropped whole passes, such as Unreal's volume + // rasterization through the geometry stage, without a trace. + private static void NoteDrawModifier(string export, ulong modifier) + { + if (modifier != 0x4000_0000 && _reportedDrawModifiers.TryAdd((export, modifier), 0)) + { + Console.Error.WriteLine( + $"[LOADER][INFO] agc.draw_modifier {export} modifier=0x{modifier:X} stage={(uint)modifier >> 29} flags=0x{(uint)modifier & 7u:X}"); + } + } } diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcDirectDrawModifierTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcDirectDrawModifierTests.cs new file mode 100644 index 0000000..6e015b7 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcDirectDrawModifierTests.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Agc; +using SharpEmu.Libs.Gpu.GpuCommands; +using SharpEmu.Libs.Tests.Gpu.GpuCommands; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +// Direct draws carry a modifier naming the shader stage and the user registers of the pipeline. +// Every modifier must still produce the draw: Unreal's volume passes use a geometry-stage modifier. +public sealed class AgcDirectDrawModifierTests +{ + private const ulong BaseAddress = 0x2_1000_0000; + private const ulong CommandBufferAddress = BaseAddress + 0x80; + private const ulong CommandAddress = BaseAddress + 0x200; + + public static TheoryData Modifiers => new() { 0x4000_0000UL, 0x6000_0000UL, 0xA000_0000UL, 0x4008_0205UL, 0x1_6000_0000UL }; + + [Theory] + [MemberData(nameof(Modifiers))] + public void DrawIndexAutoEmitsTheDrawForEveryModifier(ulong modifier) + { + var memory = CreateCommandBuffer(); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = CommandBufferAddress; + context[CpuRegister.Rsi] = 6; + context[CpuRegister.Rdx] = modifier; + + AgcExports.DcbDrawIndexAuto(context); + + Assert.Equal(CommandAddress, context[CpuRegister.Rax]); + var runner = new StreamRunner(); + Assert.Equal(SubmissionProgress.Complete, runner.Run(ReadDwords(memory, CommandAddress, 7))); + var draw = Assert.Single(runner.Host.AutoDraws); + Assert.Equal(6u, draw.VertexCount); + } + + [Theory] + [MemberData(nameof(Modifiers))] + public void DrawIndexEmitsTheDrawForEveryModifier(ulong modifier) + { + var memory = CreateCommandBuffer(); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = CommandBufferAddress; + context[CpuRegister.Rsi] = 36; + context[CpuRegister.Rdx] = StreamRunner.DataAddress; + context[CpuRegister.Rcx] = modifier; + + AgcExports.DcbDrawIndex(context); + + Assert.NotEqual(0UL, context[CpuRegister.Rax]); + var runner = new StreamRunner(); + Assert.Equal(SubmissionProgress.Complete, runner.Run(ReadDwords(memory, CommandAddress, 11))); + var draw = Assert.Single(runner.Host.IndexedDraws); + Assert.Equal(36u, draw.IndexCount); + Assert.Equal(StreamRunner.DataAddress, draw.IndexAddress); + } + + private static FakeCpuMemory CreateCommandBuffer() + { + var memory = new FakeCpuMemory(BaseAddress, 0x1000); + WriteUInt64(memory, CommandBufferAddress + 0x10, CommandAddress); + WriteUInt64(memory, CommandBufferAddress + 0x18, CommandAddress + 0x100); + return memory; + } + + private static uint[] ReadDwords(FakeCpuMemory memory, ulong address, int count) + { + var bytes = new byte[count * sizeof(uint)]; + Assert.True(memory.TryRead(address, bytes)); + var dwords = new uint[count]; + for (var index = 0; index < count; index++) + { + dwords[index] = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint))); + } + + return dwords; + } + + private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); + Assert.True(memory.TryWrite(address, buffer)); + } +}