[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)
This commit is contained in:
kuba 2026-09-26 07:56:25 +02:00
parent d5de44b6fb
commit f8c85b6168
2 changed files with 113 additions and 3 deletions

View File

@ -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}");
}
}
}

View File

@ -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<ulong> 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<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
Assert.True(memory.TryWrite(address, buffer));
}
}