[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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
public static partial class AgcExports
{
// Exact Gen5 primary register defaults originally reverse-engineered by Kyty (MIT).
// Hashes, offsets, and values match the tables consumed by the PS5 AGC SDK.
private static RegisterDefaultGroup[] CreatePrimaryRegisterDefaults() =>
[
// context register groups
new(0u, 0u, 0xE24F806Du, [new(0x202u, 0x00CC0010u)]), // CB_COLOR_CONTROL
new(0u, 1u, 0xF6C28182u, [new(0x109u, 0x00000000u)]), // CB_DCC_CONTROL
new(0u, 2u, 0x6F6E55A5u, [new(0x104u, 0x00000000u)]), // CB_RMI_GL2_CACHE_CONTROL
new(0u, 3u, 0x0BC65DA4u, [new(0x08Fu, 0x00000000u)]), // CB_SHADER_MASK
new(0u, 4u, 0x9E5AD592u, [new(0x08Eu, 0x0000000Fu)]), // CB_TARGET_MASK
new(0u, 5u, 0xBB513B98u, [new(0x2DCu, 0x0000AA00u)]), // DB_ALPHA_TO_MASK
new(0u, 6u, 0xAB64B23Bu, [new(0x001u, 0x00000000u)]), // DB_COUNT_CONTROL
new(0u, 7u, 0x53C39964u, [new(0x200u, 0x00000000u)]), // DB_DEPTH_CONTROL
new(0u, 8u, 0x01396B11u, [new(0x201u, 0x00000000u)]), // DB_EQAA
new(0u, 9u, 0x7D42019Au, [new(0x000u, 0x00000000u)]), // DB_RENDER_CONTROL
new(0u, 10u, 0x3548F523u, [new(0x006u, 0x00000000u)]), // PS_SHADER_SAMPLE_EXCLUSION_MASK
new(0u, 11u, 0xF43AD28Au, [new(0x01Fu, 0x00000000u)]), // DB_RMI_L2_CACHE_CONTROL
new(0u, 12u, 0x6DE4C312u, [new(0x203u, 0x00000000u)]), // DB_SHADER_CONTROL
new(0u, 13u, 0x00A77AE0u, [new(0x2B0u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE0
new(0u, 14u, 0x00A779B7u, [new(0x2B1u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE1
new(0u, 15u, 0x5100100Cu, [new(0x10Cu, 0x00000000u)]), // DB_STENCILREFMASK
new(0u, 16u, 0x59958BBAu, [new(0x10Du, 0x00000000u)]), // DB_STENCILREFMASK_BF
new(0u, 17u, 0x0C06F17Cu, [new(0x10Bu, 0x00000000u)]), // DB_STENCIL_CONTROL
new(0u, 18u, 0x6F104B72u, [new(0x1FFu, 0x00000000u)]), // GE_MAX_OUTPUT_PER_SUBGROUP
new(0u, 19u, 0x25C70D9Cu, [new(0x204u, 0x00000000u)]), // PA_CL_CLIP_CNTL
new(0u, 20u, 0x3881201Eu, [new(0x20Du, 0x00000000u)]), // PA_CL_OBJPRIM_ID_CNTL
new(0u, 21u, 0x09AFDDAFu, [new(0x206u, 0x0000043Fu)]), // PA_CL_VTE_CNTL
new(0u, 22u, 0x367D63CFu, [new(0x2F8u, 0x00000000u)]), // PA_SC_AA_CONFIG
new(0u, 23u, 0x43707DB8u, [new(0x083u, 0x0000FFFFu)]), // PA_SC_CLIPRECT_RULE
new(0u, 24u, 0xF6AE26BAu, [new(0x313u, 0x00000000u)]), // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL
new(0u, 25u, 0x1B917652u, [new(0x800003FEu, 0x00000000u)]), // PA_SC_FSR_ENABLE
new(0u, 26u, 0x94B1E4F7u, [new(0x0EAu, 0x00000000u)]), // PA_SC_HORIZ_GRID
new(0u, 27u, 0xE3661B6Cu, [new(0x0E9u, 0x00000000u)]), // PA_SC_LEFT_VERT_GRID
new(0u, 28u, 0x1EB8D73Au, [new(0x292u, 0x00000002u)]), // PA_SC_MODE_CNTL_0
new(0u, 29u, 0x15051FA3u, [new(0x293u, 0x00000000u)]), // PA_SC_MODE_CNTL_1
new(0u, 30u, 0x9C51A7F1u, [new(0x0E8u, 0x00000000u)]), // PA_SC_RIGHT_VERT_GRID
new(0u, 31u, 0xA20EFC70u, [new(0x080u, 0x00000000u)]), // PA_SC_WINDOW_OFFSET
new(0u, 32u, 0x0EC09F6Eu, [new(0x211u, 0x00000000u)]), // PA_STATE_STEREO_X
new(0u, 33u, 0x34A7D6D3u, [new(0x210u, 0x00000000u)]), // PA_STEREO_CNTL
new(0u, 34u, 0xCE831B94u, [new(0x08Du, 0x00000000u)]), // PA_SU_HARDWARE_SCREEN_OFFSET
new(0u, 35u, 0x5CC72A74u, [new(0x282u, 0x00000008u)]), // PA_SU_LINE_CNTL
new(0u, 36u, 0x3B77713Cu, [new(0x281u, 0xFFFF0000u)]), // PA_SU_POINT_MINMAX
new(0u, 37u, 0x40F64410u, [new(0x280u, 0x00080008u)]), // PA_SU_POINT_SIZE
new(0u, 38u, 0x69441268u, [new(0x2DFu, 0x00000000u)]), // PA_SU_POLY_OFFSET_CLAMP
new(0u, 39u, 0x2E418B83u, [new(0x2DEu, 0x000001E9u)]), // PA_SU_POLY_OFFSET_DB_FMT_CNTL
new(0u, 40u, 0xA00D0C8Du, [new(0x205u, 0x00000240u)]), // PA_SU_SC_MODE_CNTL
new(0u, 41u, 0xB1289FB3u, [new(0x20Cu, 0x00000001u)]), // PA_SU_SMALL_PRIM_FILTER_CNTL
new(0u, 42u, 0x144832FBu, [new(0x2F9u, 0x0000002Du)]), // PA_SU_VTX_CNTL
new(0u, 43u, 0x9890D9FAu, [new(0x1BAu, 0x00000000u)]), // SPI_TMPRING_SIZE
new(0u, 44u, 0x9016FAF1u, [new(0x2A6u, 0x00000000u)]), // VGT_DRAW_PAYLOAD_CNTL
new(0u, 45u, 0x4B73CE27u, [new(0x2CEu, 0x00000400u)]), // VGT_GS_MAX_VERT_OUT
new(0u, 46u, 0x5F5A3E7Bu, [new(0x29Bu, 0x00000002u)]), // VGT_GS_OUT_PRIM_TYPE
new(0u, 47u, 0xD4AF3A51u, [new(0x2D6u, 0x00000000u)]), // VGT_LS_HS_CONFIG
new(0u, 48u, 0x6CF4F543u, [new(0x2A3u, 0xFFFFFFFFu)]), // VGT_PRIMITIVEID_RESET
new(0u, 49u, 0x5FB86CCBu, [new(0x2A1u, 0x00000000u)]), // VGT_PRIMITIVEID_EN
new(0u, 50u, 0xEDEFA188u, [new(0x2ADu, 0x00000000u)]), // VGT_REUSE_OFF
new(0u, 51u, 0xD0DE9EE6u, [new(0x2D5u, 0x00000000u)]), // VGT_SHADER_STAGES_EN
new(0u, 52u, 0xC5831803u, [new(0x2D4u, 0x88101000u)]), // VGT_TESS_DISTRIBUTION
new(0u, 53u, 0x8E6DE84Bu, [new(0x2DBu, 0x00000000u)]), // VGT_TF_PARAM
new(0u, 54u, 0xD0771662u, [new(0x2F5u, 0x00000000u), new(0x2F6u, 0x00000000u)]), // PA_SC_CENTROID_PRIORITY_0, PA_SC_CENTROID_PRIORITY_1
new(0u, 55u, 0x569F7444u, [new(0x2FEu, 0x00000000u)]), // PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_0
new(0u, 56u, 0x5C6637CDu, [new(0x30Eu, 0xFFFFFFFFu), new(0x30Fu, 0xFFFFFFFFu)]), // PA_SC_AA_MASK_X0Y0_X1Y0, PA_SC_AA_MASK_X0Y1_X1Y1
new(0u, 57u, 0xCAE3E690u, [new(0x311u, 0x00000002u), new(0x312u, 0x03FF0080u)]), // PA_SC_BINNER_CNTL_0, PA_SC_BINNER_CNTL_1
new(0u, 58u, 0x43FBD769u, [new(0x105u, 0x00000000u), new(0x107u, 0x00000000u), new(0x106u, 0x00000000u), new(0x108u, 0x00000000u)]), // CB_BLEND_RED, CB_BLEND_BLUE, CB_BLEND_GREEN, CB_BLEND_ALPHA
new(0u, 59u, 0xEF550356u, [new(0x1E0u, 0x20010001u)]), // CB_BLEND0_CONTROL
new(0u, 60u, 0x8F52E279u, [new(0x020u, 0x00000000u), new(0x021u, 0x00000000u)]), // TA_BC_BASE_ADDR, TA_BC_BASE_ADDR_HI
new(0u, 61u, 0x1F2D8149u, [new(0x084u, 0x00000000u), new(0x085u, 0x20002000u)]), // PA_SC_CLIPRECT_0_TL, PA_SC_CLIPRECT_0_BR
new(0u, 62u, 0x853D0614u, [new(0x800003FFu, 0x00000000u)]), // CX_NOP
new(0u, 63u, 0x4413C6F9u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // DB_DEPTH_BOUNDS_MIN, DB_DEPTH_BOUNDS_MAX
new(0u, 64u, 0x67096014u, [new(0x010u, 0x80000000u), new(0x011u, 0x20000000u), new(0x012u, 0x00000000u), new(0x013u, 0x00000000u), new(0x014u, 0x00000000u), new(0x015u, 0x00000000u), new(0x01Au, 0x00000000u), new(0x01Bu, 0x00000000u), new(0x01Cu, 0x00000000u), new(0x01Du, 0x00000000u), new(0x01Eu, 0x00000000u), new(0x002u, 0x00000000u), new(0x005u, 0x00000000u), new(0x007u, 0x00000000u), new(0x00Bu, 0x00000000u), new(0x00Au, 0x00000000u)]), // DB_Z_INFO, DB_STENCIL_INFO, DB_Z_READ_BASE, DB_STENCIL_READ_BASE, DB_Z_WRITE_BASE, DB_STENCIL_WRITE_BASE, DB_Z_READ_BASE_HI, DB_STENCIL_READ_BASE_HI, DB_Z_WRITE_BASE_HI, DB_STENCIL_WRITE_BASE_HI, DB_HTILE_DATA_BASE_HI, DB_DEPTH_VIEW, DB_HTILE_DATA_BASE, DB_DEPTH_SIZE_XY, DB_DEPTH_CLEAR, DB_STENCIL_CLEAR
new(0u, 65u, 0x88F5E915u, [new(0x0EBu, 0xFF00FF00u), new(0x0ECu, 0x00000000u)]), // PA_SC_FOV_WINDOW_LR, PA_SC_FOV_WINDOW_TB
new(0u, 66u, 0x033F1EFFu, [new(0x800003FCu, 0x00000000u), new(0x800003FDu, 0x00000000u)]), // FSR_RECURSIONS0, FSR_RECURSIONS1
new(0u, 67u, 0x918106BBu, [new(0x090u, 0x80000000u), new(0x091u, 0x40004000u)]), // PA_SC_GENERIC_SCISSOR_TL, PA_SC_GENERIC_SCISSOR_BR
new(0u, 68u, 0x95F0E7ACu, [new(0x2FAu, 0x4E7E0000u), new(0x2FBu, 0x4E7E0000u), new(0x2FCu, 0x4E7E0000u), new(0x2FDu, 0x4E7E0000u)]), // PA_CL_GB_VERT_CLIP_ADJ, PA_CL_GB_VERT_DISC_ADJ, PA_CL_GB_HORZ_CLIP_ADJ, PA_CL_GB_HORZ_DISC_ADJ
new(0u, 69u, 0xB48CBAB2u, [new(0x2E2u, 0x00000000u), new(0x2E3u, 0x00000000u)]), // PA_SU_POLY_OFFSET_BACK_SCALE, PA_SU_POLY_OFFSET_BACK_OFFSET
new(0u, 70u, 0x05BB3BC6u, [new(0x2E0u, 0x00000000u), new(0x2E1u, 0x00000000u)]), // PA_SU_POLY_OFFSET_FRONT_SCALE, PA_SU_POLY_OFFSET_FRONT_OFFSET
new(0u, 71u, 0x94FABA07u, [new(0x003u, 0x00000000u), new(0x004u, 0x00000000u)]), // DB_RENDER_OVERRIDE, DB_RENDER_OVERRIDE2
new(0u, 72u, 0x38E92C91u, [new(0x318u, 0x00000000u), new(0x31Bu, 0x00000000u), new(0x31Cu, 0x00000000u), new(0x31Du, 0x00000000u), new(0x31Eu, 0x00000048u), new(0x31Fu, 0x00000000u), new(0x321u, 0x00000000u), new(0x323u, 0x00000000u), new(0x324u, 0x00000000u), new(0x325u, 0x00000000u), new(0x390u, 0x00000000u), new(0x398u, 0x00000000u), new(0x3A0u, 0x00000000u), new(0x3A8u, 0x00000000u), new(0x3B0u, 0x00000000u), new(0x3B8u, 0x0006C000u)]), // CB_COLOR0_BASE, CB_COLOR0_VIEW, CB_COLOR0_INFO, CB_COLOR0_ATTRIB, CB_COLOR0_DCC_CONTROL, CB_COLOR0_CMASK, CB_COLOR0_FMASK, CB_COLOR0_CLEAR_WORD0, CB_COLOR0_CLEAR_WORD1, CB_COLOR0_DCC_BASE, CB_COLOR0_BASE_EXT, CB_COLOR0_CMASK_BASE_EXT, CB_COLOR0_FMASK_BASE_EXT, CB_COLOR0_DCC_BASE_EXT, CB_COLOR0_ATTRIB2, CB_COLOR0_ATTRIB3
new(0u, 73u, 0x0B177B43u, [new(0x00Cu, 0x00000000u), new(0x00Du, 0x40004000u)]), // PA_SC_SCREEN_SCISSOR_TL, PA_SC_SCREEN_SCISSOR_BR
new(0u, 74u, 0x48531062u, [new(0x191u, 0x00000000u)]), // SPI_PS_INPUT_CNTL_0
new(0u, 75u, 0xAAA964B9u, [new(0x16Fu, 0x00000000u), new(0x170u, 0x00000000u), new(0x171u, 0x00000000u), new(0x172u, 0x00000000u)]), // PA_CL_UCP_0_X, PA_CL_UCP_0_Y, PA_CL_UCP_0_Z, PA_CL_UCP_0_W
new(0u, 76u, 0x7690AF6Fu, [new(0x10Fu, 0x4E7E0000u), new(0x111u, 0x4E7E0000u), new(0x113u, 0x4E7E0000u), new(0x110u, 0x00000000u), new(0x112u, 0x00000000u), new(0x114u, 0x00000000u), new(0x094u, 0x80000000u), new(0x095u, 0x40004000u), new(0x0B4u, 0x00000000u), new(0x0B5u, 0x00000000u)]), // PA_CL_VPORT_XSCALE, PA_CL_VPORT_YSCALE, PA_CL_VPORT_ZSCALE, PA_CL_VPORT_XOFFSET, PA_CL_VPORT_YOFFSET, PA_CL_VPORT_ZOFFSET, PA_SC_VPORT_SCISSOR_0_TL, PA_SC_VPORT_SCISSOR_0_BR, PA_SC_VPORT_ZMIN_0, PA_SC_VPORT_ZMAX_0
new(0u, 77u, 0x078D7060u, [new(0x081u, 0x80000000u), new(0x082u, 0x40004000u)]), // PA_SC_WINDOW_SCISSOR_TL, PA_SC_WINDOW_SCISSOR_BR
// shader register groups
new(1u, 0u, 0x5D6E3EC7u, [new(0x212u, 0x00000000u)]), // COMPUTE_PGM_RSRC1
new(1u, 1u, 0x57E7079Au, [new(0x213u, 0x00000000u)]), // COMPUTE_PGM_RSRC2
new(1u, 2u, 0x7467FAFDu, [new(0x228u, 0x00000000u)]), // COMPUTE_PGM_RSRC3
new(1u, 3u, 0x9E826B50u, [new(0x215u, 0x00000000u)]), // COMPUTE_RESOURCE_LIMITS
new(1u, 4u, 0xDC484F18u, [new(0x218u, 0x00000000u)]), // COMPUTE_TMPRING_SIZE
new(1u, 5u, 0x5DA8BCA3u, [new(0x08Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_GS
new(1u, 6u, 0x5CA726D8u, [new(0x10Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_HS
new(1u, 7u, 0x5DD28360u, [new(0x00Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_PS
new(1u, 8u, 0x57EFA0BEu, [new(0x08Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_GS
new(1u, 9u, 0x502363D5u, [new(0x10Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_HS
new(1u, 10u, 0x506D14BDu, [new(0x00Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_PS
new(1u, 11u, 0xB2609506u, [new(0x224u, 0x00000000u)]), // COMPUTE_USER_ACCUM_0
new(1u, 12u, 0x9E5CFB8Au, [new(0x107u, 0x00000000u), new(0x087u, 0x00000000u), new(0x007u, 0x00000000u)]), // SPI_SHADER_PGM_RSRC3_HS, SPI_SHADER_PGM_RSRC3_GS, SPI_SHADER_PGM_RSRC3_PS
new(1u, 13u, 0xC918DF3Eu, [new(0x20Cu, 0x00000000u), new(0x20Du, 0x00000000u)]), // COMPUTE_PGM_LO, COMPUTE_PGM_HI
new(1u, 14u, 0xC9751C9Cu, [new(0x0C8u, 0x00000000u), new(0x0C9u, 0x00000000u)]), // SPI_SHADER_PGM_LO_ES, SPI_SHADER_PGM_HI_ES
new(1u, 15u, 0xC97EF77Au, [new(0x088u, 0x00000000u), new(0x089u, 0x00000000u)]), // SPI_SHADER_PGM_LO_GS, SPI_SHADER_PGM_HI_GS
new(1u, 16u, 0xC927C6B9u, [new(0x108u, 0x00000000u), new(0x109u, 0x00000000u)]), // SPI_SHADER_PGM_LO_HS, SPI_SHADER_PGM_HI_HS
new(1u, 17u, 0xC92A1EC5u, [new(0x148u, 0x00000000u), new(0x149u, 0x00000000u)]), // SPI_SHADER_PGM_LO_LS, SPI_SHADER_PGM_HI_LS
new(1u, 18u, 0xC9E01B31u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // SPI_SHADER_PGM_LO_PS, SPI_SHADER_PGM_HI_PS
new(1u, 19u, 0x50685F29u, [new(0x800002FFu, 0x00000000u)]), // SH_NOP
new(1u, 20u, 0xB26219CAu, [new(0x0B2u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_ESGS_0
new(1u, 21u, 0xB25B6CF9u, [new(0x132u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_LSHS_0
new(1u, 22u, 0xB2F86101u, [new(0x032u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_PS_0
new(1u, 23u, 0x07E3B155u, [new(0x082u, 0x00000000u), new(0x083u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_GS, SPI_SHADER_USER_DATA_ADDR_HI_GS
new(1u, 24u, 0x07E383C6u, [new(0x102u, 0x00000000u), new(0x103u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_HS, SPI_SHADER_USER_DATA_ADDR_HI_HS
new(1u, 25u, 0xBDA98653u, [new(0x240u, 0x00000000u)]), // COMPUTE_USER_DATA_0
new(1u, 26u, 0xBDBD1D0Fu, [new(0x08Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_GS_0
new(1u, 27u, 0xBD946FD4u, [new(0x10Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_HS_0
new(1u, 28u, 0xBDF02A4Cu, [new(0x00Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_PS_0
// uconfig register groups
new(2u, 0u, 0x19E93E85u, [new(0x41Fu, 0x00000000u)]), // GDS_OA_ADDRESS
new(2u, 1u, 0x3B5C2AF3u, [new(0x41Du, 0x00000000u)]), // GDS_OA_CNTL
new(2u, 2u, 0x47974A35u, [new(0x41Eu, 0x00000000u)]), // GDS_OA_COUNTER
new(2u, 3u, 0x105971C2u, [new(0x25Bu, 0x00000000u)]), // GE_CNTL
new(2u, 4u, 0x7D137765u, [new(0x24Au, 0x00000000u)]), // GE_INDX_OFFSET
new(2u, 5u, 0xD187FEBCu, [new(0x24Bu, 0x00000000u)]), // GE_MULTI_PRIM_IB_RESET_EN
new(2u, 6u, 0x12F854ACu, [new(0x25Fu, 0x00000000u)]), // GE_STEREO_CNTL
new(2u, 7u, 0x40D49AD1u, [new(0x262u, 0x00000000u)]), // GE_USER_VGPR_EN
new(2u, 8u, 0x8C0923DAu, [new(0x80003FF4u, 0x00000000u)]), // FSR_EXTEND_SUBPIXEL_ROUNDING
new(2u, 9u, 0xBB8DF494u, [new(0x80003FFDu, 0x00000000u)]), // TEXTURE_GRADIENT_CONTROL
new(2u, 10u, 0xF6D8A76Eu, [new(0x382u, 0x40000040u)]), // TEXTURE_GRADIENT_FACTORS
new(2u, 11u, 0x7620F1E9u, [new(0x248u, 0x00000000u)]), // VGT_OBJECT_ID
new(2u, 12u, 0x9EBFAB10u, [new(0x242u, 0x00000000u)]), // VGT_PRIMITIVE_TYPE
new(2u, 13u, 0x98A09D0Eu, [new(0x380u, 0x00000000u), new(0x381u, 0x00000000u)]), // TA_CS_BC_BASE_ADDR, TA_CS_BC_BASE_ADDR_HI
new(2u, 14u, 0x195D37D2u, [new(0x80003FF5u, 0x00000000u), new(0x80003FF6u, 0x00000000u)]), // FSR_ALPHA_VALUE0, FSR_ALPHA_VALUE1
new(2u, 15u, 0xF9EC4F85u, [new(0x80003FF7u, 0x00000000u), new(0x80003FF8u, 0x00000000u), new(0x80003FF9u, 0x00000000u), new(0x80003FFAu, 0x00000000u)]), // FSR_CONTROL_POINT0, FSR_CONTROL_POINT1, FSR_CONTROL_POINT2, FSR_CONTROL_POINT3
new(2u, 16u, 0x4626B750u, [new(0x80003FFBu, 0x00000000u), new(0x80003FFCu, 0x00000000u)]), // FSR_WINDOW0, FSR_WINDOW1
new(2u, 17u, 0x4CC673A0u, [new(0x80003FFEu, 0x00000000u)]), // MEMORY_MAPPING_MASK
new(2u, 18u, 0xDE5B3431u, [new(0x80003FFFu, 0x00000000u)]), // UC_NOP
new(2u, 19u, 0x036AC8A6u, [new(0x25Cu, 0x00000000u)]), // GE_USER_VGPR1
];
}
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.VideoOut;
using SharpEmu.ShaderCompiler;
namespace SharpEmu.Libs.Agc;
@@ -26,5 +27,7 @@ internal static class AgcShaderCompilerHooks
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
VulkanVideoPresenter.GuestDataPool;
}
}
+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.");
}
}
+179 -16
View File
@@ -3,10 +3,17 @@
namespace SharpEmu.Libs.Agc;
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
// AgcExports re-checks every waiter against guest memory on each submit and resumes
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
// DmaData packets or by direct CPU writes).
/// <summary>
/// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM
/// condition. AgcExports re-checks every waiter against guest memory on each
/// submit and resumes the ones whose condition became true (labels are advanced
/// by ReleaseMem / WriteData / DmaData packets, or by direct CPU writes).
///
/// This preserves cross-submit ordering: the work that follows a wait inside a
/// DCB is only queued once the awaited completion label is genuinely written,
/// instead of being force-satisfied at parse time and running ahead of the
/// compute/graphics work it depends on (which produced a black composite).
/// </summary>
internal static class GpuWaitRegistry
{
public struct WaitingDcb
@@ -19,13 +26,56 @@ internal static class GpuWaitRegistry
public ulong ReferenceValue;
public ulong Mask;
public uint CompareFunction;
public uint ControlValue;
public bool Is64Bit;
public bool IsStandard;
public object? Memory;
public string? QueueName;
public ulong SubmissionId;
// Stopwatch timestamp captured at registration. Stale waiters remain
// registered; this only controls one-shot diagnostics.
public long RegisteredTicks;
public bool StaleReported;
public object? State;
}
private static readonly object _gate = new();
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
public static int Count
{
get
{
lock (_gate)
{
var total = 0;
foreach (var (_, list) in _waiters)
{
total += list.Count;
}
return total;
}
}
}
public static int CountForMemory(object memory)
{
lock (_gate)
{
var total = 0;
foreach (var (_, list) in _waiters)
{
foreach (var waiter in list)
{
total += ReferenceEquals(waiter.Memory, memory) ? 1 : 0;
}
}
return total;
}
}
public static void Register(ulong address, WaitingDcb waiter)
{
waiter.WaitAddress = address;
@@ -41,9 +91,15 @@ internal static class GpuWaitRegistry
}
}
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
// returns null when the memory is unreadable; such waiters are kept registered.
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
/// <summary>
/// Re-evaluates every registered waiter. <paramref name="readValue"/>
/// receives (address, is64Bit) and returns null when the memory is
/// unreadable; such waiters are kept registered. Returns the waiters whose
/// condition is now satisfied (removed from the registry), or null.
/// </summary>
public static List<WaitingDcb>? CollectSatisfied(
object memory,
Func<ulong, bool, ulong?> readValue)
{
List<WaitingDcb>? woken = null;
lock (_gate)
@@ -53,6 +109,11 @@ internal static class GpuWaitRegistry
{
for (var i = list.Count - 1; i >= 0; i--)
{
if (!ReferenceEquals(list[i].Memory, memory))
{
continue;
}
var value = readValue(address, list[i].Is64Bit);
if (value is null || !Compare(list[i], value.Value))
{
@@ -71,7 +132,7 @@ internal static class GpuWaitRegistry
}
}
if (emptied != null)
if (emptied is not null)
{
foreach (var address in emptied)
{
@@ -83,20 +144,122 @@ internal static class GpuWaitRegistry
return woken;
}
/// <summary>
/// Returns waiters that have remained unsatisfied longer than
/// <paramref name="maxAgeTicks"/> exactly once, without removing them or
/// changing their labels. Missing GPU work must fail closed: advancing a
/// command buffer without its real producer corrupts cross-queue ordering.
/// </summary>
public static List<WaitingDcb>? CollectUnreportedStale(
object memory,
long nowTicks,
long maxAgeTicks)
{
List<WaitingDcb>? stale = null;
lock (_gate)
{
foreach (var (_, list) in _waiters)
{
for (var i = list.Count - 1; i >= 0; i--)
{
var waiter = list[i];
if (!ReferenceEquals(waiter.Memory, memory) ||
waiter.StaleReported ||
nowTicks - waiter.RegisteredTicks < maxAgeTicks)
{
continue;
}
stale ??= new List<WaitingDcb>();
waiter.StaleReported = true;
list[i] = waiter;
stale.Add(waiter);
}
}
}
return stale;
}
/// <summary>
/// Returns watched labels overlapped by a newly discovered producer. Used
/// only for diagnostics; producer completion still wakes through the
/// normal CollectSatisfied path after the ordered memory write executes.
/// </summary>
public static List<(ulong Address, int Count)> SnapshotInRange(
object memory,
ulong start,
ulong length)
{
var matches = new List<(ulong Address, int Count)>();
if (length == 0)
{
return matches;
}
var end = start > ulong.MaxValue - length ? ulong.MaxValue : start + length;
lock (_gate)
{
foreach (var (address, list) in _waiters)
{
var matchingCount = 0;
var any64Bit = false;
foreach (var waiter in list)
{
if (!ReferenceEquals(waiter.Memory, memory))
{
continue;
}
matchingCount++;
any64Bit |= waiter.Is64Bit;
}
if (matchingCount == 0)
{
continue;
}
var width = any64Bit
? sizeof(ulong)
: sizeof(uint);
var waitEnd = address > ulong.MaxValue - (ulong)width
? ulong.MaxValue
: address + (ulong)width;
if (start < waitEnd && address < end)
{
matches.Add((address, matchingCount));
}
}
}
return matches;
}
public static bool Compare(in WaitingDcb waiter, ulong value)
{
var masked = value & waiter.Mask;
var reference = waiter.ReferenceValue & waiter.Mask;
return waiter.CompareFunction switch
{
1 => masked < waiter.ReferenceValue,
2 => masked <= waiter.ReferenceValue,
3 => masked == waiter.ReferenceValue,
4 => masked != waiter.ReferenceValue,
5 => masked >= waiter.ReferenceValue,
6 => masked > waiter.ReferenceValue,
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
// satisfied keeps a malformed packet from suspending its DCB forever.
0 => true,
1 => masked < reference,
2 => masked <= reference,
3 => masked == reference,
4 => masked != reference,
5 => masked >= reference,
6 => masked > reference,
// 7 is reserved; treating it as satisfied keeps a malformed packet
// from suspending forever.
_ => true,
};
}
public static void Clear()
{
lock (_gate)
{
_waiters.Clear();
}
}
}