[AGC/Vulkan] Extend PS5 runtime and rendering compatibility (#216)

* [Core] Add POSIX native execution and PS5 SELF support

Extend the native backend, guest TLS, fixed-address memory, and loader paths needed by PS5 titles on Windows, Linux, and macOS. Keep workstation GC so high-core-count hosts do not reserve over fixed guest image bases.

* [HLE] Expand PS5 service and media compatibility

Add the kernel, threading, save-data, networking, audio, video-codec, font, dialog, and service exports required by newer PS5 software. Preserve every SysAbi NID currently registered by main while adding the compatibility surface used by ASTRO BOT.

* [AGC/Vulkan] Extend Gen5 shader and presentation support

Expand PM4 handling, Gen5 shader translation, MRT and packed export support, guest image tracking, depth initialization, texture aliasing, and Vulkan presentation. Add the performance overlay and address-filtered diagnostics used to validate ASTRO BOT with original shaders.

* [Core] Align static TLS reservation across hosts

* [Pad] Align primary user ID with UserService

* [Gpu] Preserve runtime scalar buffers across renderer seam

* [AGC] Restore omitted command helper exports

* [Vulkan] Reuse primary views for promoted MRT targets

* [Vulkan] Preserve scratch storage bindings in compute dispatches
This commit is contained in:
Miguel Cruz
2026-07-15 19:02:34 -04:00
committed by GitHub
parent ad5a7d3799
commit 864cbb0fa0
85 changed files with 36299 additions and 9176 deletions
+486
View File
@@ -0,0 +1,486 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
/// <summary>
/// Deswizzles RDNA2 (GFX10) tiled texture surfaces into linear layout so they
/// can be uploaded to Vulkan. PS5 stores most textures in a swizzled layout
/// selected by the 5-bit SWIZZLE_MODE in the image descriptor; uploading those
/// bytes verbatim samples as garbage.
///
/// The GFX10 addressing uses power-of-two swizzle blocks (256 B / 4 KiB /
/// 64 KiB) whose internal element order follows the standard (S), display (D),
/// render (R), or z-order/depth (Z) equations. This implements the exact base
/// S and Z 2D single-sample modes plus the PS5's RB+ 64 KiB Z_X/R_X equations;
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
/// equations are being ported.
/// </summary>
internal static class GnmTiling
{
// Oberon uses the 16-pipe / 8-pixel-packer RB+ topology. These are the
// single-sample 64 KiB equations generated by AMD AddrLib for that exact
// topology. Each entry describes one address bit as an XOR of X/Y bits.
private readonly record struct AddressBit(uint XMask, uint YMask);
private static readonly AddressBit[][] RbPlus64KRenderX =
[
// 1 byte/element: nibble01=0, nibble2=307, nibble3=379.
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(6), Y(6), XY(7, 8), XY(8, 7)],
// 2 bytes/element: nibble01=1, nibble2=307, nibble3=389.
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
Y(3), X(6), XY(7, 7), XY(8, 6)],
// 4 bytes/element: nibble01=39, nibble2=307, nibble3=381.
[Zero, Zero, X(0), X(1), Y(0), Y(1), X(2), Y(2),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(3), Y(3), XY(6, 7), XY(7, 6)],
// 8 bytes/element: nibble01=6, nibble2=307, nibble3=382.
[Zero, Zero, Zero, X(0), Y(0), X(1), X(2), Y(1),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
Y(2), X(3), XY(7, 3), XY(6, 6)],
// 16 bytes/element: nibble01=7, nibble2=307, nibble3=390.
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(2), Y(2), XY(6, 3), XY(3, 6)],
];
private static readonly AddressBit[][] RbPlus64KDepthX =
[
// 1 byte/element: nibble01=8, nibble2=306, nibble3=379.
[X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3), Y(3),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(6), Y(6), XY(7, 8), XY(8, 7)],
// 2 bytes/element: nibble01=9, nibble2=306, nibble3=389.
[Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
Y(3), X(6), XY(7, 7), XY(8, 6)],
// 4 bytes/element: nibble01=10, nibble2=306, nibble3=381.
[Zero, Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(3), Y(3), XY(6, 7), XY(7, 6)],
// 8 bytes/element: nibble01=11, nibble2=307, nibble3=382.
[Zero, Zero, Zero, X(0), Y(0), X(1), Y(1), X(2),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
Y(2), X(3), XY(7, 3), XY(6, 6)],
// 16 bytes/element is identical to R_X for a 2D single-sample image.
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(2), Y(2), XY(6, 3), XY(3, 6)],
];
private static readonly AddressBit[][] RbPlus64KStandard =
[
// GFX10_SW_64K_S_RBPLUS_PATINFO, 1 byte/element.
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
Y(4), X(4), Y(5), X(5), Y(6), X(6), Y(7), X(7)],
// 2 bytes/element.
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
Y(3), X(4), Y(4), X(5), Y(5), X(6), Y(6), X(7)],
// 4 bytes/element.
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
Y(3), X(3), Y(4), X(4), Y(5), X(5), Y(6), X(6)],
// 8 bytes/element (also BC1/BC4 compressed blocks).
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
Y(2), X(3), Y(3), X(4), Y(4), X(5), Y(5), X(6)],
// 16 bytes/element (also 16-byte BC compressed blocks).
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
Y(2), X(2), Y(3), X(3), Y(4), X(4), Y(5), X(5)],
];
// GFX10 4K_S has a separate 12-bit micro-tile equation. It is not the
// generic x/y interleave used by the 64K standard block; using that larger
// equation leaves a regular grid in linearized atlases.
private static readonly AddressBit[][] Standard4K =
[
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
Y(4), X(4), Y(5), X(5)],
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
Y(3), X(4), Y(4), X(5)],
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
Y(3), X(3), Y(4), X(4)],
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
Y(2), X(3), Y(3), X(4)],
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
Y(2), X(2), Y(3), X(3)],
];
private static readonly bool _enabled = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
"1",
StringComparison.Ordinal);
private static readonly bool _disabled = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
"0",
StringComparison.Ordinal);
private static readonly HashSet<uint> _reportedModes = new();
public static bool Enabled => _enabled || !_disabled;
/// <summary>
/// Base S/Z modes and the Oberon RB+ 64 KiB Z_X/R_X modes for which this
/// implementation carries the exact AddrLib address equations. Other
/// bank/pipe-XOR variants remain opt-in.
/// </summary>
private static bool IsTrustedByDefault(uint swizzleMode) =>
// Exact base S/Z modes. D/R use different GFX10 swizzle equations and
// the T/X modes additionally apply pipe/bank XOR between blocks.
swizzleMode is 1 or 4 or 5 or 8 or 9 or 24 or 27;
// Detile a surface when it is verified-correct by default (trusted base mode),
// or when the user opts the approximate modes in with SHARPEMU_DETILE=1.
// SHARPEMU_DETILE=0 forces the old raw-upload behavior for everything.
private static bool ShouldDetile(uint swizzleMode) =>
swizzleMode != 0 && !_disabled && (_enabled || IsTrustedByDefault(swizzleMode));
/// <summary>
/// True when a surface with the given swizzle mode needs deswizzling.
/// Mode 0 is linear and never needs it.
/// </summary>
public static bool NeedsDetile(uint swizzleMode) => ShouldDetile(swizzleMode);
/// <summary>
/// Gets the physical byte span occupied by a tiled mip. GNM allocates whole
/// swizzle blocks even when the logical image is much smaller than a block;
/// reading only the logical texel count truncates most source offsets during
/// detiling (a 64x64 BC1 mode-9 image is 2 KiB logically but occupies one
/// 64 KiB swizzle block).
/// </summary>
public static bool TryGetTiledByteCount(
uint swizzleMode,
int elementsWide,
int elementsHigh,
int bytesPerElement,
out ulong byteCount)
{
byteCount = 0;
if (!ShouldDetile(swizzleMode) ||
elementsWide <= 0 ||
elementsHigh <= 0 ||
bytesPerElement <= 0 ||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
{
return false;
}
var bppLog2 = BitLog2((uint)bytesPerElement);
if (bppLog2 < 0)
{
return false;
}
var blockElements = blockBytes >> bppLog2;
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
if (blockWidth == 0 || blockHeight == 0)
{
return false;
}
var blocksWide = ((ulong)elementsWide + (ulong)blockWidth - 1) / (ulong)blockWidth;
var blocksHigh = ((ulong)elementsHigh + (ulong)blockHeight - 1) / (ulong)blockHeight;
try
{
byteCount = checked(blocksWide * blocksHigh * (ulong)blockBytes);
return true;
}
catch (OverflowException)
{
byteCount = 0;
return false;
}
}
/// <summary>
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
/// Elements are pixels for uncompressed formats and 4x4 blocks for
/// block-compressed formats, so callers pass the element grid dimensions
/// and the bytes per element. Returns false (leaving output untouched) for
/// unsupported swizzle modes so the caller can fall back to the raw bytes.
/// </summary>
public static bool TryDetile(
ReadOnlySpan<byte> tiled,
Span<byte> linear,
uint swizzleMode,
int elementsWide,
int elementsHigh,
int bytesPerElement)
{
if (!ShouldDetile(swizzleMode) || elementsWide <= 0 || elementsHigh <= 0 || bytesPerElement <= 0)
{
return false;
}
if (!TryGetSwizzleKind(swizzleMode, out var kind, out var blockBytes))
{
ReportUnsupported(swizzleMode);
return false;
}
var bppLog2 = BitLog2((uint)bytesPerElement);
if (bppLog2 < 0)
{
// Non-power-of-two element size (e.g. 24-bit) is not swizzled in a
// way this equation models.
ReportUnsupported(swizzleMode);
return false;
}
// Block dimensions in elements: a swizzle block holds blockBytes bytes,
// laid out as a square-ish power-of-two element grid scaled by bpp.
var blockElements = blockBytes >> bppLog2;
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
if (blockWidth == 0 || blockHeight == 0)
{
ReportUnsupported(swizzleMode);
return false;
}
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
var requiredLinear = (long)elementsWide * elementsHigh * bytesPerElement;
if (linear.Length < requiredLinear)
{
return false;
}
// Precompute the within-block element offset for each (x, y) inside a
// single block. The swizzle equation only depends on the in-block
// coordinates, so this table is reused for every block — turning the
// per-pixel bit-interleave (a loop + calls) into a single array lookup.
// Detiling a 2048x2048 texture is millions of elements; without this the
// per-pixel math makes DETILE unusably slow during asset streaming.
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
{
for (var bx = 0; bx < blockWidth; bx++)
{
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
}
}
for (var y = 0; y < elementsHigh; y++)
{
var blockY = y / blockHeight;
var inBlockY = y % blockHeight;
var rowBlockBase = (long)blockY * blocksPerRow;
var tableRowBase = inBlockY * blockWidth;
var destRowBase = (long)y * elementsWide * bytesPerElement;
for (var x = 0; x < elementsWide; x++)
{
var blockX = x / blockWidth;
var inBlockX = x % blockWidth;
var blockIndex = rowBlockBase + blockX;
var sourceByte = hasExactXorPattern
? blockIndex * blockBytes + ComputePatternOffset((uint)x, (uint)y, xorPattern)
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
(long)bytesPerElement;
var destByte = destRowBase + (long)x * bytesPerElement;
if (sourceByte + bytesPerElement > tiled.Length ||
destByte + bytesPerElement > linear.Length)
{
continue;
}
tiled.Slice((int)sourceByte, bytesPerElement)
.CopyTo(linear.Slice((int)destByte, bytesPerElement));
}
}
return true;
}
private enum SwizzleKind
{
Standard,
ZOrder,
}
private static readonly AddressBit Zero = new(0, 0);
private static AddressBit X(int bit) => new(1u << bit, 0);
private static AddressBit Y(int bit) => new(0, 1u << bit);
private static AddressBit XY(int xBit, int yBit) => new(1u << xBit, 1u << yBit);
private static AddressBit XY(int xBit, int yBit1, int yBit2) =>
new(1u << xBit, (1u << yBit1) | (1u << yBit2));
private static bool TryGetExactXorPattern(
uint swizzleMode,
int bytesPerElementLog2,
out AddressBit[] pattern)
{
pattern = [];
if ((uint)bytesPerElementLog2 >= RbPlus64KRenderX.Length)
{
return false;
}
pattern = swizzleMode switch
{
5 => Standard4K[bytesPerElementLog2],
9 => RbPlus64KStandard[bytesPerElementLog2],
24 => RbPlus64KDepthX[bytesPerElementLog2],
27 => RbPlus64KRenderX[bytesPerElementLog2],
_ => [],
};
return pattern.Length != 0;
}
private static long ComputePatternOffset(uint x, uint y, AddressBit[] pattern)
{
uint offset = 0;
for (var bit = 0; bit < pattern.Length; bit++)
{
var equation = pattern[bit];
var parity = (System.Numerics.BitOperations.PopCount(x & equation.XMask) +
System.Numerics.BitOperations.PopCount(y & equation.YMask)) & 1;
offset |= (uint)parity << bit;
}
return offset;
}
private static bool TryGetSwizzleKind(uint swizzleMode, out SwizzleKind kind, out int blockBytes)
{
// GFX10 AddrLib SWIZZLE_MODE enumeration:
// 1-3 = 256 B S/D/R
// 4-7 = 4 KiB Z/S/D/R
// 8-11 = 64 KiB Z/S/D/R
// 16-19 = 64 KiB Z/S/D/R _T
// 20-23 = 4 KiB Z/S/D/R _X
// 24-27 = 64 KiB Z/S/D/R _X
// The pipe/bank XOR (_T/_X) affects which block a given tile lands in,
// but the *within-block* element order matches the base S/Z equation,
// which is what dominates visible correctness. We model the block
// interior and treat blocks as linear-ordered.
kind = SwizzleKind.Standard;
blockBytes = 0;
switch (swizzleMode)
{
case 4: case 8: case 16: case 20: case 24:
kind = SwizzleKind.ZOrder;
break;
case 1: case 2: case 3:
case 5: case 6: case 7:
case 9: case 10: case 11:
case 17: case 18: case 19:
case 21: case 22: case 23:
case 25: case 26: case 27:
kind = SwizzleKind.Standard;
break;
default:
return false;
}
blockBytes = swizzleMode switch
{
>= 1 and <= 3 => 256,
>= 4 and <= 7 => 4096,
>= 20 and <= 23 => 4096,
_ => 65536,
};
return true;
}
// Standard (S) 2D swizzle: within a block, the element order interleaves x
// and y bits with x taking the low bit — the AMD "standard" microtile.
private static long StandardSwizzleOffset(uint x, uint y, int blockWidth, int blockHeight)
{
var widthBits = BitLog2((uint)blockWidth);
var heightBits = BitLog2((uint)blockHeight);
long offset = 0;
var outBit = 0;
var xi = 0;
var yi = 0;
while (xi < widthBits || yi < heightBits)
{
if (xi < widthBits)
{
offset |= (long)((x >> xi) & 1u) << outBit++;
xi++;
}
if (yi < heightBits)
{
offset |= (long)((y >> yi) & 1u) << outBit++;
yi++;
}
}
return offset;
}
// Z-order (Morton) swizzle: pure bit interleave, y taking the low bit.
private static long MortonInterleave(uint x, uint y, int blockWidth, int blockHeight)
{
var widthBits = BitLog2((uint)blockWidth);
var heightBits = BitLog2((uint)blockHeight);
long offset = 0;
var outBit = 0;
var xi = 0;
var yi = 0;
while (xi < widthBits || yi < heightBits)
{
if (yi < heightBits)
{
offset |= (long)((y >> yi) & 1u) << outBit++;
yi++;
}
if (xi < widthBits)
{
offset |= (long)((x >> xi) & 1u) << outBit++;
xi++;
}
}
return offset;
}
private static (int Width, int Height) SquareBlockDimensions(int blockElements)
{
if (blockElements <= 0 || (blockElements & (blockElements - 1)) != 0)
{
return (0, 0);
}
var totalBits = BitLog2((uint)blockElements);
// Split as evenly as possible with width >= height (x gets the extra bit).
var widthBits = (totalBits + 1) / 2;
var heightBits = totalBits - widthBits;
return (1 << widthBits, 1 << heightBits);
}
private static int BitLog2(uint value)
{
if (value == 0 || (value & (value - 1)) != 0)
{
return -1;
}
return System.Numerics.BitOperations.TrailingZeroCount(value);
}
private static void ReportUnsupported(uint swizzleMode)
{
lock (_reportedModes)
{
if (!_reportedModes.Add(swizzleMode))
{
return;
}
}
Console.Error.WriteLine(
$"[LOADER][WARN] GNM detile: unsupported swizzle mode {swizzleMode}; texture uploaded linear.");
}
}