mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-02 16:09:47 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ee7cd1dfa | |||
| ea9be7484f | |||
| a8fa9c96dc | |||
| c387b969e1 |
@@ -1220,8 +1220,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<StackPanel Spacing="8">
|
||||
<!--Latest commit info-->
|
||||
<Border Classes="optionsInfoRow">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Image Grid.Column="0"
|
||||
Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
||||
Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="LatestCommitLabel"
|
||||
Text="{Binding [About.Github.LatestCommitLabel], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="14"
|
||||
@@ -1232,7 +1237,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
<Button Grid.Column="2"
|
||||
x:Name="LatestCommitHashText"
|
||||
Classes="optionAction"
|
||||
Content="Loading…"
|
||||
@@ -1245,8 +1250,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<!--Update-->
|
||||
<Border Classes="optionsInfoRow">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Image Grid.Column="0"
|
||||
Source="avares://SharpEmu.GUI/Assets/update-icon.png"
|
||||
Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="UpdateLabel"
|
||||
Text="{Binding [Updater.Label], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="14"
|
||||
@@ -1257,7 +1267,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
<Button Grid.Column="2"
|
||||
x:Name="UpdateButton"
|
||||
Classes="optionAction"
|
||||
Content="Check for updates"
|
||||
@@ -1267,8 +1277,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<!--Github-->
|
||||
<Border Classes="optionsInfoRow">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Image Grid.Column="0"
|
||||
Source="avares://SharpEmu.GUI/Assets/github.png"
|
||||
Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="GithubLabel"
|
||||
Text="{Binding [About.Github.Label], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="14"
|
||||
@@ -1279,7 +1294,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
<Button Grid.Column="2"
|
||||
x:Name="GithubButton"
|
||||
Classes="optionAction"
|
||||
Content="{Binding [About.GithubButton], Source={x:Static local:Localization.Instance}}"
|
||||
@@ -1289,8 +1304,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<!--Discord-->
|
||||
<Border Classes="optionsInfoRow">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Image Grid.Column="0"
|
||||
Source="avares://SharpEmu.GUI/Assets/discord.png"
|
||||
Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="DiscordServerLabel"
|
||||
Text="{Binding [About.Discord.Label], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="14"
|
||||
@@ -1301,7 +1321,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1"
|
||||
<TextBlock Grid.Column="2"
|
||||
x:Name="DiscordComingSoonText"
|
||||
Text="{Binding [About.DiscordComingSoon], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="12"
|
||||
|
||||
@@ -95,8 +95,10 @@ public static partial class AgcExports
|
||||
private const uint SpiShaderPgmRsrc1Hs = 0x10A;
|
||||
private const uint SpiShaderPgmLoLs = 0x148;
|
||||
private const uint SpiShaderPgmHiLs = 0x149;
|
||||
private const uint SpiShaderPgmLoGs = 0x8A;
|
||||
private const uint SpiShaderPgmHiGs = 0x8B;
|
||||
// Not 0x8A/0x8B - those are SPI_SHADER_PGM_RSRC1/RSRC2_GS, and reading them
|
||||
// as an address yields a 58-bit value (observed live: 0x30004622C008300).
|
||||
private const uint SpiShaderPgmLoGs = 0x88;
|
||||
private const uint SpiShaderPgmHiGs = 0x89;
|
||||
private const uint SpiShaderPgmChksumGs = 0x80;
|
||||
private const uint SpiPsInputEna = 0x1B3;
|
||||
private const uint SpiPsInputAddr = 0x1B4;
|
||||
@@ -2174,6 +2176,18 @@ public static partial class AgcExports
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "r98I08t+LOg",
|
||||
ExportName = "sceAgcDcbDrawIndexIndirectMultiGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbDrawIndexIndirectMultiGetSize(CpuContext ctx)
|
||||
{
|
||||
// Eight, matching the packet DcbDrawIndexIndirectMulti emits.
|
||||
ctx[CpuRegister.Rax] = 8u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rUuVjyR+Rd4",
|
||||
ExportName = "sceAgcDcbGetLodStatsGetSize",
|
||||
@@ -6426,6 +6440,48 @@ public static partial class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-only view of a parsed graphics context register. False when the
|
||||
/// register was never written.
|
||||
/// </summary>
|
||||
internal static bool TryGetGraphicsContextRegisterForTests(
|
||||
CpuContext ctx,
|
||||
uint registerOffset,
|
||||
out uint value)
|
||||
{
|
||||
value = 0;
|
||||
if (!_submittedGpuStates.TryGetValue(ctx.Memory, out var gpuState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
return gpuState.Graphics.CxRegisters.TryGetValue(registerOffset, out value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SH-register counterpart of <see cref="TryGetGraphicsContextRegisterForTests"/>;
|
||||
/// the shader stage addresses live here.
|
||||
/// </summary>
|
||||
internal static bool TryGetGraphicsShRegisterForTests(
|
||||
CpuContext ctx,
|
||||
uint registerOffset,
|
||||
out uint value)
|
||||
{
|
||||
value = 0;
|
||||
if (!_submittedGpuStates.TryGetValue(ctx.Memory, out var gpuState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
return gpuState.Graphics.ShRegisters.TryGetValue(registerOffset, out value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GraphicsDcbSetIndexSize writes VGT_INDEX_TYPE via SET_UCONFIG_REG.
|
||||
/// Mirror that into <see cref="SubmittedDcbState.IndexSize"/>.
|
||||
@@ -7444,22 +7500,6 @@ public static partial class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var earlyPrimitiveType);
|
||||
if (IsRectListPrimitive(earlyPrimitiveType) &&
|
||||
(exportEvaluation.VertexInputs is null || exportEvaluation.VertexInputs.Count == 0) &&
|
||||
!VertexProgramExportsParameters(exportState.Program) &&
|
||||
GetInterpolatedAttributeCount(pixelState) != 0)
|
||||
{
|
||||
ReturnPooledEvaluationArrays(exportEvaluation);
|
||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||
error =
|
||||
$"rect-list-no-param-exports ps_inputs={GetInterpolatedAttributeCount(pixelState)}";
|
||||
TraceAgcShader(
|
||||
$"agc.rect_list_skip es=0x{exportShaderAddress:X16} " +
|
||||
$"ps=0x{pixelShaderAddress:X16} {error}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -8236,20 +8276,6 @@ public static partial class AgcExports
|
||||
? (packedMasks >> (int)(target * 4)) & 0xFu
|
||||
: 0;
|
||||
|
||||
private static bool VertexProgramExportsParameters(Gen5ShaderProgram program)
|
||||
{
|
||||
foreach (var instruction in program.Instructions)
|
||||
{
|
||||
if (instruction.Control is Gen5ExportControl export &&
|
||||
export.Target is >= 32 and < 64)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||
{
|
||||
var maxAttribute = -1;
|
||||
@@ -12673,9 +12699,6 @@ public static partial class AgcExports
|
||||
private static bool IsEsGeometryShaderType(byte shaderType) =>
|
||||
shaderType is GsShaderType or GsBackShaderType;
|
||||
|
||||
private static bool IsRectListPrimitive(uint primitiveType) =>
|
||||
AgcPrimitiveHelpers.IsRectListPrimitive(primitiveType);
|
||||
|
||||
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
|
||||
{
|
||||
var commandAddress = ctx[CpuRegister.Rdi];
|
||||
|
||||
@@ -4041,6 +4041,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
"vkCreateDebugUtilsMessengerEXT");
|
||||
}
|
||||
|
||||
|
||||
[ThreadStatic]
|
||||
private static string? _pendingShaderModuleDumpPath;
|
||||
|
||||
private static unsafe uint DebugCallback(
|
||||
DebugUtilsMessageSeverityFlagsEXT severity,
|
||||
DebugUtilsMessageTypeFlagsEXT type,
|
||||
@@ -4055,6 +4059,24 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_ => "[VULKAN][INFO]",
|
||||
};
|
||||
Console.Error.WriteLine($"{prefix} {message}");
|
||||
|
||||
|
||||
if (severity == DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt &&
|
||||
message is not null &&
|
||||
message.Contains("vkCreateShaderModule", StringComparison.Ordinal))
|
||||
{
|
||||
var dumpPath = _pendingShaderModuleDumpPath;
|
||||
var dumpHint = dumpPath is null
|
||||
? "Set SHARPEMU_SHADER_SPIRV_DUMP_DIR to a directory to "
|
||||
+ "capture every module's .spv bytes for offline "
|
||||
+ "spirv-dis/spirv-val analysis."
|
||||
: $"Dumped module for this failure: {dumpPath}";
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[SHARPEMU][ERROR] A guest shader compiled to invalid SPIR-V."
|
||||
+ "The shader module was created without an API-level error. {dumpHint}");
|
||||
}
|
||||
|
||||
return Vk.False;
|
||||
}
|
||||
private void CreateSurface()
|
||||
@@ -6844,20 +6866,43 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
private static int _shaderModuleDumpSequence;
|
||||
|
||||
private ShaderModule CreateShaderModule(byte[] code)
|
||||
{
|
||||
fixed (byte* codePointer = code)
|
||||
string? dumpPath = null;
|
||||
var dumpDirectory = Environment.GetEnvironmentVariable("SHARPEMU_SHADER_SPIRV_DUMP_DIR");
|
||||
if (!string.IsNullOrWhiteSpace(dumpDirectory))
|
||||
{
|
||||
var createInfo = new ShaderModuleCreateInfo
|
||||
Directory.CreateDirectory(dumpDirectory);
|
||||
|
||||
var sequence = Interlocked.Increment(ref _shaderModuleDumpSequence);
|
||||
dumpPath = Path.Combine(dumpDirectory, $"{sequence:D4}.spv");
|
||||
File.WriteAllBytes(dumpPath, code);
|
||||
|
||||
_pendingShaderModuleDumpPath = dumpPath;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* codePointer = code)
|
||||
{
|
||||
SType = StructureType.ShaderModuleCreateInfo,
|
||||
CodeSize = (nuint)code.Length,
|
||||
PCode = (uint*)codePointer,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateShaderModule(_device, &createInfo, null, out var module),
|
||||
"vkCreateShaderModule");
|
||||
return module;
|
||||
var createInfo = new ShaderModuleCreateInfo
|
||||
{
|
||||
SType = StructureType.ShaderModuleCreateInfo,
|
||||
CodeSize = (nuint)code.Length,
|
||||
PCode = (uint*)codePointer,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateShaderModule(_device, &createInfo, null, out var module),
|
||||
"vkCreateShaderModule");
|
||||
return module;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pendingShaderModuleDumpPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -712,6 +712,8 @@ public static partial class Gen5SpirvTranslator
|
||||
if (UsesSubgroupOperations())
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniform);
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniformBallot);
|
||||
|
||||
if (UsesSubgroupShuffle())
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniformShuffle);
|
||||
@@ -722,10 +724,6 @@ public static partial class Gen5SpirvTranslator
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniformVote);
|
||||
}
|
||||
|
||||
if (UsesSubgroupBroadcast() || UsesWaveControl())
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniformBallot);
|
||||
}
|
||||
}
|
||||
|
||||
_glsl = _module.ImportExtInst("GLSL.std.450");
|
||||
@@ -1803,13 +1801,16 @@ public static partial class Gen5SpirvTranslator
|
||||
|
||||
if (instruction.Opcode == "SBarrier")
|
||||
{
|
||||
var workgroup = UInt(2);
|
||||
var semantics = UInt(0x108);
|
||||
_module.AddStatement(
|
||||
SpirvOp.ControlBarrier,
|
||||
workgroup,
|
||||
workgroup,
|
||||
semantics);
|
||||
if (_stage == Gen5SpirvStage.Compute)
|
||||
{
|
||||
var workgroup = UInt(2);
|
||||
var semantics = UInt(0x108);
|
||||
_module.AddStatement(
|
||||
SpirvOp.ControlBarrier,
|
||||
workgroup,
|
||||
workgroup,
|
||||
semantics);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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 Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage for the graphics context-register path in the PM4 parser. Draw
|
||||
/// translation reads render state out of this dictionary (CB_TARGET_MASK
|
||||
/// decides whether a draw writes alpha, CB_COLOR_CONTROL decides what the draw
|
||||
/// means), so a write that lands under the wrong key, or fails to overwrite an
|
||||
/// earlier one, silently changes what every later draw does. These drive real
|
||||
/// PM4 packets through the public submit export and assert what the parser
|
||||
/// retained.
|
||||
/// </summary>
|
||||
public sealed class AgcContextRegisterTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x2_0000_0000;
|
||||
private const ulong SubmitPacketAddress = BaseAddress + 0x40;
|
||||
private const ulong CommandAddress = BaseAddress + 0x200;
|
||||
private const ulong IndirectTableAddress = BaseAddress + 0x600;
|
||||
|
||||
private const uint ItNop = 0x10;
|
||||
private const uint ItSetContextReg = 0x69;
|
||||
private const uint RCxRegsIndirect = 0x12;
|
||||
private const uint CbTargetMask = 0x8E;
|
||||
private const uint CbColorControl = 0x202;
|
||||
|
||||
// PM4 type-3 header: 0xC0000000 | ((dwords - 2) << 16) | (opcode << 8), with the
|
||||
// NOP sub-register in bits 2..7 — the parser reads it as (header >> 2) & 0x3F.
|
||||
private static uint Pm4Header(uint dwords, uint opcode, uint register = 0) =>
|
||||
0xC000_0000u | ((dwords - 2) << 16) | (opcode << 8) | ((register & 0x3Fu) << 2);
|
||||
|
||||
[Fact]
|
||||
public void SetContextRegRetainsTargetMask()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(3, ItSetContextReg),
|
||||
CbTargetMask,
|
||||
0x0000_0007u);
|
||||
Submit(ctx, memory, dwordCount: 3);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value));
|
||||
Assert.Equal(0x0000_0007u, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The indirect form carries (offset, value) pairs out of guest memory
|
||||
/// rather than inline dwords, so an offset-encoding mismatch here would
|
||||
/// store the register under a key no reader looks at.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IndirectRegisterWriteRetainsTargetMask()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectRegisterCommand(memory, (CbTargetMask, 0xFFFF_FFFFu));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value));
|
||||
Assert.Equal(0xFFFF_FFFFu, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context registers persist across submissions on hardware until something
|
||||
/// clears them, so a mask written in one submission has to still be there
|
||||
/// for a draw in the next.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TargetMaskSurvivesASecondSubmission()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectRegisterCommand(memory, (CbTargetMask, 0x8888_8888u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
// A second, unrelated submission: a bare NOP that touches no registers.
|
||||
WriteDwords(memory, CommandAddress, Pm4Header(2, ItNop), 0);
|
||||
Submit(ctx, memory, dwordCount: 2);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value));
|
||||
Assert.Equal(0x8888_8888u, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Both encodings must land on the same key, or a title that sets the
|
||||
/// register one way and a reader that expects the other silently disagree.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DirectAndIndirectWritesShareOneKey()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(3, ItSetContextReg),
|
||||
CbTargetMask,
|
||||
0x0000_0007u);
|
||||
Submit(ctx, memory, dwordCount: 3);
|
||||
|
||||
WriteIndirectRegisterCommand(memory, (CbTargetMask, 0x0000_000Fu));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value));
|
||||
Assert.Equal(0x0000_000Fu, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CB_COLOR_CONTROL (0x202) MODE bits [6:4] give Normal=1,
|
||||
/// EliminateFastClear=2, Resolve=3, FmaskDecompress=5, DccDecompress=6. The
|
||||
/// value has to survive the parser intact, ROP3 bits and all, because the
|
||||
/// mode decides whether a draw shades or resolves.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0x0000_0010u, 1u)] // Normal
|
||||
[InlineData(0x0000_0020u, 2u)] // EliminateFastClear
|
||||
[InlineData(0x00CC_0060u, 6u)] // DccDecompress, with ROP3=0xCC alongside
|
||||
public void ColorControlRetainsMode(uint written, uint expectedMode)
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectRegisterCommand(memory, (CbColorControl, written));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbColorControl, out var value));
|
||||
Assert.Equal(written, value);
|
||||
Assert.Equal(expectedMode, (value >> 4) & 0x7u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A later write must win. If the parser kept the first value, a draw that
|
||||
/// sets EliminateFastClear after an earlier Normal would still read Normal
|
||||
/// and the clear would be silently dropped.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ColorControlLaterWriteOverwritesEarlier()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectRegisterCommand(memory, (CbColorControl, 0x00CC_0010u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteIndirectRegisterCommand(memory, (CbColorControl, 0x00CC_0020u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbColorControl, out var value));
|
||||
Assert.Equal(0x00CC_0020u, value);
|
||||
Assert.Equal(2u, (value >> 4) & 0x7u);
|
||||
}
|
||||
|
||||
private static void WriteIndirectRegisterCommand(
|
||||
FakeCpuMemory memory,
|
||||
params (uint Offset, uint Value)[] registers)
|
||||
{
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(4, ItNop, RCxRegsIndirect),
|
||||
(uint)registers.Length,
|
||||
(uint)(IndirectTableAddress & 0xFFFF_FFFFu),
|
||||
(uint)(IndirectTableAddress >> 32));
|
||||
|
||||
for (var index = 0; index < registers.Length; index++)
|
||||
{
|
||||
var entry = IndirectTableAddress + ((ulong)index * 8);
|
||||
WriteUInt32(memory, entry, registers[index].Offset);
|
||||
WriteUInt32(memory, entry + 4, registers[index].Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Submit(CpuContext ctx, FakeCpuMemory memory, uint dwordCount)
|
||||
{
|
||||
WriteUInt64(memory, SubmitPacketAddress, CommandAddress);
|
||||
WriteUInt32(memory, SubmitPacketAddress + 8, dwordCount);
|
||||
ctx[CpuRegister.Rdi] = SubmitPacketAddress;
|
||||
AgcExports.DriverSubmitDcb(ctx);
|
||||
}
|
||||
|
||||
private static CpuContext CreateContext(out FakeCpuMemory memory)
|
||||
{
|
||||
memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
return new CpuContext(memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
private static void WriteDwords(FakeCpuMemory memory, ulong address, params uint[] values)
|
||||
{
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
WriteUInt32(memory, address + ((ulong)index * sizeof(uint)), values[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// 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 Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage for the SH-register path in the PM4 parser. A draw resolves its
|
||||
/// vertex stage from SPI_SHADER_PGM_LO_ES/HI_ES and its pixel stage from
|
||||
/// SPI_SHADER_PGM_LO_PS/HI_PS, both out of this dictionary, so a key that is
|
||||
/// dropped or written under a different encoding pairs a current pixel shader
|
||||
/// with a stale vertex shader — a failure that produces plausible-looking
|
||||
/// garbage rather than an error. These drive real PM4 packets through the
|
||||
/// public submit export and assert what the parser retained.
|
||||
/// </summary>
|
||||
public sealed class AgcShaderStageRegisterTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x2_0000_0000;
|
||||
private const ulong SubmitPacketAddress = BaseAddress + 0x40;
|
||||
private const ulong CommandAddress = BaseAddress + 0x200;
|
||||
private const ulong IndirectTableAddress = BaseAddress + 0x600;
|
||||
|
||||
private const uint ItNop = 0x10;
|
||||
private const uint ItSetShReg = 0x76;
|
||||
private const uint RShRegsIndirect = 0x11;
|
||||
|
||||
// SH register offsets. ES is the vertex stage on GFX10 — the standalone
|
||||
// PGM_LO/HI_GS pair is dead post-GCN and the merged ES/GS stage is addressed
|
||||
// through ES.
|
||||
private const uint SpiShaderPgmLoPs = 0x8;
|
||||
private const uint SpiShaderPgmLoEs = 0xC8;
|
||||
private const uint SpiShaderPgmHiEs = 0xC9;
|
||||
|
||||
private static uint Pm4Header(uint dwords, uint opcode, uint register = 0) =>
|
||||
0xC000_0000u | ((dwords - 2) << 16) | (opcode << 8) | ((register & 0x3Fu) << 2);
|
||||
|
||||
/// <summary>
|
||||
/// The baseline: a direct SET_SH_REG write of the vertex stage address has
|
||||
/// to be readable afterwards. If this fails, nothing downstream can pair
|
||||
/// shaders correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SetShRegRetainsExportShaderAddress()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(3, ItSetShReg),
|
||||
SpiShaderPgmLoEs,
|
||||
0x0044_8582u);
|
||||
Submit(ctx, memory, dwordCount: 3);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value));
|
||||
Assert.Equal(0x0044_8582u, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The indirect encoding must land on the same keys as the direct one. A
|
||||
/// mismatch would store the stage address where the draw never reads it,
|
||||
/// leaving the draw to see whatever a previous submission left behind.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IndirectShRegisterWriteRetainsExportShaderAddress()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8DD1u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value));
|
||||
Assert.Equal(0x0044_8DD1u, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Both stages written in one submission must both read back as written. If
|
||||
/// the vertex stage kept an older value while the pixel stage updated, every
|
||||
/// draw after it would be mis-paired.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BothStagesUpdateTogetherWithinOneSubmission()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(
|
||||
memory,
|
||||
(SpiShaderPgmLoEs, 0x0080_2933u),
|
||||
(SpiShaderPgmLoPs, 0x0044_858Au));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteIndirectShRegisterCommand(
|
||||
memory,
|
||||
(SpiShaderPgmLoEs, 0x0044_8581u),
|
||||
(SpiShaderPgmLoPs, 0x0044_8719u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var es));
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoPs, out var ps));
|
||||
Assert.Equal(0x0044_8581u, es);
|
||||
Assert.Equal(0x0044_8719u, ps);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updating only the pixel stage must leave the vertex stage at its previous
|
||||
/// value rather than dropping the key, or the draw falls back to whatever
|
||||
/// default the resolver finds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PixelStageUpdateLeavesExportStageIntact()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8582u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoPs, 0x0044_858Au));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var es));
|
||||
Assert.Equal(0x0044_8582u, es);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stage addresses are 64-bit: LO carries bits 39:8 and HI the top bits, and
|
||||
/// the draw combines them. A HI retained from an earlier shader while LO
|
||||
/// updates resolves to a splice of two different programs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HighAndLowHalvesUpdateTogether()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(
|
||||
memory,
|
||||
(SpiShaderPgmLoEs, 0x0080_2933u),
|
||||
(SpiShaderPgmHiEs, 0x0000_0008u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteIndirectShRegisterCommand(
|
||||
memory,
|
||||
(SpiShaderPgmLoEs, 0x0044_8582u),
|
||||
(SpiShaderPgmHiEs, 0x0000_0004u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var lo));
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmHiEs, out var hi));
|
||||
Assert.Equal(0x0044_8582u, lo);
|
||||
Assert.Equal(0x0000_0004u, hi);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SH registers persist across submissions on hardware. A stage address set
|
||||
/// in one submission must still be there for a draw in the next.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExportShaderAddressSurvivesASecondSubmission()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8583u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteDwords(memory, CommandAddress, Pm4Header(2, ItNop), 0);
|
||||
Submit(ctx, memory, dwordCount: 2);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value));
|
||||
Assert.Equal(0x0044_8583u, value);
|
||||
}
|
||||
|
||||
private static void WriteIndirectShRegisterCommand(
|
||||
FakeCpuMemory memory,
|
||||
params (uint Offset, uint Value)[] registers)
|
||||
{
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(4, ItNop, RShRegsIndirect),
|
||||
(uint)registers.Length,
|
||||
(uint)(IndirectTableAddress & 0xFFFF_FFFFu),
|
||||
(uint)(IndirectTableAddress >> 32));
|
||||
|
||||
for (var index = 0; index < registers.Length; index++)
|
||||
{
|
||||
var entry = IndirectTableAddress + ((ulong)index * 8);
|
||||
WriteUInt32(memory, entry, registers[index].Offset);
|
||||
WriteUInt32(memory, entry + 4, registers[index].Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Submit(CpuContext ctx, FakeCpuMemory memory, uint dwordCount)
|
||||
{
|
||||
WriteUInt64(memory, SubmitPacketAddress, CommandAddress);
|
||||
WriteUInt32(memory, SubmitPacketAddress + 8, dwordCount);
|
||||
ctx[CpuRegister.Rdi] = SubmitPacketAddress;
|
||||
AgcExports.DriverSubmitDcb(ctx);
|
||||
}
|
||||
|
||||
private static CpuContext CreateContext(out FakeCpuMemory memory)
|
||||
{
|
||||
memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
return new CpuContext(memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
private static void WriteDwords(FakeCpuMemory memory, ulong address, params uint[] values)
|
||||
{
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
WriteUInt32(memory, address + ((ulong)index * sizeof(uint)), values[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user