[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();
}
}
}
+2 -22
View File
@@ -7,31 +7,11 @@ namespace SharpEmu.Libs.Ajm;
public static class AjmExports
{
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
private static int _nextContextId;
[SysAbiExport(
Nid = "dl+4eHSzUu4",
ExportName = "sceAjmInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int Initialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outContextAddress = ctx[CpuRegister.Rsi];
// AJM requires a zero reserved argument.
if (reserved != 0 || outContextAddress == 0)
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
return ctx.SetReturn(0);
}
public static int Initialize(CpuContext ctx) =>
SharpEmu.Libs.Audio.AjmExports.AjmInitialize(ctx);
}
+51 -39
View File
@@ -36,6 +36,7 @@ public static class AmprExports
public ulong Buffer;
public ulong Size;
public ulong WriteOffset;
public ulong CommandCount;
}
private sealed class CachedHostFile
@@ -267,53 +268,19 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong bytesRead = 0;
// Unregistered/missing files are zero-filled instead of failing: games queue
// speculative reads and only consume the bytes on success paths.
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
{
if (destination != 0 && size > 0)
{
int chunkSize = (int)Math.Min(size, 4096);
Span<byte> zeros = stackalloc byte[chunkSize];
zeros.Clear();
while (bytesRead < size)
{
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
{
break;
}
bytesRead += (ulong)currentChunk;
}
}
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
// Offset -1 means "continue after the previous read of this file id".
if (fileOffset == unchecked((ulong)(long)-1))
{
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
}
else if (fileOffset > long.MaxValue)
{
fileOffset = 0;
}
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
return result;
}
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -418,6 +385,35 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "gzndltBEzWc",
ExportName = "sceAmprCommandBufferGetNumCommands",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferGetNumCommands(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong commandCount;
lock (state)
{
commandCount = state.CommandCount;
}
TraceAmpr(ctx, "get_num_commands", commandBuffer, commandCount, 0);
ctx[CpuRegister.Rax] = commandCount;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "H896Pt-yB4I",
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
@@ -533,7 +529,7 @@ public static class AmprExports
var offset = 0UL;
while (offset < writeOffset)
{
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -649,6 +645,7 @@ public static class AmprExports
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = writeOffset;
state.CommandCount = 0;
}
}
@@ -681,6 +678,7 @@ public static class AmprExports
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = 0;
state.CommandCount = 0;
}
return true;
@@ -911,6 +909,7 @@ public static class AmprExports
}
state.WriteOffset += recordSize;
state.CommandCount++;
}
return true;
@@ -963,6 +962,19 @@ public static class AmprExports
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
{
if (!_traceAmpr)
+104
View File
@@ -0,0 +1,104 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Audio;
public static class AjmExports
{
private static readonly ConcurrentDictionary<uint, byte> Contexts = new();
private static int _nextContextId;
public static int AjmInitialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outputAddress = ctx[CpuRegister.Rsi];
if (reserved != 0 || outputAddress == 0)
{
return unchecked((int)0x806A0001);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
Span<byte> value = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(value, contextId);
if (!ctx.Memory.TryWrite(outputAddress, value))
{
return unchecked((int)0x806A0001);
}
Contexts[contextId] = 0;
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] ajm.initialize reserved={reserved} out=0x{outputAddress:X16} context={contextId}");
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "MHur6qCsUus",
ExportName = "sceAjmFinalize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmFinalize(CpuContext ctx)
{
Contexts.TryRemove(unchecked((uint)ctx[CpuRegister.Rdi]), out _);
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "Q3dyFuwGn64",
ExportName = "sceAjmModuleRegister",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmModuleRegister(CpuContext ctx)
{
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
var reserved = ctx[CpuRegister.Rdx];
if (reserved != 0 || !Contexts.ContainsKey(contextId))
{
return unchecked((int)0x806A0001);
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] ajm.module_register context={contextId} codec={codecType} reserved={reserved}");
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "Wi7DtlLV+KI",
ExportName = "sceAjmModuleUnregister",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmModuleUnregister(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "MmpF1XsQiHw",
ExportName = "sceAjmBatchInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmBatchInitialize(CpuContext ctx)
{
// The caller owns and initializes the batch storage. This API resets
// its submission cursor on hardware; FMOD does not consume a return
// value or an additional output object here.
ctx[CpuRegister.Rax] = 0;
return 0;
}
}
+214 -39
View File
@@ -3,22 +3,70 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
namespace SharpEmu.Libs.Audio;
public static class AudioOut2Exports
{
// Sized from guest evidence, not SDK headers: Quake keeps its
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
// and an earlier 0x80-byte ResetParam write zeroed that canary
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
// the whole audio init). Stay well below 0x60 and only write the prefix we
// populate.
private const int AudioOut2ContextParamSize = 0x30;
// FMOD's PS5 backend allocates this ABI structure as four 16-byte lanes.
// Clearing 0x80 bytes here overwrote the caller's stack canary immediately
// following the 0x40-byte parameter block.
private const int AudioOut2ContextParamSize = 0x40;
private const int AudioOut2ContextMemorySize = 0x10000;
private const int AudioOut2ContextMemoryAlignment = 0x10000;
private static long _nextContextHandle = 1;
private static long _nextUserHandle = 1;
private static int _nextPortId;
private static long _pushTraceCount;
// Per-context audio parameters captured at ContextCreate so ContextAdvance
// can pace to the real playback cadence (grain samples at the sample rate).
private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new();
private sealed class ContextState
{
private readonly object _paceGate = new();
private long _nextAdvanceTimestamp;
public ContextState(uint frequency, uint channels, uint grainSamples)
{
Frequency = frequency == 0 ? 48000 : frequency;
Channels = channels == 0 ? 2 : channels;
GrainSamples = grainSamples == 0 ? 256 : grainSamples;
}
public uint Frequency { get; }
public uint Channels { get; }
public uint GrainSamples { get; }
// Blocks the advancing thread until one grain worth of wall-clock time
// has elapsed since the previous advance, matching hardware timing so
// audio-gated titles neither spin nor drift ahead.
public void PaceAdvance()
{
long delay;
lock (_paceGate)
{
var now = Stopwatch.GetTimestamp();
if (_nextAdvanceTimestamp < now)
{
_nextAdvanceTimestamp = now;
}
delay = _nextAdvanceTimestamp - now;
_nextAdvanceTimestamp += checked(
(long)Math.Ceiling(Stopwatch.Frequency * (double)GrainSamples / Frequency));
}
if (delay > 0)
{
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
}
}
}
[SysAbiExport(
Nid = "g2tViFIohHE",
@@ -41,7 +89,7 @@ public static class AudioOut2Exports
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
@@ -52,8 +100,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
return ctx.Memory.TryWrite(paramAddress, param)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -64,15 +112,22 @@ public static class AudioOut2Exports
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || outMemorySizeAddress == 0)
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
Span<byte> memoryInfo = stackalloc byte[0x20];
memoryInfo.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -88,13 +143,34 @@ public static class AudioOut2Exports
var outContextAddress = ctx[CpuRegister.Rcx];
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// Read channels/frequency/grain from the reset-param blob so the
// context can pace advances to the real audio cadence.
uint channels = 2;
uint frequency = 48000;
uint grain = 256;
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
if (ctx.Memory.TryRead(paramAddress, param))
{
var pc = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
var pf = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]);
if (pc is > 0 and <= 8) channels = pc;
if (pf is >= 8000 and <= 192000) frequency = pf;
// Values below one cache line are flags/counts in observed PS5
// callers, not audio grains. Keep the hardware-sized default.
if (pg is >= 64 and <= 0x4000) grain = pg;
TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}");
}
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
return ctx.TryWriteUInt64(outContextAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
Contexts[handle] = new ContextState(frequency, channels, grain);
TraceAudioOut2($"context-create handle=0x{handle:X} frequency={frequency} channels={channels} grain={grain} memory=0x{memoryAddress:X} size=0x{memorySize:X}");
return TryWriteUInt64(ctx, outContextAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -102,7 +178,77 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2ContextDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2ContextDestroy(CpuContext ctx)
{
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "DxGyV8dtOR8",
ExportName = "sceAudioOut2ContextBedWrite",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "aII9h5nli9U",
ExportName = "sceAudioOut2ContextPush",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextPush(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var traceCount = Interlocked.Increment(ref _pushTraceCount);
if (traceCount <= 16)
{
TraceAudioOut2($"context-push count={traceCount} rdi=0x{handle:X} rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{ctx[CpuRegister.Rcx]:X}");
}
if (Contexts.TryGetValue(handle, out var context))
{
// FMOD's PS5 output path uses ContextPush as the submission clock
// and does not call ContextAdvance. Pace pushes to one hardware
// grain so the feeder cannot outrun playback and starve the game.
context.PaceAdvance();
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "PE2zHMqLSHs",
ExportName = "sceAudioOut2ContextAdvance",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextAdvance(CpuContext ctx)
{
// Advancing renders one grain of audio on hardware; pace it to the same
// wall-clock cadence so the guest audio thread runs at the right speed.
if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var context))
{
context.PaceAdvance();
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "R7d0F1g2qsU",
ExportName = "sceAudioOut2ContextGetQueueLevel",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextGetQueueLevel(CpuContext ctx)
{
// The advance path paces synchronously, so the queue is always drained.
var levelAddress = ctx[CpuRegister.Rsi];
if (levelAddress != 0)
{
_ = TryWriteUInt64(ctx, levelAddress, 0);
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "JK2wamZPzwM",
@@ -117,16 +263,23 @@ public static class AudioOut2Exports
var contextAddress = ctx[CpuRegister.Rcx];
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return ctx.TryWriteUInt64(outPortAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return TryWriteUInt64(ctx, outPortAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "8XTArSPyWHk",
ExportName = "sceAudioOut2PortSetAttributes",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "gatEUKG+Ea4",
ExportName = "sceAudioOut2PortGetState",
@@ -138,7 +291,7 @@ public static class AudioOut2Exports
var stateAddress = ctx[CpuRegister.Rsi];
if (handle == 0 || stateAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var type = (int)((handle >> 16) & 0xFF);
@@ -151,8 +304,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
return ctx.Memory.TryWrite(stateAddress, state)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -165,7 +318,7 @@ public static class AudioOut2Exports
var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> info = stackalloc byte[0x40];
@@ -175,8 +328,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
return ctx.Memory.TryWrite(infoAddress, info)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -184,14 +337,14 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "IaZXJ9M79uo",
ExportName = "sceAudioOut2UserDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "xywYcRB7nbQ",
@@ -202,14 +355,36 @@ public static class AudioOut2Exports
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 0x10000000 && userId != 255) ||
outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return ctx.TryWriteUInt64(outUserAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return TryWriteUInt64(ctx, outUserAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceAudioOut2(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT2"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] audio_out2.{message}");
}
}
}
+40 -5
View File
@@ -50,6 +50,7 @@ public static class AudioOutExports
public int BytesPerSample { get; }
public bool IsFloat { get; }
public IHostAudioStream? Backend { get; }
public volatile float Volume = 1.0f;
public int BufferByteLength =>
checked((int)BufferLength * Channels * BytesPerSample);
@@ -198,7 +199,8 @@ public static class AudioOutExports
checked((int)port.BufferLength),
port.Channels,
port.BytesPerSample,
port.IsFloat);
port.IsFloat,
port.Volume);
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
{
port.PaceSilence();
@@ -225,10 +227,43 @@ public static class AudioOutExports
public static int AudioOutSetVolume(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return ctx.SetReturn(
Ports.ContainsKey(handle)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
var channelFlags = unchecked((uint)ctx[CpuRegister.Rsi]);
var volumeArrayAddress = ctx[CpuRegister.Rdx];
if (!Ports.TryGetValue(handle, out var port))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
const int unityVolume = 32768;
var maxVolume = 0;
var found = false;
if (volumeArrayAddress != 0)
{
Span<byte> raw = stackalloc byte[sizeof(int)];
for (var channel = 0; channel < 8; channel++)
{
if ((channelFlags & (1u << channel)) == 0)
{
continue;
}
if (!ctx.Memory.TryRead(volumeArrayAddress + (ulong)(channel * sizeof(int)), raw))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var value = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(raw);
maxVolume = Math.Max(maxVolume, value);
found = true;
}
}
if (found)
{
port.Volume = Math.Clamp(maxVolume / (float)unityVolume, 0f, 1f);
}
return ctx.SetReturn(0);
}
public static void ShutdownAllPorts()
+10 -1
View File
@@ -21,7 +21,8 @@ internal static class AudioPcmConversion
int frames,
int channels,
int bytesPerSample,
bool isFloat)
bool isFloat,
float volume)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
@@ -31,6 +32,8 @@ internal static class AudioPcmConversion
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
left = ApplyVolume(left, volume);
right = ApplyVolume(right, volume);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
}
@@ -52,4 +55,10 @@ internal static class AudioPcmConversion
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
private static short ApplyVolume(short sample, float volume)
{
var scaled = MathF.Round(sample * Math.Clamp(volume, 0.0f, 1.0f));
return (short)Math.Clamp(scaled, short.MinValue, short.MaxValue);
}
}
@@ -0,0 +1,47 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Audio;
public static class FmodCompatExports
{
private static readonly ConcurrentDictionary<ulong, int> SetOutputCalls = new();
[SysAbiExport(
Nid = "uPLTdl3psGk",
Target = Generation.Gen5,
LibraryName = "libfmod")]
public static int FmodSystemSetOutput(CpuContext ctx)
{
var system = ctx[CpuRegister.Rdi];
var output = unchecked((int)ctx[CpuRegister.Rsi]);
var callCount = SetOutputCalls.AddOrUpdate(system, 1, static (_, count) => count + 1);
var resetPrematureInit = callCount <= 2;
if (resetPrematureInit && system != 0)
{
Span<byte> zeroByte = stackalloc byte[1];
Span<byte> outputBytes = stackalloc byte[sizeof(int)];
Span<byte> zeroInt = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(outputBytes, output);
_ = ctx.Memory.TryWrite(system + 0x08, zeroByte);
_ = ctx.Memory.TryWrite(system + 0x116D0, outputBytes);
_ = ctx.Memory.TryWrite(system + 0x116D4, zeroInt);
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FMOD"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fmod.system_set_output system=0x{system:X16} output={output} call={callCount} reset_premature_init={resetPrematureInit} -> 0");
}
// The PS5 FMOD object arrives with its initialized byte set before Unity has
// applied the startup output configuration. Preserve those settings while
// clearing the premature marker so Studio can execute the real core init.
ctx[CpuRegister.Rax] = 0;
return 0;
}
}
File diff suppressed because it is too large Load Diff
+403
View File
@@ -0,0 +1,403 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Bink;
/// <summary>
/// Optional host-side Bink 2 bridge for games that ship a static Bink player.
///
/// The game in that case never imports libSceVideodec, so an HLE video-decoder
/// export cannot see its movie frames. Kernel file opens identify the active
/// .bk2 file and the presenter requests BGRA frames from a tiny native adapter.
/// The adapter is deliberately a separate, user-supplied library: Bink 2 is a
/// proprietary SDK and SharpEmu must neither bundle it nor depend on its ABI.
/// </summary>
internal static class Bink2MovieBridge
{
private const uint MaxDimension = 16384;
private static readonly object Gate = new();
private static NativeAdapter? _adapter;
private static string? _activePath;
private static IntPtr _activeMovie;
private static Bink2MovieInfo _activeInfo;
private static byte[]? _frameBuffer;
private static bool _usingDummyMovie;
private static bool _loadAttempted;
private static bool _availabilityReported;
/// <summary>
/// Returns true when the guest should receive a normal "file not found"
/// result for a Bink movie. This is the safe default without a decoder:
/// games that treat movies as optional fall through to their next state
/// rather than submitting an empty Bink GPU texture forever.
/// </summary>
internal static bool ShouldSkipGuestMovie(string hostPath) =>
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
ResolveMode() == MovieMode.Skip;
internal static void ObserveGuestMovie(string hostPath)
{
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
!File.Exists(hostPath))
{
return;
}
lock (Gate)
{
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
{
return;
}
if (ResolveMode() == MovieMode.Dummy)
{
AttachDummyMovieLocked(hostPath);
return;
}
var adapter = GetAdapterLocked();
if (adapter is null)
{
return;
}
CloseActiveLocked();
if (!adapter.TryOpen(hostPath, out var movie, out var info))
{
Console.Error.WriteLine(
"[LOADER][WARN] Bink2 bridge could not open movie '" +
Path.GetFileName(hostPath) + "'.");
return;
}
if (!IsValid(info))
{
adapter.Close(movie);
Console.Error.WriteLine(
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
Path.GetFileName(hostPath) + "'.");
return;
}
_activePath = hostPath;
_activeMovie = movie;
_activeInfo = info;
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
Console.Error.WriteLine(
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
info.Width + "x" + info.Height + " @ " +
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
}
}
internal static bool TryDecodeNextFrame(
out byte[] pixels,
out uint width,
out uint height)
{
lock (Gate)
{
pixels = [];
width = 0;
height = 0;
if (_adapter is null || _activeMovie == IntPtr.Zero || _frameBuffer is null)
{
if (_usingDummyMovie && _frameBuffer is not null)
{
pixels = _frameBuffer;
width = _activeInfo.Width;
height = _activeInfo.Height;
return true;
}
return false;
}
unsafe
{
fixed (byte* destination = _frameBuffer)
{
if (!_adapter.DecodeNextBgra(
_activeMovie,
(IntPtr)destination,
_activeInfo.Width * 4,
(uint)_frameBuffer.Length))
{
return false;
}
}
}
pixels = _frameBuffer;
width = _activeInfo.Width;
height = _activeInfo.Height;
return true;
}
}
private static bool IsValid(Bink2MovieInfo info) =>
info.Width > 0 && info.Height > 0 &&
info.Width <= MaxDimension && info.Height <= MaxDimension &&
(ulong)info.Width * info.Height * 4 <= int.MaxValue;
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
checked((int)((ulong)info.Width * info.Height * 4));
private static MovieMode ResolveMode()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
if (string.Equals(configured, "dummy", StringComparison.OrdinalIgnoreCase))
{
return MovieMode.Dummy;
}
if (string.Equals(configured, "native", StringComparison.OrdinalIgnoreCase))
{
return MovieMode.Native;
}
if (string.Equals(configured, "skip", StringComparison.OrdinalIgnoreCase))
{
return MovieMode.Skip;
}
// With no SDK adapter present, returning "not found" makes optional
// cinematics advance. Supplying either an explicit path or the normal
// side-by-side adapter enables native playback automatically.
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
EnumerateAdapterCandidates().Any(File.Exists))
{
return MovieMode.Native;
}
return MovieMode.Skip;
}
private static void AttachDummyMovieLocked(string hostPath)
{
if (!TryReadBinkInfo(hostPath, out var info) || !IsValid(info))
{
Console.Error.WriteLine(
"[LOADER][WARN] Bink dummy could not read movie header '" +
Path.GetFileName(hostPath) + "'.");
return;
}
CloseActiveLocked();
_activePath = hostPath;
_activeInfo = info;
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
FillDummyFrame(_frameBuffer, info.Width, info.Height);
_usingDummyMovie = true;
Console.Error.WriteLine(
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
info.Width + "x" + info.Height + ".");
}
private static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
{
info = default;
Span<byte> header = stackalloc byte[32];
try
{
using var stream = File.OpenRead(path);
if (stream.Read(header) != header.Length ||
!header[..4].SequenceEqual("KB2j"u8))
{
return false;
}
info = new Bink2MovieInfo(
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
1);
return true;
}
catch (IOException)
{
return false;
}
}
private static void FillDummyFrame(byte[] pixels, uint width, uint height)
{
for (var y = 0u; y < height; y++)
{
for (var x = 0u; x < width; x++)
{
var offset = checked((int)(((ulong)y * width + x) * 4));
var band = ((x / 96) + (y / 96)) & 1;
pixels[offset] = band == 0 ? (byte)0x28 : (byte)0x18;
pixels[offset + 1] = band == 0 ? (byte)0x18 : (byte)0x28;
pixels[offset + 2] = 0x10;
pixels[offset + 3] = 0xFF;
}
}
}
private static NativeAdapter? GetAdapterLocked()
{
if (_loadAttempted)
{
return _adapter;
}
_loadAttempted = true;
foreach (var candidate in EnumerateAdapterCandidates())
{
if (!NativeLibrary.TryLoad(candidate, out var library))
{
continue;
}
if (NativeAdapter.TryCreate(library, out var adapter))
{
_adapter = adapter;
Console.Error.WriteLine("[LOADER][INFO] Bink2 bridge loaded: " + candidate);
return adapter;
}
NativeLibrary.Free(library);
}
if (!_availabilityReported)
{
_availabilityReported = true;
Console.Error.WriteLine(
"[LOADER][INFO] Bink2 bridge unavailable; install the licensed adapter and set SHARPEMU_BINK2_BRIDGE.");
}
return null;
}
private static IEnumerable<string> EnumerateAdapterCandidates()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE");
if (!string.IsNullOrWhiteSpace(configured))
{
yield return configured;
}
var baseDirectory = AppContext.BaseDirectory;
if (OperatingSystem.IsMacOS())
{
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.dylib");
}
else if (OperatingSystem.IsWindows())
{
yield return Path.Combine(baseDirectory, "sharpemu_bink2_bridge.dll");
}
else
{
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.so");
}
}
private static void CloseActiveLocked()
{
if (_activeMovie != IntPtr.Zero)
{
_adapter?.Close(_activeMovie);
}
_activePath = null;
_activeMovie = IntPtr.Zero;
_activeInfo = default;
_frameBuffer = null;
_usingDummyMovie = false;
}
[StructLayout(LayoutKind.Sequential)]
private readonly struct Bink2MovieInfo
{
public readonly uint Width;
public readonly uint Height;
public readonly uint FramesPerSecondNumerator;
public readonly uint FramesPerSecondDenominator;
internal Bink2MovieInfo(
uint width,
uint height,
uint framesPerSecondNumerator,
uint framesPerSecondDenominator)
{
Width = width;
Height = height;
FramesPerSecondNumerator = framesPerSecondNumerator;
FramesPerSecondDenominator = framesPerSecondDenominator;
}
}
private enum MovieMode
{
Skip,
Dummy,
Native,
}
private sealed class NativeAdapter
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int DecodeNextBgraDelegate(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void CloseDelegate(IntPtr movie);
private readonly OpenUtf8Delegate _openUtf8;
private readonly DecodeNextBgraDelegate _decodeNextBgra;
private readonly CloseDelegate _close;
private NativeAdapter(
OpenUtf8Delegate openUtf8,
DecodeNextBgraDelegate decodeNextBgra,
CloseDelegate close)
{
_openUtf8 = openUtf8;
_decodeNextBgra = decodeNextBgra;
_close = close;
}
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
{
adapter = null;
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
{
return false;
}
adapter = new NativeAdapter(
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
return true;
}
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
{
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
try
{
return _openUtf8(utf8, out movie, out info) != 0 && movie != IntPtr.Zero;
}
finally
{
Marshal.FreeCoTaskMem(utf8);
}
}
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
internal void Close(IntPtr movie) => _close(movie);
}
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Codec;
/// <summary>
/// libSceVideodec / libSceAudiodec handle management. Actual H.264/HEVC and
/// AAC/AT9 decoding requires an external codec, which is out of scope; these
/// exports keep the decoder lifecycle resolvable (create/decode/flush/delete)
/// and report "no output produced" so guests advance instead of failing on
/// unresolved imports.
/// </summary>
public static class CodecExports
{
private const int Ok = 0;
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
private const int AudiodecErrorInvalidArg = unchecked((int)0x807F0002);
private static readonly ConcurrentDictionary<ulong, byte> VideoDecoders = new();
private static readonly ConcurrentDictionary<ulong, byte> AudioDecoders = new();
private static long _nextHandle = 1;
// ---- Video decoder ----
[SysAbiExport(Nid = "qkgRiwHyheU", ExportName = "sceVideodecCreateDecoder",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
public static int VideodecCreateDecoder(CpuContext ctx)
{
var outHandleAddress = ctx[CpuRegister.Rdx];
if (outHandleAddress == 0)
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
VideoDecoders[handle] = 1;
return TryWriteHandle(ctx, outHandleAddress, handle) ? SetReturn(ctx, Ok) : SetReturn(ctx, VideodecErrorInvalidArg);
}
[SysAbiExport(Nid = "q0W5GJMovMs", ExportName = "sceVideodecDecode",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
public static int VideodecDecode(CpuContext ctx)
{
// No decoder is present: report success with no picture produced.
return SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
}
[SysAbiExport(Nid = "jeigLlKdp5I", ExportName = "sceVideodecFlush",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
public static int VideodecFlush(CpuContext ctx) =>
SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
[SysAbiExport(Nid = "U0kpGF1cl90", ExportName = "sceVideodecDeleteDecoder",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
public static int VideodecDeleteDecoder(CpuContext ctx)
{
VideoDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetReturn(ctx, Ok);
}
// ---- Audio decoder ----
[SysAbiExport(Nid = "O3f1sLMWRvs", ExportName = "sceAudiodecCreateDecoder",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
public static int AudiodecCreateDecoder(CpuContext ctx)
{
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
AudioDecoders[handle] = 1;
// sceAudiodec returns the handle directly (>= 0) or a negative error.
ctx[CpuRegister.Rax] = handle;
return unchecked((int)handle);
}
[SysAbiExport(Nid = "KHXHMDLkILw", ExportName = "sceAudiodecDecode",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
public static int AudiodecDecode(CpuContext ctx)
{
// No decoder present: report success with zero output samples so the
// caller treats the frame as silent rather than erroring.
return SetReturn(ctx, AudioDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : AudiodecErrorInvalidArg);
}
[SysAbiExport(Nid = "Tp+ZEy69mLk", ExportName = "sceAudiodecDeleteDecoder",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
public static int AudiodecDeleteDecoder(CpuContext ctx)
{
AudioDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetReturn(ctx, Ok);
}
private static bool TryWriteHandle(CpuContext ctx, ulong address, ulong handle) =>
ctx.TryWriteUInt64(address, handle);
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -0,0 +1,106 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Threading;
namespace SharpEmu.Libs.CommonDialog;
/// <summary>
/// libSceErrorDialog. There is no host error dialog, so an opened dialog
/// immediately reports finished — this keeps guests that block on the dialog
/// status (a common fatal-error path) from spinning forever.
/// </summary>
public static class ErrorDialogExports
{
private const int AlreadyInitialized = unchecked((int)0x80ED0001);
private const int NotInitialized = unchecked((int)0x80ED0002);
private const int ArgNull = unchecked((int)0x80ED0005);
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusFinished = 3;
private static int _initialized;
private static int _status;
[SysAbiExport(
Nid = "I88KChlynSs",
ExportName = "sceErrorDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogInitialize(CpuContext ctx)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0 ? 0 : AlreadyInitialized;
if (result == 0)
{
Volatile.Write(ref _status, StatusInitialized);
}
return SetReturn(ctx, result);
}
[SysAbiExport(
Nid = "M2ZF-ClLhgY",
ExportName = "sceErrorDialogOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogOpen(CpuContext ctx)
{
if (ctx[CpuRegister.Rdi] == 0)
{
return SetReturn(ctx, ArgNull);
}
if (Volatile.Read(ref _initialized) == 0)
{
return SetReturn(ctx, NotInitialized);
}
Volatile.Write(ref _status, StatusFinished);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "t2FvHRXzgqk",
ExportName = "sceErrorDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "WWiGuh9XfgQ",
ExportName = "sceErrorDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "ekXHb1kDBl0",
ExportName = "sceErrorDialogClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogClose(CpuContext ctx)
{
Volatile.Write(ref _status, StatusFinished);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "9XAxK2PMwk8",
ExportName = "sceErrorDialogTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogTerminate(CpuContext ctx)
{
Volatile.Write(ref _status, StatusNone);
Interlocked.Exchange(ref _initialized, 0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
+379 -182
View File
@@ -3,6 +3,7 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using SharpEmu.HLE;
@@ -22,6 +23,10 @@ public static class FiberExports
private const uint FiberStateIdle = 2;
private const uint FiberStateTerminated = 3;
private const uint FiberFlagContextSizeCheck = 0x10;
private const uint FiberFlagSetFpuRegs = 0x100;
private const uint Firmware350BuildVersion = 0x03500000;
private const ushort InitialFpuControlWord = 0x037F;
private const uint InitialMxcsr = 0x9FC0;
private const int FiberErrorNull = unchecked((int)0x80590001);
private const int FiberErrorAlignment = unchecked((int)0x80590002);
@@ -45,13 +50,26 @@ public static class FiberExports
private static int _contextSizeCheck;
[ThreadStatic]
private static ulong _currentFiberAddress;
private static readonly object _fiberGate = new();
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
private static readonly ConcurrentDictionary<ulong, FiberThreadState> _threadStates = new();
static FiberExports()
{
RunFiberSelfChecks();
}
public static void ResetRuntimeState()
{
lock (_fiberGate)
{
_continuations.Clear();
_stackRanges.Clear();
_threadStates.Clear();
Volatile.Write(ref _contextSizeCheck, 0);
}
}
[SysAbiExport(
Nid = "hVYD7Ou2pCQ",
@@ -61,6 +79,7 @@ public static class FiberExports
public static int FiberInitialize(CpuContext ctx)
{
var optParam = ReadStackArg64(ctx, 0);
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 1));
return FiberInitializeCore(
ctx,
ctx[CpuRegister.Rdi],
@@ -70,7 +89,8 @@ public static class FiberExports
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
optParam,
flags: 0);
flags: 0,
buildVersion);
}
[SysAbiExport(
@@ -82,6 +102,7 @@ public static class FiberExports
{
var optParam = ReadStackArg64(ctx, 0);
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 2));
return FiberInitializeCore(
ctx,
ctx[CpuRegister.Rdi],
@@ -91,7 +112,8 @@ public static class FiberExports
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
optParam,
flags);
flags,
buildVersion);
}
[SysAbiExport(
@@ -104,17 +126,17 @@ public static class FiberExports
var optParam = ctx[CpuRegister.Rdi];
if (optParam == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if ((optParam & 7) != 0)
{
return ctx.SetReturn(FiberErrorAlignment);
return SetReturn(ctx, FiberErrorAlignment);
}
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -127,24 +149,23 @@ public static class FiberExports
var fiber = ctx[CpuRegister.Rdi];
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (state != FiberStateIdle)
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
_continuations.TryRemove(fiber, out _);
_returnTargets.TryRemove(fiber, out _);
_stackRanges.TryRemove(fiber, out _);
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
return ctx.SetReturn(0);
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -229,74 +250,47 @@ public static class FiberExports
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
var returnArgument = ctx[CpuRegister.Rdi];
var argOnRunAddress = ctx[CpuRegister.Rsi];
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
{
return ctx.SetReturn(FiberErrorInvalid);
}
GuestCpuContinuation transferTarget;
ulong previousFiber;
lock (_fiberGate)
{
_continuations[fiberAddress] = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
argOnRunAddress);
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
var threadKey = GetThreadKey(ctx);
if (!_threadStates.TryGetValue(threadKey, out var threadState) ||
!TryWriteResumeArgument(ctx, threadState.RootContinuation, returnArgument))
{
_continuations.TryRemove(fiberAddress, out _);
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
previousFiber = returnTarget.PreviousFiber;
if (previousFiber != 0)
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
{
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
{
_continuations.TryRemove(fiberAddress, out _);
return ctx.SetReturn(FiberErrorState);
}
transferTarget = previousContinuation.Context with { Rax = 0 };
}
else
{
if (!returnTarget.ThreadContinuation.HasValue ||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
{
_continuations.TryRemove(fiberAddress, out _);
return ctx.SetReturn(FiberErrorState);
}
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
{
return ctx.SetReturn(FiberErrorInvalid);
}
transferTarget = threadState.RootContinuation.Context with { Rax = 0 };
_threadStates.TryRemove(threadKey, out _);
}
_currentFiberAddress = previousFiber;
_ = GuestThreadExecution.EnterFiber(previousFiber);
_ = GuestThreadExecution.EnterFiber(0);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
$"return-to-thread fiber=0x{fiberAddress:X16} " +
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -309,18 +303,18 @@ public static class FiberExports
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
return ctx.TryWriteUInt64(outAddress, fiberAddress)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt64(ctx, outAddress, fiberAddress)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -334,35 +328,35 @@ public static class FiberExports
var info = ctx[CpuRegister.Rsi];
if (info == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
!TryWriteName(ctx, info + 40, fields.Name) ||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -376,22 +370,22 @@ public static class FiberExports
var nameAddress = ctx[CpuRegister.Rsi];
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (nameAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return TryWriteName(ctx, fiber + FiberNameOffset, name)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -403,12 +397,12 @@ public static class FiberExports
{
if (ctx[CpuRegister.Rdi] != 0)
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorState);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorState);
}
[SysAbiExport(
@@ -419,8 +413,8 @@ public static class FiberExports
public static int FiberStopContextSizeCheck(CpuContext ctx)
{
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorState);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorState);
}
[SysAbiExport(
@@ -433,17 +427,18 @@ public static class FiberExports
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (ResolveCurrentFiberAddress(ctx) == 0)
if (ResolveCurrentFiberAddress(ctx) == 0 ||
!_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt64(ctx, outAddress, threadState.RootContinuation.Context.Rbp)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
private static int FiberInitializeCore(
@@ -455,69 +450,70 @@ public static class FiberExports
ulong contextAddress,
ulong contextSize,
ulong optParam,
uint flags)
uint flags,
uint buildVersion)
{
if (fiber == 0 || nameAddress == 0 || entry == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if ((fiber & 7) != 0 ||
(contextAddress & 15) != 0 ||
(optParam & 7) != 0)
{
return ctx.SetReturn(FiberErrorAlignment);
return SetReturn(ctx, FiberErrorAlignment);
}
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
{
return ctx.SetReturn(FiberErrorRange);
return SetReturn(ctx, FiberErrorRange);
}
if ((contextSize & 15) != 0 ||
(contextAddress == 0 && contextSize != 0) ||
(contextAddress != 0 && contextSize == 0))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (optParam != 0 &&
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (Volatile.Read(ref _contextSizeCheck) != 0)
{
flags |= FiberFlagContextSizeCheck;
}
flags = ApplyInitializationFlags(
flags,
buildVersion,
Volatile.Read(ref _contextSizeCheck) != 0);
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (contextAddress != 0)
{
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if ((flags & FiberFlagContextSizeCheck) != 0)
@@ -531,8 +527,10 @@ public static class FiberExports
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
}
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
return ctx.SetReturn(0);
TraceFiber(
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
return SetReturn(ctx, 0);
}
private static int FiberRunCore(
@@ -547,12 +545,12 @@ public static class FiberExports
{
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (attachContextAddress != 0 || attachContextSize != 0)
@@ -560,7 +558,7 @@ public static class FiberExports
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
if (attachResult != 0)
{
return ctx.SetReturn(attachResult);
return SetReturn(ctx, attachResult);
}
}
@@ -568,78 +566,96 @@ public static class FiberExports
if ((isSwitch && previousFiber == 0) ||
(!isSwitch && previousFiber != 0))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
if (previousFiber == fiber)
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
GuestCpuContinuation transferTarget;
var resumed = false;
lock (_fiberGate)
{
var threadKey = GetThreadKey(ctx);
if (!TryReadFiberFields(ctx, fiber, out fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (fields.State != FiberStateIdle)
{
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
FiberContinuation targetContinuation;
FiberContinuation targetContinuation = default;
if (_continuations.TryGetValue(fiber, out var savedContinuation))
{
targetContinuation = savedContinuation;
resumed = true;
}
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
{
return ctx.SetReturn(FiberErrorInvalid);
}
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
{
return ctx.SetReturn(FiberErrorInvalid);
}
var callerContinuation = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
outArgumentAddress);
if (!resumed)
{
var rootStackTop = _threadStates.TryGetValue(threadKey, out var existingThreadState)
? existingThreadState.RootContinuation.Context.Rsp
: callerContinuation.Context.Rsp;
if (!TryCreateInitialContinuation(
ctx,
fields,
argOnRun,
rootStackTop,
out targetContinuation))
{
return SetReturn(ctx, FiberErrorInvalid);
}
}
else if (!TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (previousFiber != 0)
{
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
previousState != FiberStateRun ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
_continuations[previousFiber] = callerContinuation;
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
}
else
{
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
if (_threadStates.ContainsKey(threadKey))
{
return SetReturn(ctx, FiberErrorPermission);
}
_threadStates[threadKey] = new FiberThreadState(callerContinuation, fiber, previousFiber);
}
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
{
if (previousFiber != 0)
{
_continuations.TryRemove(previousFiber, out _);
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
}
_returnTargets.TryRemove(fiber, out _);
return ctx.SetReturn(FiberErrorInvalid);
else
{
_threadStates.TryRemove(threadKey, out _);
}
return SetReturn(ctx, FiberErrorInvalid);
}
if (resumed)
@@ -648,35 +664,66 @@ public static class FiberExports
}
transferTarget = targetContinuation.Context with { Rax = 0 };
if (_threadStates.TryGetValue(threadKey, out var activeState))
{
_threadStates[threadKey] = activeState with
{
CurrentFiber = fiber,
PreviousFiber = previousFiber,
};
}
}
_currentFiberAddress = fiber;
_ = GuestThreadExecution.EnterFiber(fiber);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
private static bool TryCreateInitialContinuation(
CpuContext ctx,
FiberFields fields,
ulong argOnRun,
ulong rootStackTop,
out FiberContinuation continuation)
{
continuation = default;
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
ulong stackEnd;
if (fields.ContextAddress == 0)
{
if (rootStackTop < 32)
{
return false;
}
// Contextless fibers are specified to execute on the root
// sceFiberRun stack. Keep the suspended import return record
// above the entry stack and use a synthetic transfer slot below it.
stackEnd = rootStackTop & ~15UL;
}
else
{
if (fields.ContextSize < FiberContextMinimumSize)
{
return false;
}
stackEnd = fields.ContextAddress + fields.ContextSize;
}
if (!TryCalculateInitialStackLayout(stackEnd, out var entryRsp, out var transferSlot))
{
return false;
}
if (!TryWriteUInt64(ctx, transferSlot, fields.Entry) ||
!TryWriteUInt64(ctx, entryRsp, 0))
{
return false;
}
var stackEnd = fields.ContextAddress + fields.ContextSize;
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
if (!ctx.TryWriteUInt64(entryRsp, 0))
{
return false;
}
var setFpuRegisters = (fields.Flags & FiberFlagSetFpuRegs) != 0;
continuation = new FiberContinuation(
new GuestCpuContinuation(
@@ -698,7 +745,12 @@ public static class FiberExports
0,
0,
0,
0),
0,
0,
0,
setFpuRegisters ? InitialFpuControlWord : ctx.FpuControlWord,
setFpuRegisters ? InitialMxcsr : ctx.Mxcsr,
RestoreFullFpuState: setFpuRegisters),
0);
return true;
}
@@ -708,7 +760,7 @@ public static class FiberExports
FiberContinuation continuation,
ulong argument) =>
continuation.ArgOnRunAddress == 0 ||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
private static GuestCpuContinuation CaptureContinuation(
CpuContext ctx,
@@ -731,26 +783,99 @@ public static class FiberExports
ctx[CpuRegister.Rdi],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
ctx[CpuRegister.R10],
ctx[CpuRegister.R11],
ctx[CpuRegister.R12],
ctx[CpuRegister.R13],
ctx[CpuRegister.R14],
ctx[CpuRegister.R15]);
ctx[CpuRegister.R15],
ctx.FpuControlWord,
ctx.Mxcsr,
RestoreFullFpuState: false);
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
{
if (_currentFiberAddress != 0)
{
return _currentFiberAddress;
}
if (GuestThreadExecution.CurrentFiberAddress != 0)
{
return GuestThreadExecution.CurrentFiberAddress;
}
if (_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState) &&
threadState.CurrentFiber != 0)
{
return threadState.CurrentFiber;
}
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
}
private static ulong GetThreadKey(CpuContext ctx)
{
var handle = GuestThreadExecution.CurrentGuestThreadHandle;
if (handle != 0)
{
return handle;
}
// The main guest thread does not always have a scheduler handle. Its
// ABI thread pointer is stable across native/managed transitions.
return ctx.FsBase != 0 ? ctx.FsBase : 1;
}
private static uint ApplyInitializationFlags(
uint flags,
uint buildVersion,
bool contextSizeCheck)
{
if (buildVersion >= Firmware350BuildVersion)
{
flags |= FiberFlagSetFpuRegs;
}
if (contextSizeCheck)
{
flags |= FiberFlagContextSizeCheck;
}
return flags;
}
private static bool TryCalculateInitialStackLayout(
ulong stackEnd,
out ulong entryRsp,
out ulong transferSlot)
{
var alignedEnd = stackEnd & ~15UL;
if (alignedEnd < 2 * sizeof(ulong))
{
entryRsp = 0;
transferSlot = 0;
return false;
}
entryRsp = alignedEnd - sizeof(ulong);
transferSlot = entryRsp - sizeof(ulong);
return true;
}
[Conditional("DEBUG")]
private static void RunFiberSelfChecks()
{
Debug.Assert(
ApplyInitializationFlags(0, Firmware350BuildVersion - 1, false) == 0,
"Pre-3.50 fibers unexpectedly enable SetFpuRegs.");
Debug.Assert(
ApplyInitializationFlags(0, Firmware350BuildVersion, false) == FiberFlagSetFpuRegs,
"3.50+ fibers must initialize x87/MXCSR control state.");
Debug.Assert(
ApplyInitializationFlags(1, Firmware350BuildVersion, true) ==
(1u | FiberFlagSetFpuRegs | FiberFlagContextSizeCheck),
"Fiber initialization discarded caller/internal flags.");
Debug.Assert(
TryCalculateInitialStackLayout(0x1017, out var rsp, out var slot) &&
rsp == 0x1008 && slot == 0x1000 && (rsp & 15) == 8,
"Initial fiber transfer slot does not produce SysV entry alignment.");
}
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
ResolveCurrentFiberAddress(ctx);
@@ -779,11 +904,11 @@ public static class FiberExports
return FiberErrorInvalid;
}
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
{
return FiberErrorInvalid;
}
@@ -839,8 +964,8 @@ public static class FiberExports
return false;
}
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
magicStart != FiberSignature0 ||
magicEnd != FiberSignature1)
{
@@ -855,12 +980,12 @@ public static class FiberExports
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
{
fields = default;
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
{
return false;
@@ -898,6 +1023,46 @@ public static class FiberExports
return 0;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
{
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
@@ -925,6 +1090,37 @@ public static class FiberExports
return true;
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
{
var bytes = new byte[capacity];
Span<byte> current = stackalloc byte[1];
for (var index = 0; index < bytes.Length; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
value = string.Empty;
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceFiber(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
@@ -946,9 +1142,10 @@ public static class FiberExports
GuestCpuContinuation Context,
ulong ArgOnRunAddress);
private readonly record struct FiberReturnTarget(
ulong PreviousFiber,
FiberContinuation? ThreadContinuation);
private sealed record FiberThreadState(
FiberContinuation RootContinuation,
ulong CurrentFiber,
ulong PreviousFiber);
private readonly record struct FiberStackRange(ulong Start, ulong Size)
{
+293
View File
@@ -0,0 +1,293 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Font;
public static class FontExports
{
private static readonly object AllocationGate = new();
private static ulong _librarySelectionAddress;
private static ulong _rendererSelectionAddress;
[SysAbiExport(
Nid = "whrS4oksXc4",
ExportName = "sceFontMemoryInit",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int MemoryInit(CpuContext ctx)
{
var descriptorAddress = ctx[CpuRegister.Rdi];
var regionAddress = ctx[CpuRegister.Rsi];
var regionSize = (uint)ctx[CpuRegister.Rdx];
var interfaceAddress = ctx[CpuRegister.Rcx];
var mspaceAddress = ctx[CpuRegister.R8];
var destroyCallback = ctx[CpuRegister.R9];
if (descriptorAddress == 0 ||
!TryWriteUInt32(ctx, descriptorAddress, 0x00000F00) ||
!TryWriteUInt32(ctx, descriptorAddress + 0x04, regionSize) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x08, regionAddress) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x10, mspaceAddress) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x18, interfaceAddress) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x20, destroyCallback) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x28, 0) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x30, 0) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x38, mspaceAddress))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetSuccess(ctx);
}
[SysAbiExport(
Nid = "oM+XCzVG3oM",
ExportName = "sceFontSelectLibraryFt",
Target = Generation.Gen5,
LibraryName = "libSceFontFt")]
public static int SelectLibraryFt(CpuContext ctx) =>
ReturnSelection(ctx, ref _librarySelectionAddress, 0x38);
[SysAbiExport(
Nid = "Xx974EW-QFY",
ExportName = "sceFontSelectRendererFt",
Target = Generation.Gen5,
LibraryName = "libSceFontFt")]
public static int SelectRendererFt(CpuContext ctx) =>
ReturnSelection(ctx, ref _rendererSelectionAddress, 0x100);
[SysAbiExport(
Nid = "n590hj5Oe-k",
ExportName = "sceFontCreateLibraryWithEdition",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int CreateLibraryWithEdition(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F01);
[SysAbiExport(
Nid = "WaSFJoRWXaI",
ExportName = "sceFontCreateRendererWithEdition",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int CreateRendererWithEdition(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F07);
[SysAbiExport(
Nid = "cKYtVmeSTcw",
ExportName = "sceFontOpenFontSet",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int OpenFontSet(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
[SysAbiExport(
Nid = "KXUpebrFk1U",
ExportName = "sceFontOpenFontMemory",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int OpenFontMemory(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
[SysAbiExport(
Nid = "JzCH3SCFnAU",
ExportName = "sceFontOpenFontInstance",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int OpenFontInstance(CpuContext ctx)
{
var sourceHandle = ctx[CpuRegister.Rdi];
var setupHandle = ctx[CpuRegister.Rsi];
var outputAddress = ctx[CpuRegister.Rdx];
if (outputAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (setupHandle != 0)
{
return ctx.TryWriteUInt64(outputAddress, setupHandle)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!TryAllocateOpaque(ctx, 0x100, out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (sourceHandle != 0)
{
Span<byte> source = stackalloc byte[0x100];
if (ctx.Memory.TryRead(sourceHandle, source))
{
_ = ctx.Memory.TryWrite(handle, source);
}
}
_ = TryWriteUInt16(ctx, handle, 0x0F02);
return ctx.TryWriteUInt64(outputAddress, handle)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "SsRbbCiWoGw",
ExportName = "sceFontSupportSystemFonts",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SupportSystemFonts(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "mz2iTY0MK4A",
ExportName = "sceFontSupportExternalFonts",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SupportExternalFonts(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "CUKn5pX-NVY",
ExportName = "sceFontAttachDeviceCacheBuffer",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int AttachDeviceCacheBuffer(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "IQtleGLL5pQ",
ExportName = "sceFontGetRenderCharGlyphMetrics",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int GetRenderCharGlyphMetrics(CpuContext ctx)
{
var metricsAddress = ctx[CpuRegister.Rdx];
if (metricsAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
ctx,
metricsAddress + (ulong)(index * sizeof(float)),
BitConverter.SingleToUInt32Bits(values[index])))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
return SetSuccess(ctx);
}
[SysAbiExport(
Nid = "gdUCnU0gHdI",
ExportName = "sceFontRenderSurfaceInit",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int RenderSurfaceInit(CpuContext ctx)
{
var surfaceAddress = ctx[CpuRegister.Rdi];
var bufferAddress = ctx[CpuRegister.Rsi];
var widthBytes = (uint)ctx[CpuRegister.Rdx];
var pixelBytes = (uint)ctx[CpuRegister.Rcx] & 0xFF;
var width = (uint)ctx[CpuRegister.R8];
var height = (uint)ctx[CpuRegister.R9];
if (surfaceAddress == 0 ||
!ctx.TryWriteUInt64(surfaceAddress, bufferAddress) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x08, widthBytes) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x0C, pixelBytes) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x10, width) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x14, height) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x18, 0) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x1C, 0) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x20, width) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x24, height))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetSuccess(ctx);
}
private static int ReturnSelection(CpuContext ctx, ref ulong selectionAddress, uint objectSize)
{
if (ctx[CpuRegister.Rdi] != 0)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
lock (AllocationGate)
{
if (selectionAddress == 0)
{
if (!TryAllocateOpaque(ctx, 0x20, out selectionAddress) ||
!TryWriteUInt32(ctx, selectionAddress, 0) ||
!TryWriteUInt32(ctx, selectionAddress + 4, objectSize))
{
selectionAddress = 0;
}
}
}
ctx[CpuRegister.Rax] = selectionAddress;
return 0;
}
private static int CreateOpaqueHandle(CpuContext ctx, ulong outputAddress, int size, ushort magic)
{
if (outputAddress == 0 || !TryAllocateOpaque(ctx, size, out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!TryWriteUInt16(ctx, handle, magic) || !ctx.TryWriteUInt64(outputAddress, handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetSuccess(ctx);
}
private static bool TryAllocateOpaque(CpuContext ctx, int size, out ulong address)
{
address = 0;
if (ctx.Memory is not IGuestMemoryAllocator allocator ||
!allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address))
{
return false;
}
Span<byte> bytes = stackalloc byte[size];
bytes.Clear();
return ctx.Memory.TryWrite(address, bytes);
}
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetSuccess(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
}
+52 -5
View File
@@ -22,6 +22,8 @@ internal sealed record GuestDrawTexture(
bool IsStorage,
uint MipLevels = 1,
uint MipLevel = 0,
uint BaseMipLevel = 0,
uint ResourceMipLevels = 1,
uint Pitch = 0,
uint TileMode = 0,
uint DstSelect = 0xFAC,
@@ -36,7 +38,11 @@ internal readonly record struct GuestSampler(
internal sealed record GuestMemoryBuffer(
ulong BaseAddress,
byte[] Data);
byte[] Data,
int Length,
bool Pooled,
bool Writable = false,
bool WriteBackToGuest = true);
/// <summary>DataFormat/NumberFormat are raw guest vertex-attribute codes.</summary>
internal sealed record GuestVertexBuffer(
@@ -47,11 +53,15 @@ internal sealed record GuestVertexBuffer(
ulong BaseAddress,
uint Stride,
uint OffsetBytes,
byte[] Data);
byte[] Data,
int Length,
bool Pooled);
internal sealed record GuestIndexBuffer(
byte[] Data,
bool Is32Bit);
int Length,
bool Is32Bit,
bool Pooled);
internal readonly record struct GuestRect(
int X,
@@ -67,6 +77,25 @@ internal readonly record struct GuestViewport(
float MinDepth,
float MaxDepth);
internal readonly record struct GuestRasterState(
bool CullFront,
bool CullBack,
bool FrontFaceClockwise,
bool Wireframe)
{
public static GuestRasterState Default { get; } = new(false, false, false, false);
}
// CompareOp uses the GCN DB_DEPTH_CONTROL ZFUNC encoding, which matches the
// Vulkan CompareOp ordering (0=Never through 7=Always).
internal readonly record struct GuestDepthState(
bool TestEnable,
bool WriteEnable,
uint CompareOp)
{
public static GuestDepthState Default { get; } = new(false, false, 7);
}
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
/// defaults (1/0) are the guest ONE/ZERO codes.</summary>
internal readonly record struct GuestBlendState(
@@ -95,12 +124,16 @@ internal readonly record struct GuestBlendState(
internal sealed record GuestRenderState(
IReadOnlyList<GuestBlendState> Blends,
GuestRect? Scissor,
GuestViewport? Viewport)
GuestViewport? Viewport,
GuestRasterState Raster,
GuestDepthState Depth)
{
public static GuestRenderState Default { get; } = new(
[GuestBlendState.Default],
Scissor: null,
Viewport: null);
Viewport: null,
GuestRasterState.Default,
GuestDepthState.Default);
public GuestBlendState Blend =>
Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
@@ -114,3 +147,17 @@ internal sealed record GuestRenderTarget(
uint Format,
uint NumberType,
uint MipLevels = 1);
/// <summary>Guest DB surface bound alongside a color render target.</summary>
internal sealed record GuestDepthTarget(
ulong ReadAddress,
ulong WriteAddress,
uint Width,
uint Height,
uint GuestFormat,
uint SwizzleMode,
float ClearDepth,
bool ReadOnly)
{
public ulong Address => WriteAddress != 0 ? WriteAddress : ReadAddress;
}
+59 -8
View File
@@ -33,7 +33,9 @@ internal interface IGuestGpuBackend
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1);
int scalarRegisterBufferIndex = -1,
int requiredVertexOutputCount = 0,
ulong storageBufferOffsetAlignment = 1);
bool TryCompilePixelShader(
Gen5ShaderState state,
@@ -44,7 +46,10 @@ internal interface IGuestGpuBackend
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1);
int scalarRegisterBufferIndex = -1,
uint pixelInputEnable = 0,
uint pixelInputAddress = 0,
ulong storageBufferOffsetAlignment = 1);
bool TryCompileComputeShader(
Gen5ShaderState state,
@@ -53,7 +58,14 @@ internal interface IGuestGpuBackend
uint localSizeY,
uint localSizeZ,
out IGuestCompiledShader? shader,
out string error);
out string error,
int totalGlobalBufferCount = -1,
int initialScalarBufferIndex = -1,
uint waveLaneCount = 32,
ulong storageBufferOffsetAlignment = 1);
/// <summary>Returns the backend's no-color-output fragment shader.</summary>
IGuestCompiledShader GetDepthOnlyFragmentShader();
void HideSplashScreen();
@@ -78,6 +90,21 @@ internal interface IGuestGpuBackend
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null);
void SubmitDepthOnlyTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
GuestDepthTarget depthTarget,
IGuestCompiledShader? vertexShader = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
ulong shaderAddress = 0);
void SubmitOffscreenTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
@@ -90,7 +117,9 @@ internal interface IGuestGpuBackend
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null);
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0);
void SubmitStorageTranslatedDraw(
IGuestCompiledShader pixelShader,
@@ -98,16 +127,28 @@ internal interface IGuestGpuBackend
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
uint width,
uint height);
uint height,
ulong shaderAddress = 0);
void SubmitComputeDispatch(
long SubmitComputeDispatch(
ulong shaderAddress,
IGuestCompiledShader computeShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint groupCountX,
uint groupCountY,
uint groupCountZ);
uint groupCountZ,
uint baseGroupX,
uint baseGroupY,
uint baseGroupZ,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
bool isIndirect,
bool writesGlobalMemory,
uint threadCountX = uint.MaxValue,
uint threadCountY = uint.MaxValue,
uint threadCountZ = uint.MaxValue);
bool TrySubmitGuestImage(
ulong address,
@@ -115,6 +156,14 @@ internal interface IGuestGpuBackend
uint height,
uint pitchInPixel);
bool TrySubmitOrderedGuestImageFlip(
int videoOutHandle,
int displayBufferIndex,
ulong address,
uint width,
uint height,
uint pitchInPixel);
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
@@ -126,10 +175,12 @@ internal interface IGuestGpuBackend
uint sourceWidth,
uint sourceHeight,
uint sourceFormat,
uint sourceNumberType,
ulong destinationAddress,
uint destinationWidth,
uint destinationHeight,
uint destinationFormat);
uint destinationFormat,
uint destinationNumberType);
/// <summary>
/// Whether the backend supports the guest render-target format, and how its pixel
@@ -15,6 +15,9 @@ namespace SharpEmu.Libs.Gpu.Vulkan;
/// </summary>
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
{
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
public bool TryCompileVertexShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
@@ -23,7 +26,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1)
int scalarRegisterBufferIndex = -1,
int requiredVertexOutputCount = 0,
ulong storageBufferOffsetAlignment = 1)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompileVertexShader(
@@ -34,7 +39,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase,
scalarRegisterBufferIndex))
scalarRegisterBufferIndex,
requiredVertexOutputCount,
storageBufferOffsetAlignment))
{
return false;
}
@@ -52,7 +59,10 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1)
int scalarRegisterBufferIndex = -1,
uint pixelInputEnable = 0,
uint pixelInputAddress = 0,
ulong storageBufferOffsetAlignment = 1)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompilePixelShader(
@@ -64,7 +74,10 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase,
scalarRegisterBufferIndex))
scalarRegisterBufferIndex,
pixelInputEnable,
pixelInputAddress,
storageBufferOffsetAlignment))
{
return false;
}
@@ -80,7 +93,11 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
uint localSizeY,
uint localSizeZ,
out IGuestCompiledShader? shader,
out string error)
out string error,
int totalGlobalBufferCount = -1,
int initialScalarBufferIndex = -1,
uint waveLaneCount = 32,
ulong storageBufferOffsetAlignment = 1)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompileComputeShader(
@@ -90,7 +107,11 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
localSizeY,
localSizeZ,
out var compiled,
out error))
out error,
totalGlobalBufferCount,
initialScalarBufferIndex,
waveLaneCount,
storageBufferOffsetAlignment))
{
return false;
}
@@ -99,6 +120,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
return true;
}
public IGuestCompiledShader GetDepthOnlyFragmentShader() =>
DepthOnlyFragmentShader;
public void EnsureStarted(uint width, uint height) =>
VulkanVideoPresenter.EnsureStarted(width, height);
@@ -140,6 +164,35 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
vertexBuffers,
renderState);
public void SubmitDepthOnlyTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
GuestDepthTarget depthTarget,
IGuestCompiledShader? vertexShader = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
ulong shaderAddress = 0) =>
VulkanVideoPresenter.SubmitDepthOnlyTranslatedDraw(
Spirv(pixelShader),
textures,
globalMemoryBuffers,
attributeCount,
depthTarget,
vertexShader is null ? null : Spirv(vertexShader),
vertexCount,
instanceCount,
primitiveType,
indexBuffer,
vertexBuffers,
renderState,
shaderAddress);
public void SubmitOffscreenTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
@@ -152,7 +205,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null) =>
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0) =>
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
Spirv(pixelShader),
textures,
@@ -165,7 +220,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
primitiveType,
indexBuffer,
vertexBuffers,
renderState);
renderState,
depthTarget,
shaderAddress);
public void SubmitStorageTranslatedDraw(
IGuestCompiledShader pixelShader,
@@ -173,23 +230,36 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
uint width,
uint height) =>
uint height,
ulong shaderAddress = 0) =>
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
Spirv(pixelShader),
textures,
globalMemoryBuffers,
attributeCount,
width,
height);
height,
shaderAddress);
public void SubmitComputeDispatch(
public long SubmitComputeDispatch(
ulong shaderAddress,
IGuestCompiledShader computeShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint groupCountX,
uint groupCountY,
uint groupCountZ) =>
uint groupCountZ,
uint baseGroupX,
uint baseGroupY,
uint baseGroupZ,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
bool isIndirect,
bool writesGlobalMemory,
uint threadCountX = uint.MaxValue,
uint threadCountY = uint.MaxValue,
uint threadCountZ = uint.MaxValue) =>
VulkanVideoPresenter.SubmitComputeDispatch(
shaderAddress,
Spirv(computeShader),
@@ -197,7 +267,18 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
globalMemoryBuffers,
groupCountX,
groupCountY,
groupCountZ);
groupCountZ,
baseGroupX,
baseGroupY,
baseGroupZ,
localSizeX,
localSizeY,
localSizeZ,
isIndirect,
writesGlobalMemory,
threadCountX,
threadCountY,
threadCountZ);
public bool TrySubmitGuestImage(
ulong address,
@@ -206,6 +287,21 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
uint pitchInPixel) =>
VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
public bool TrySubmitOrderedGuestImageFlip(
int videoOutHandle,
int displayBufferIndex,
ulong address,
uint width,
uint height,
uint pitchInPixel) =>
VulkanVideoPresenter.TrySubmitOrderedGuestImageFlip(
videoOutHandle,
displayBufferIndex,
address,
width,
height,
pitchInPixel);
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
@@ -217,19 +313,23 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
uint sourceWidth,
uint sourceHeight,
uint sourceFormat,
uint sourceNumberType,
ulong destinationAddress,
uint destinationWidth,
uint destinationHeight,
uint destinationFormat) =>
uint destinationFormat,
uint destinationNumberType) =>
VulkanVideoPresenter.TrySubmitGuestImageBlit(
sourceAddress,
sourceWidth,
sourceHeight,
sourceFormat,
sourceNumberType,
destinationAddress,
destinationWidth,
destinationHeight,
destinationFormat);
destinationFormat,
destinationNumberType);
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
{
+82
View File
@@ -0,0 +1,82 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.Libs;
/// <summary>
/// High-resolution host sleeps for guest pacing. <see cref="Thread.Sleep(int)"/>
/// routinely overshoots by a scheduler quantum, which turns per-frame waits
/// (flip pacing, usleep-based game loops) into a hard frame-rate cap; these
/// helpers sleep coarsely for the bulk of the interval and yield-spin the
/// remainder so wakeups land within tens of microseconds of the target.
/// </summary>
internal static class HostTiming
{
/// <summary>
/// Blocks until <see cref="Stopwatch.GetTimestamp"/> reaches
/// <paramref name="targetTimestamp"/>.
/// </summary>
public static void SleepUntil(long targetTimestamp)
{
while (true)
{
var remainingTicks = targetTimestamp - Stopwatch.GetTimestamp();
if (remainingTicks <= 0)
{
return;
}
if (remainingTicks > Stopwatch.Frequency * 60)
{
// Far-future target: coarse sleep avoids overflowing the
// microsecond conversion below and needs no precision.
Thread.Sleep(30_000);
continue;
}
var remainingMicroseconds = remainingTicks * 1_000_000 / Stopwatch.Frequency;
if (remainingMicroseconds > 2200)
{
// Coarse sleep for the bulk; macOS overshoots ~0.5-1.5 ms.
Thread.Sleep((int)((remainingMicroseconds - 1200) / 1000));
}
else if (remainingMicroseconds > 1200)
{
// A 1 ms nap typically wakes within the margin and costs no
// CPU, unlike yield-spinning through the whole tail.
Thread.Sleep(1);
}
else if (remainingMicroseconds > 100)
{
Thread.Sleep(0);
}
else
{
Thread.SpinWait(64);
}
}
}
/// <summary>Blocks for the given number of microseconds.</summary>
public static void SleepMicroseconds(long microseconds)
{
if (microseconds <= 0)
{
return;
}
if (microseconds >= 10_000_000)
{
// Long/sentinel sleeps (usleep(-1) parks): sub-millisecond
// precision is irrelevant and the tick conversion below would
// overflow, which used to turn the park into a hot spin.
Thread.Sleep((int)Math.Min(microseconds / 1000, int.MaxValue));
return;
}
var ticks = microseconds * Stopwatch.Frequency / 1_000_000;
SleepUntil(Stopwatch.GetTimestamp() + ticks);
}
}
+540 -21
View File
@@ -2,12 +2,34 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Text.Json;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Json;
public static class JsonExports
{
private const int ValueObjectSize = 0x20;
private const int StringObjectSize = 0x08;
private const ulong MaximumJsonBufferSize = 16 * 1024 * 1024;
private const int SceJsonParserErrorInvalidToken = unchecked((int)0x80920101);
private const int SceJsonParserErrorEmptyBuffer = unchecked((int)0x80920105);
private sealed record JsonValueState(JsonElement Element);
private sealed record JsonStringState(
string Value,
ulong GuestBufferAddress = 0,
int GuestBufferCapacity = 0);
private static readonly ConcurrentDictionary<ulong, JsonValueState> _values = new();
private static readonly ConcurrentDictionary<ulong, JsonStringState> _strings = new();
private static readonly JsonElement _nullElement = CreateNullElement();
[SysAbiExport(
Nid = "-hJRce8wn1U",
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
@@ -78,9 +100,6 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
[SysAbiExport(
Nid = "+drDFyAS6u4",
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
@@ -91,26 +110,38 @@ public static class JsonExports
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// The PS5 ABI object occupies 0x28 bytes in the caller's frame. Its
// setters below replace the allocator and file-buffer fields.
Span<byte> parameter = stackalloc byte[0x28];
parameter.Clear();
if (!ctx.Memory.TryWrite(thisAddress, parameter))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceJson("InitParameter2.ctor", thisAddress, 0);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -118,12 +149,24 @@ public static class JsonExports
[SysAbiExport(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2SetAllocator(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> fields = stackalloc byte[sizeof(ulong) * 2];
BinaryPrimitives.WriteUInt64LittleEndian(fields, ctx[CpuRegister.Rsi]);
BinaryPrimitives.WriteUInt64LittleEndian(fields[sizeof(ulong)..], ctx[CpuRegister.Rdx]);
if (!ctx.Memory.TryWrite(thisAddress, fields))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -131,12 +174,16 @@ public static class JsonExports
[SysAbiExport(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
if (thisAddress == 0 || !ctx.TryWriteUInt64(thisAddress + 0x10, ctx[CpuRegister.Rsi]))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -144,14 +191,474 @@ public static class JsonExports
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int Initializer2Constructor(CpuContext ctx)
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerInitialize2(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
var initParameterAddress = ctx[CpuRegister.Rsi];
if (thisAddress == 0 || initParameterAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
TraceJson("Initializer.initialize2", thisAddress, initParameterAddress);
return SetReturn(ctx, 0);
}
public static int ValueConstructor(CpuContext ctx)
{
_ = ConstructValue(ctx);
return JsonValueExports.ValueDefaultConstructor(ctx);
}
[SysAbiExport(
Nid = "-wa17B7TGnw",
ExportName = "_ZN3sce4Json5ValueC2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueBaseConstructor(CpuContext ctx) => ConstructValue(ctx);
public static int ValueDestructor(CpuContext ctx)
{
_ = DestroyValue(ctx);
return JsonValueExports.ValueDestructor(ctx);
}
[SysAbiExport(
Nid = "0eUrW9JAxM0",
ExportName = "_ZN3sce4Json5ValueD2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueBaseDestructor(CpuContext ctx) => DestroyValue(ctx);
[SysAbiExport(
Nid = "S5JxQnoGF3E",
ExportName = "_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ParserParseBuffer(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var bufferAddress = ctx[CpuRegister.Rsi];
var bufferSize = ctx[CpuRegister.Rdx];
if (valueAddress == 0 || bufferAddress == 0 || bufferSize == 0)
{
return SetReturn(ctx, SceJsonParserErrorEmptyBuffer);
}
if (bufferSize > MaximumJsonBufferSize || bufferSize > int.MaxValue)
{
return SetReturn(ctx, SceJsonParserErrorInvalidToken);
}
var buffer = new byte[(int)bufferSize];
if (!ctx.Memory.TryRead(bufferAddress, buffer))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
try
{
using var document = JsonDocument.Parse(buffer);
var element = document.RootElement.Clone();
StoreValue(ctx, valueAddress, element);
TraceJsonText("Parser.parse", valueAddress, Encoding.UTF8.GetString(buffer));
return SetReturn(ctx, 0);
}
catch (JsonException)
{
return SetReturn(ctx, SceJsonParserErrorInvalidToken);
}
}
[SysAbiExport(
Nid = "SHtAad20YYM",
ExportName = "_ZNK3sce4Json5Value7getTypeEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetType(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var element = GetValue(valueAddress);
ctx[CpuRegister.Rax] = (ulong)GetValueType(element);
return 0;
}
[SysAbiExport(
Nid = "RBw+4NukeGQ",
ExportName = "_ZNK3sce4Json5Value5countEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueCount(CpuContext ctx)
{
var element = GetValue(ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = element.ValueKind switch
{
System.Text.Json.JsonValueKind.Array => (ulong)element.GetArrayLength(),
System.Text.Json.JsonValueKind.Object => (ulong)element.EnumerateObject().Count(),
_ => 0,
};
return 0;
}
[SysAbiExport(
Nid = "zTwZdI8AZ5Y",
ExportName = "_ZNK3sce4Json5Value10getBooleanEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetBoolean(CpuContext ctx) => ReturnValueStorage(ctx);
[SysAbiExport(
Nid = "DIxvoy7Ngvk",
ExportName = "_ZNK3sce4Json5Value10getIntegerEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetInteger(CpuContext ctx) => ReturnValueStorage(ctx);
[SysAbiExport(
Nid = "sn4HNCtNRzY",
ExportName = "_ZNK3sce4Json5Value11getUIntegerEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetUnsignedInteger(CpuContext ctx) => ReturnValueStorage(ctx);
[SysAbiExport(
Nid = "3qrge7L-AU4",
ExportName = "_ZNK3sce4Json5Value7getRealEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetReal(CpuContext ctx) => ReturnValueStorage(ctx);
[SysAbiExport(
Nid = "HwDt5lD9Bfo",
ExportName = "_ZNK3sce4Json5ValueixEPKc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueIndexCString(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var keyAddress = ctx[CpuRegister.Rsi];
if (!TryReadUtf8CString(ctx, keyAddress, 4096, out var key) ||
!TryAllocateGuestObject(ctx, ValueObjectSize, out var childAddress))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
var parent = GetValue(valueAddress);
var child = parent.ValueKind == System.Text.Json.JsonValueKind.Object &&
parent.TryGetProperty(key, out var property)
? property.Clone()
: _nullElement;
StoreValue(ctx, childAddress, child);
ctx[CpuRegister.Rax] = childAddress;
TraceJsonText("Value.index", valueAddress, key);
return 0;
}
[SysAbiExport(
Nid = "XlWbvieLj2M",
ExportName = "_ZNK3sce4Json5ValueixEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueIndexPosition(CpuContext ctx) => ReturnIndexedValue(ctx);
[SysAbiExport(
Nid = "0YqYAoO-+Uo",
ExportName = "_ZNK3sce4Json5Value8getValueEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetPosition(CpuContext ctx) => ReturnIndexedValue(ctx);
[SysAbiExport(
Nid = "4zrm6VrgIAw",
ExportName = "_ZN3sce4Json5ValueaSERKS1_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueAssignment(CpuContext ctx)
{
var destinationAddress = ctx[CpuRegister.Rdi];
var sourceAddress = ctx[CpuRegister.Rsi];
if (destinationAddress != 0)
{
StoreValue(ctx, destinationAddress, GetValue(sourceAddress));
}
ctx[CpuRegister.Rax] = destinationAddress;
return 0;
}
public static int StringConstructor(CpuContext ctx)
{
_ = ConstructString(ctx);
return JsonValueExports.StringDefaultConstructor(ctx);
}
[SysAbiExport(
Nid = "eG9E9M6XvTM",
ExportName = "_ZN3sce4Json6StringC2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int StringBaseConstructor(CpuContext ctx) => ConstructString(ctx);
public static int StringDestructor(CpuContext ctx)
{
_ = DestroyString(ctx);
return JsonValueExports.StringDestructor(ctx);
}
[SysAbiExport(
Nid = "Ui7YFnSTCBw",
ExportName = "_ZN3sce4Json6StringD2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int StringBaseDestructor(CpuContext ctx) => DestroyString(ctx);
[SysAbiExport(
Nid = "Ncel8t2Rrpc",
ExportName = "_ZNK3sce4Json5Value8toStringERNS0_6StringE",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueToString(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var stringAddress = ctx[CpuRegister.Rsi];
if (stringAddress != 0)
{
var element = GetValue(valueAddress);
var value = element.ValueKind == System.Text.Json.JsonValueKind.String
? element.GetString() ?? string.Empty
: element.GetRawText();
_strings[stringAddress] = new JsonStringState(value);
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "L1KAkYWml-M",
ExportName = "_ZNK3sce4Json6String5c_strEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int StringCStr(CpuContext ctx)
{
var stringAddress = ctx[CpuRegister.Rdi];
if (!_strings.TryGetValue(stringAddress, out var state))
{
state = new JsonStringState(string.Empty);
}
var bytes = Encoding.UTF8.GetBytes(state.Value + '\0');
var guestBufferAddress = state.GuestBufferAddress;
if (guestBufferAddress == 0 || state.GuestBufferCapacity < bytes.Length)
{
if (!TryAllocateGuestObject(ctx, bytes.Length, out guestBufferAddress))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
}
if (!ctx.Memory.TryWrite(guestBufferAddress, bytes))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
_strings[stringAddress] = state with
{
GuestBufferAddress = guestBufferAddress,
GuestBufferCapacity = bytes.Length,
};
ctx.TryWriteUInt64(stringAddress, guestBufferAddress);
ctx[CpuRegister.Rax] = guestBufferAddress;
TraceJsonText("String.c_str", stringAddress, state.Value);
return 0;
}
private static int ConstructValue(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
StoreValue(ctx, thisAddress, _nullElement);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
return 0;
}
private static int ReturnIndexedValue(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var position = ctx[CpuRegister.Rsi];
if (!TryAllocateGuestObject(ctx, ValueObjectSize, out var childAddress))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
var parent = GetValue(valueAddress);
var child = parent.ValueKind == System.Text.Json.JsonValueKind.Array &&
position < (ulong)parent.GetArrayLength()
? parent[(int)position].Clone()
: _nullElement;
StoreValue(ctx, childAddress, child);
ctx[CpuRegister.Rax] = childAddress;
return 0;
}
private static int ReturnValueStorage(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
ctx[CpuRegister.Rax] = thisAddress == 0 ? 0 : thisAddress + 0x10;
return 0;
}
private static int DestroyValue(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
_values.TryRemove(thisAddress, out _);
if (thisAddress != 0)
{
Span<byte> empty = stackalloc byte[ValueObjectSize];
empty.Clear();
ctx.Memory.TryWrite(thisAddress, empty);
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
private static int ConstructString(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
_strings[thisAddress] = new JsonStringState(string.Empty);
ctx.TryWriteUInt64(thisAddress, 0);
ctx[CpuRegister.Rax] = thisAddress;
return 0;
}
private static int DestroyString(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
_strings.TryRemove(thisAddress, out _);
if (thisAddress != 0)
{
ctx.TryWriteUInt64(thisAddress, 0);
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
private static JsonElement CreateNullElement()
{
using var document = JsonDocument.Parse("null");
return document.RootElement.Clone();
}
private static JsonElement GetValue(ulong address) =>
address != 0 && _values.TryGetValue(address, out var state)
? state.Element
: _nullElement;
private static void StoreValue(CpuContext ctx, ulong address, JsonElement element)
{
if (address == 0)
{
return;
}
var clone = element.Clone();
_values[address] = new JsonValueState(clone);
Span<byte> mirror = stackalloc byte[ValueObjectSize];
mirror.Clear();
var type = GetValueType(clone);
BinaryPrimitives.WriteInt32LittleEndian(mirror[0x1C..], type);
switch (clone.ValueKind)
{
case System.Text.Json.JsonValueKind.True:
mirror[0x10] = 1;
break;
case System.Text.Json.JsonValueKind.Number when clone.TryGetInt64(out var integer):
BinaryPrimitives.WriteInt64LittleEndian(mirror[0x10..], integer);
break;
case System.Text.Json.JsonValueKind.Number when clone.TryGetUInt64(out var unsignedInteger):
BinaryPrimitives.WriteUInt64LittleEndian(mirror[0x10..], unsignedInteger);
break;
case System.Text.Json.JsonValueKind.Number:
BinaryPrimitives.WriteInt64LittleEndian(
mirror[0x10..],
BitConverter.DoubleToInt64Bits(clone.GetDouble()));
break;
}
ctx.Memory.TryWrite(address, mirror);
}
private static int GetValueType(JsonElement element) => element.ValueKind switch
{
System.Text.Json.JsonValueKind.True or System.Text.Json.JsonValueKind.False => 1,
System.Text.Json.JsonValueKind.Number when element.TryGetInt64(out _) => 2,
System.Text.Json.JsonValueKind.Number when element.TryGetUInt64(out _) => 3,
System.Text.Json.JsonValueKind.Number => 4,
System.Text.Json.JsonValueKind.String => 5,
System.Text.Json.JsonValueKind.Array => 6,
System.Text.Json.JsonValueKind.Object => 7,
_ => 0,
};
private static bool TryAllocateGuestObject(CpuContext ctx, int size, out ulong address)
{
address = 0;
return size > 0 &&
ctx.Memory is IGuestMemoryAllocator allocator &&
allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address);
}
private static bool TryReadUtf8CString(
CpuContext ctx,
ulong address,
int maximumLength,
out string value)
{
value = string.Empty;
if (address == 0 || maximumLength <= 0)
{
return false;
}
var bytes = new byte[maximumLength];
Span<byte> current = stackalloc byte[1];
for (var index = 0; index < bytes.Length; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
return false;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
@@ -164,4 +671,16 @@ public static class JsonExports
Console.Error.WriteLine(
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} arg=0x{argument:X16}");
}
private static void TraceJsonText(string operation, ulong thisAddress, string value)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
{
return;
}
var preview = value.Length <= 128 ? value : value[..128];
Console.Error.WriteLine(
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} value={preview}");
}
}
@@ -1,8 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
@@ -34,45 +34,6 @@ public static class KernelEventFlagCompatExports
public object Gate { get; } = new();
}
private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
{
public required CpuContext Ctx { get; init; }
public required EventFlagState State { get; init; }
public required ulong Pattern { get; init; }
public required uint WaitMode { get; init; }
public required ulong ResultAddress { get; init; }
public bool Timed { get; init; }
// Timed-wait completion state; unused when Timed is false.
public ulong TimeoutAddress { get; init; }
public long DeadlineTimestamp { get; init; }
public OrbisGen2Result? Result { get; set; }
// Untimed waits stash the prepared result here at wake and return it at resume.
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
public int Resume() => Timed
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
: (int)_blockedResult;
public bool TryWake()
{
if (Timed)
{
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
}
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
{
return false;
}
_blockedResult = preparedResult;
return true;
}
}
[SysAbiExport(
Nid = "BpFoboUJoZU",
ExportName = "sceKernelCreateEventFlag",
@@ -91,17 +52,17 @@ public static class KernelEventFlagCompatExports
optionAddress != 0 ||
!IsValidAttributes(attributes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
@@ -115,11 +76,11 @@ public static class KernelEventFlagCompatExports
if (!ctx.TryWriteUInt64(outAddress, handle))
{
_eventFlags.TryRemove(handle, out _);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -132,7 +93,7 @@ public static class KernelEventFlagCompatExports
var handle = ctx[CpuRegister.Rdi];
if (!_eventFlags.TryRemove(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -141,7 +102,7 @@ public static class KernelEventFlagCompatExports
}
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -156,7 +117,7 @@ public static class KernelEventFlagCompatExports
var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -167,7 +128,7 @@ public static class KernelEventFlagCompatExports
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -181,7 +142,7 @@ public static class KernelEventFlagCompatExports
var pattern = ctx[CpuRegister.Rsi];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -190,7 +151,7 @@ public static class KernelEventFlagCompatExports
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -207,29 +168,29 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (state.Gate)
{
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
ApplyClearMode(state, pattern, waitMode);
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -249,18 +210,18 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Monitor.Enter(state.Gate);
@@ -268,65 +229,64 @@ public static class KernelEventFlagCompatExports
{
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{
return ctx.SetReturn(immediateWaitResult);
return SetReturn(ctx, immediateWaitResult);
}
if (timeoutAddress != 0)
{
if (timeoutUsec == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
TimeSpan.FromTicks((long)timeoutUsec * 10L));
var timedWaiter = new EventFlagWaiter
{
Ctx = ctx,
State = state,
Pattern = pattern,
WaitMode = waitMode,
ResultAddress = resultAddress,
Timed = true,
TimeoutAddress = timeoutAddress,
DeadlineTimestamp = deadline,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
timedWaiter,
blockDeadlineTimestamp: deadline))
{
state.WaitingThreads++;
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
// Timed waits block on a deadline instead of returning TIMED_OUT
// immediately; a zero-microsecond timeout still degrades to an
// instant poll because the deadline is already in the past.
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
var hostDeadlineMs = timeoutAddress != 0
? Environment.TickCount64 + (timeoutUsec == 0
? 0L
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
: long.MaxValue;
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId;
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
var satisfied = false;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
new EventFlagWaiter
() =>
{
Ctx = ctx,
State = state,
Pattern = pattern,
WaitMode = waitMode,
ResultAddress = resultAddress,
});
if (satisfied)
{
return (int)blockedWaitResult;
}
// Deadline expiry: report timeout with the current bits.
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
},
() =>
{
if (!TryPrepareBlockedWait(
ctx,
state,
pattern,
waitMode,
resultAddress,
out var preparedResult))
{
return false;
}
blockedWaitResult = preparedResult;
satisfied = true;
return true;
},
deadline);
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock)
@@ -334,7 +294,7 @@ public static class KernelEventFlagCompatExports
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
state.WaitingThreads++;
@@ -359,10 +319,21 @@ public static class KernelEventFlagCompatExports
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return ctx.SetReturn(pumpedWaitResult);
return SetReturn(ctx, pumpedWaitResult);
}
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
var remaining = hostDeadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(state.Gate, (int)Math.Min(remaining, HostWaitPumpMilliseconds));
}
}
finally
@@ -376,7 +347,7 @@ public static class KernelEventFlagCompatExports
state.WaitingThreads++;
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
finally
{
@@ -396,24 +367,26 @@ public static class KernelEventFlagCompatExports
var waiterCountAddress = ctx[CpuRegister.Rdx];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
if (waiterCountAddress != 0 &&
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
state.Bits = setPattern;
state.WaitingThreads = 0;
Monitor.PulseAll(state.Gate);
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
TraceEventFlag(
$"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " +
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static bool IsValidAttributes(uint attributes)
@@ -509,98 +482,90 @@ public static class KernelEventFlagCompatExports
}
}
private static bool TryCompleteBlockedTimedWait(
CpuContext ctx,
EventFlagState state,
EventFlagWaiter waiter,
ulong pattern,
uint waitMode,
ulong resultAddress)
{
lock (state.Gate)
{
if (waiter.Result is not null)
{
return true;
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_OK
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
ApplyClearMode(state, pattern, waitMode);
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
return true;
}
}
private static int CompleteBlockedTimedWait(
CpuContext ctx,
EventFlagState state,
EventFlagWaiter waiter,
ulong pattern,
uint waitMode,
ulong resultAddress,
ulong timeoutAddress,
long deadlineTimestamp)
{
lock (state.Gate)
{
if (waiter.Result is null)
{
if (IsSatisfied(state.Bits, pattern, waitMode))
{
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_OK
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
ApplyClearMode(state, pattern, waitMode);
}
}
else
{
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
}
}
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(
uint.MaxValue,
remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
}
return (int)waiter.Result.Value;
}
private static string GetEventFlagWakeKey(ulong handle) =>
$"event_flag:0x{handle:X16}";
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits);
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
{
var bytes = new byte[capacity];
Span<byte> current = stackalloc byte[1];
for (var index = 0; index < bytes.Length; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
value = string.Empty;
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static void TraceEventFlag(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
@@ -684,7 +649,7 @@ public static class KernelEventFlagCompatExports
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadByte(address, out var value))
if (TryReadByte(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
@@ -692,7 +657,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt32(address, out var value))
if (TryReadUInt32(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
@@ -700,7 +665,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt64(address, out var value))
if (TryReadUInt64(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
@@ -395,13 +395,13 @@ public static class KernelEventQueueCompatExports
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -450,7 +450,7 @@ public static class KernelEventQueueCompatExports
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -649,6 +649,60 @@ public static class KernelEventQueueCompatExports
return triggeredCount;
}
/// <summary>
/// Queues one event for every registration using <paramref name="filter"/>.
/// Unlike <see cref="TriggerRegisteredEvents"/>, this preserves distinct
/// event identifiers registered on the same queue. AGC driver completion
/// queues use this form because the driver, rather than a packet-provided
/// identifier, announces that the whole submission reached end-of-pipe.
/// </summary>
public static int TriggerRegisteredEventsDistinct(short filter)
{
HashSet<ulong>? wakeHandles = null;
var triggeredCount = 0;
lock (_eventQueueGate)
{
foreach (var (handle, registrations) in _registeredEvents)
{
foreach (var registration in registrations.Values)
{
if (registration.Filter != filter)
{
continue;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new KernelEventDeque();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
0,
1,
registration.Ident,
registration.UserData));
(wakeHandles ??= []).Add(handle);
triggeredCount++;
}
}
}
if (wakeHandles is not null)
{
foreach (var handle in wakeHandles)
{
WakeEventQueue(handle);
}
}
return triggeredCount;
}
private static bool TriggerRegisteredEvent(
ulong handle,
ulong ident,
@@ -752,7 +806,7 @@ public static class KernelEventQueueCompatExports
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -865,8 +919,29 @@ public static class KernelEventQueueCompatExports
return;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
}
@@ -62,4 +62,75 @@ public static class KernelExceptionCompatExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "il03nluKfMk",
ExportName = "sceKernelRaiseException",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int RaiseException(CpuContext ctx)
{
var targetThread = ctx[CpuRegister.Rdi];
var exceptionType = unchecked((int)ctx[CpuRegister.Rsi]);
if (targetThread == 0 || exceptionType is < 0 or >= 128)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ulong handler;
lock (_gate)
{
if (!_installedHandlers.TryGetValue(exceptionType, out handler))
{
// The kernel accepts a raise for a type with no process
// handler; there is simply no user callback to deliver.
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_IGNORE_GUEST_EXCEPTIONS"),
"1",
StringComparison.Ordinal))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
var scheduler = GuestThreadExecution.Scheduler;
string? error = null;
if (scheduler is null ||
!scheduler.TryRaiseGuestException(
ctx,
targetThread,
handler,
exceptionType,
out error))
{
Console.Error.WriteLine(
$"[LOADER][WARN] sceKernelRaiseException delivery failed: " +
$"target=0x{targetThread:X16} type=0x{exceptionType:X2} " +
$"error={error ?? "scheduler unavailable"}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_EXCEPTIONS"),
"1",
StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] guest_exception.raise " +
$"target=0x{targetThread:X16} type=0x{exceptionType:X2} " +
$"handler=0x{handler:X16}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
}
+55 -56
View File
@@ -13,8 +13,6 @@ public static class KernelExports
private static readonly object _coredumpGate = new();
private static ulong _coredumpHandler;
private static ulong _coredumpHandlerContext;
private const uint Gen4CompiledSdkVersion = 0x05000000;
private const uint Gen5CompiledSdkVersion = 0x09000000;
private readonly record struct CxaDestructorEntry(
ulong Function,
@@ -28,24 +26,7 @@ public static class KernelExports
LibraryName = "libKernel")]
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
{
var versionAddress = ctx[CpuRegister.Rdi];
if (versionAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
? Gen5CompiledSdkVersion
: Gen4CompiledSdkVersion;
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -71,20 +52,12 @@ public static class KernelExports
ExportName = "exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
[SysAbiExport(
Nid = "L1SBTkC+Cvw",
ExportName = "abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Abort(CpuContext ctx)
public static int Exit(CpuContext ctx)
{
// Route through the same graceful guest-entry-exit path as exit(): letting the call
// fall through to the host's native abort() does not unwind the guest thread cleanly.
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -199,7 +172,11 @@ public static class KernelExports
ExportName = "_exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
public static int UnderscoreExit(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ac86z8q7T8A",
@@ -218,12 +195,14 @@ public static class KernelExports
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCreate(CpuContext ctx)
=> PthreadCreateCore(ctx, ctx[CpuRegister.R8]);
private static int PthreadCreateCore(CpuContext ctx, ulong nameAddress)
{
var threadIdAddress = ctx[CpuRegister.Rdi];
var attrAddress = ctx[CpuRegister.Rsi];
var entryAddress = ctx[CpuRegister.Rdx];
var argument = ctx[CpuRegister.Rcx];
var nameAddress = ctx[CpuRegister.R8];
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
@@ -278,14 +257,20 @@ public static class KernelExports
ExportName = "pthread_create",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
public static int PosixPthreadCreate(CpuContext ctx)
{
return PthreadCreateCore(ctx, nameAddress: 0);
}
[SysAbiExport(
Nid = "Jmi+9w9u0E4",
ExportName = "pthread_create_name_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
public static int PosixPthreadCreateNameNp(CpuContext ctx)
{
return PthreadCreateCore(ctx, ctx[CpuRegister.R8]);
}
[SysAbiExport(
Nid = "3kg7rT0NQIs",
@@ -295,6 +280,9 @@ public static class KernelExports
public static int PthreadExit(CpuContext ctx)
{
var value = ctx[CpuRegister.Rdi];
// Run cleanup on the still-executable thread before unwinding it.
KernelPthreadExtendedCompatExports.RunThreadLocalDestructors(ctx);
KernelMemoryCompatExports.RunThreadDtors(ctx);
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -308,6 +296,8 @@ public static class KernelExports
public static int PosixPthreadExit(CpuContext ctx)
{
var value = ctx[CpuRegister.Rdi];
KernelPthreadExtendedCompatExports.RunThreadLocalDestructors(ctx);
KernelMemoryCompatExports.RunThreadDtors(ctx);
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -362,7 +352,10 @@ public static class KernelExports
ExportName = "pthread_join",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
public static int PosixPthreadJoin(CpuContext ctx)
{
return PthreadJoin(ctx);
}
[SysAbiExport(
Nid = "wuCroIGjt2g",
@@ -437,14 +430,21 @@ public static class KernelExports
private static string ReadCString(CpuContext ctx, ulong address, int maxLen)
{
Span<byte> buf = stackalloc byte[maxLen];
if (!ctx.Memory.TryRead(address, buf))
return $"<unreadable 0x{address:X16}>";
Span<byte> one = stackalloc byte[1];
var len = 0;
while (len < buf.Length)
{
if (!ctx.Memory.TryRead(address + (ulong)len, one))
return len == 0 ? $"<unreadable 0x{address:X16}>" : System.Text.Encoding.UTF8.GetString(buf[..len]);
int len = 0;
while (len < buf.Length && buf[len] != 0) len++;
if (one[0] == 0)
break;
try { return System.Text.Encoding.UTF8.GetString(buf.Slice(0, len)); }
catch { return System.Text.Encoding.ASCII.GetString(buf.Slice(0, len)); }
buf[len++] = one[0];
}
try { return System.Text.Encoding.UTF8.GetString(buf[..len]); }
catch { return System.Text.Encoding.ASCII.GetString(buf[..len]); }
}
private static bool ShouldTracePthread()
@@ -452,19 +452,18 @@ public static class KernelExports
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
}
private static int RequestProcessExit(CpuContext ctx, string syscallName)
[SysAbiExport(
Nid = "L1SBTkC+Cvw",
ExportName = "abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Abort(CpuContext ctx)
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
// Route through the same graceful guest-entry-exit path as exit(): letting the call
// fall through to the host's native abort() does not unwind the guest thread cleanly.
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
// shadowing them breaks every C++ static-init guard in the game.
}
@@ -0,0 +1,662 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Kernel;
/// <summary>
/// Positional and scatter/gather file I/O, synchronization, renaming, fcntl,
/// polling and asynchronous I/O (AIO) that complement the base file exports.
/// These share the same guest FD table and path-resolution helpers via the
/// partial <see cref="KernelMemoryCompatExports"/> class.
/// </summary>
public static partial class KernelMemoryCompatExports
{
// fcntl commands (FreeBSD numbering used by the PS4/PS5 libc).
private const int F_DUPFD = 0;
private const int F_GETFD = 1;
private const int F_SETFD = 2;
private const int F_GETFL = 3;
private const int F_SETFL = 4;
// poll event bits.
private const short POLLIN = 0x0001;
private const short POLLOUT = 0x0004;
// AIO request state (SCE_KERNEL_AIO_STATE_*).
private const uint AioStateCompleted = 3;
private const int SizeofAioRwRequest = 40;
private static long _nextAioSubmitId = 1;
private static readonly ConcurrentDictionary<uint, int> _aioResults = new();
private static FileStream? GetOpenFile(int fd)
{
lock (_fdGate)
{
return _openFiles.TryGetValue(fd, out var stream) ? stream : null;
}
}
// ---- Positional read/write (do not move the file offset) ----
[SysAbiExport(Nid = "ezv-RSBNKqI", ExportName = "pread",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixPread(CpuContext ctx) => KernelPreadCore(ctx);
[SysAbiExport(Nid = "+r3rMFwItV4", ExportName = "sceKernelPread",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelPread(CpuContext ctx) => KernelPreadCore(ctx);
private static int KernelPreadCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var bufferAddress = ctx[CpuRegister.Rsi];
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
var offset = unchecked((long)ctx[CpuRegister.Rcx]);
if (requested < 0 || (requested > 0 && bufferAddress == 0) || offset < 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var stream = GetOpenFile(fd);
if (stream is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (requested == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var buffer = GC.AllocateUninitializedArray<byte>(requested);
int read;
try
{
read = RandomAccess.Read(stream.SafeFileHandle, buffer, offset);
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)read);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "C2kJ-byS5rM", ExportName = "pwrite",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
[SysAbiExport(Nid = "nKWi-N2HBV4", ExportName = "sceKernelPwrite",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
private static int KernelPwriteCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var bufferAddress = ctx[CpuRegister.Rsi];
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
var offset = unchecked((long)ctx[CpuRegister.Rcx]);
if (requested < 0 || (requested > 0 && bufferAddress == 0) || offset < 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var stream = GetOpenFile(fd);
if (stream is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (requested == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var payload = GC.AllocateUninitializedArray<byte>(requested);
if (!ctx.Memory.TryRead(bufferAddress, payload))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
try
{
RandomAccess.Write(stream.SafeFileHandle, payload, offset);
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Synchronization ----
[SysAbiExport(Nid = "juWbTNM+8hw", ExportName = "fsync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFsync(CpuContext ctx) => KernelFsyncCore(ctx);
[SysAbiExport(Nid = "fTx66l5iWIA", ExportName = "sceKernelFsync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelFsync(CpuContext ctx) => KernelFsyncCore(ctx);
[SysAbiExport(Nid = "KIbJFQ0I1Cg", ExportName = "fdatasync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
[SysAbiExport(Nid = "30Rh4ixbKy4", ExportName = "sceKernelFdatasync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
private static int KernelFsyncCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var stream = GetOpenFile(fd);
if (stream is null)
{
if (fd is 0 or 1 or 2)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
try
{
stream.Flush(flushToDisk: true);
}
catch (IOException)
{
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "Y2OqwJQ3lr8", ExportName = "sync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixSync(CpuContext ctx)
{
lock (_fdGate)
{
foreach (var stream in _openFiles.Values)
{
try { stream.Flush(flushToDisk: true); } catch (IOException) { }
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Truncation ----
[SysAbiExport(Nid = "ih4CD9-gghM", ExportName = "ftruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
[SysAbiExport(Nid = "VW3TVZiM4-E", ExportName = "sceKernelFtruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
private static int KernelFtruncateCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var length = unchecked((long)ctx[CpuRegister.Rsi]);
if (length < 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var stream = GetOpenFile(fd);
if (stream is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
try
{
stream.SetLength(length);
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "ayrtszI7GBg", ExportName = "truncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
[SysAbiExport(Nid = "WlyEA-sLDf0", ExportName = "sceKernelTruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
private static int KernelTruncateCore(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var length = unchecked((long)ctx[CpuRegister.Rsi]);
if (length < 0 || !TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (IsReadOnlyGuestMutationPath(guestPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
var hostPath = ResolveGuestPath(guestPath);
try
{
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
stream.SetLength(length);
}
catch (FileNotFoundException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Rename ----
[SysAbiExport(Nid = "NN01qLRhiqU", ExportName = "rename",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixRename(CpuContext ctx) => KernelRenameCore(ctx);
[SysAbiExport(Nid = "52NcYU9+lEo", ExportName = "sceKernelRename",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelRename(CpuContext ctx) => KernelRenameCore(ctx);
private static int KernelRenameCore(CpuContext ctx)
{
if (!TryReadNullTerminatedUtf8(ctx, ctx[CpuRegister.Rdi], MaxGuestStringLength, out var fromGuest) ||
!TryReadNullTerminatedUtf8(ctx, ctx[CpuRegister.Rsi], MaxGuestStringLength, out var toGuest))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (IsReadOnlyGuestMutationPath(fromGuest) || IsReadOnlyGuestMutationPath(toGuest))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
var fromHost = ResolveGuestPath(fromGuest);
var toHost = ResolveGuestPath(toGuest);
try
{
if (Directory.Exists(fromHost))
{
Directory.Move(fromHost, toHost);
}
else
{
File.Move(fromHost, toHost, overwrite: true);
}
}
catch (FileNotFoundException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
catch (DirectoryNotFoundException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
InvalidateNegativeStatCacheForPathAndAncestors(toGuest);
AddNegativeStatCacheForGuestPath(fromGuest);
InvalidateAprFileSizeCache(fromHost);
InvalidateAprFileSizeCache(toHost);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Descriptor duplication ----
[SysAbiExport(Nid = "iiQjzvfWDq0", ExportName = "dup",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixDup(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_fdGate)
{
if (!_openFiles.TryGetValue(fd, out var stream))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
// POSIX dup shares the open file description (and offset), which is
// exactly the shared FileStream reference.
var newFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
_openFiles[newFd] = stream;
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "wdUufa9g-D8", ExportName = "dup2",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixDup2(CpuContext ctx)
{
var oldFd = unchecked((int)ctx[CpuRegister.Rdi]);
var newFd = unchecked((int)ctx[CpuRegister.Rsi]);
lock (_fdGate)
{
if (!_openFiles.TryGetValue(oldFd, out var stream))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (oldFd == newFd)
{
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// If newFd names an open file, dup2 closes it first.
if (_openFiles.TryGetValue(newFd, out var existing) && !ReferenceEquals(existing, stream))
{
try { existing.Dispose(); } catch (IOException) { }
}
_openFiles[newFd] = stream;
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- fcntl ----
[SysAbiExport(Nid = "8nY19bKoiZk", ExportName = "fcntl",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
[SysAbiExport(Nid = "SoZkxZkCHaw", ExportName = "sceKernelFcntl",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
private static int KernelFcntlCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var command = unchecked((int)ctx[CpuRegister.Rsi]);
var argument = unchecked((int)ctx[CpuRegister.Rdx]);
switch (command)
{
case F_DUPFD:
lock (_fdGate)
{
if (!_openFiles.TryGetValue(fd, out var stream))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var newFd = Math.Max((int)Interlocked.Increment(ref _nextFileDescriptor), argument);
_openFiles[newFd] = stream;
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
case F_GETFD:
case F_GETFL:
// No close-on-exec / status flags are tracked; report cleared.
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
case F_SETFD:
case F_SETFL:
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
default:
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
// ---- poll / select ----
// Regular files are always ready for both read and write, so report every
// requested descriptor as immediately ready.
[SysAbiExport(Nid = "ku7D4q1Y9PI", ExportName = "poll",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixPoll(CpuContext ctx)
{
var fdsAddress = ctx[CpuRegister.Rdi];
var count = unchecked((uint)ctx[CpuRegister.Rsi]);
if (fdsAddress == 0 || count == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// struct pollfd { int fd; short events; short revents; } == 8 bytes.
var ready = 0;
Span<byte> buffer = stackalloc byte[8];
for (uint i = 0; i < count && i < 4096; i++)
{
var entry = fdsAddress + i * 8;
if (!ctx.Memory.TryRead(entry, buffer))
{
break;
}
var events = BinaryPrimitives.ReadInt16LittleEndian(buffer[4..]);
var revents = (short)(events & (POLLIN | POLLOUT));
BinaryPrimitives.WriteInt16LittleEndian(buffer[6..], revents);
if (revents != 0)
{
ready++;
}
_ = ctx.Memory.TryWrite(entry, buffer);
}
ctx[CpuRegister.Rax] = unchecked((ulong)ready);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "T8fER+tIGgk", ExportName = "select",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixSelect(CpuContext ctx)
{
// nfds in Rdi; the fd_sets are left as-is (all reported ready) and the
// ready count returned is nfds so callers proceed without blocking.
var nfds = unchecked((int)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = unchecked((ulong)Math.Max(nfds, 0));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Asynchronous I/O (executed synchronously) ----
[SysAbiExport(Nid = "HgX7+AORI58", ExportName = "sceKernelAioSubmitReadCommands",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioSubmitReadCommands(CpuContext ctx) => KernelAioSubmit(ctx, write: false);
[SysAbiExport(Nid = "lXT0m3P-vs4", ExportName = "sceKernelAioSubmitReadCommandsMultiple",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioSubmitReadCommandsMultiple(CpuContext ctx) => KernelAioSubmit(ctx, write: false);
[SysAbiExport(Nid = "XQ8C8y+de+E", ExportName = "sceKernelAioSubmitWriteCommands",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioSubmitWriteCommands(CpuContext ctx) => KernelAioSubmit(ctx, write: true);
private static int KernelAioSubmit(CpuContext ctx, bool write)
{
var requestsAddress = ctx[CpuRegister.Rdi];
var count = unchecked((int)ctx[CpuRegister.Rsi]);
var outIdAddress = ctx[CpuRegister.Rcx];
if (requestsAddress == 0 || count <= 0 || count > 0x10000)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
// Each SceKernelAioRWRequest: offset(8) nbyte(8) buf(8) result(8) fd(4).
Span<byte> request = stackalloc byte[SizeofAioRwRequest];
Span<byte> result = stackalloc byte[16];
for (var i = 0; i < count; i++)
{
var entry = requestsAddress + (ulong)(i * SizeofAioRwRequest);
if (!ctx.Memory.TryRead(entry, request))
{
break;
}
var offset = BinaryPrimitives.ReadInt64LittleEndian(request[0..]);
var nbyte = (int)Math.Min(BinaryPrimitives.ReadUInt64LittleEndian(request[8..]), int.MaxValue);
var buf = BinaryPrimitives.ReadUInt64LittleEndian(request[16..]);
var resultPtr = BinaryPrimitives.ReadUInt64LittleEndian(request[24..]);
var fd = BinaryPrimitives.ReadInt32LittleEndian(request[32..]);
long transferred = KernelAioTransfer(ctx, fd, offset, nbyte, buf, write);
if (resultPtr != 0)
{
BinaryPrimitives.WriteInt64LittleEndian(result[0..], transferred);
BinaryPrimitives.WriteUInt32LittleEndian(result[8..], AioStateCompleted);
_ = ctx.Memory.TryWrite(resultPtr, result);
}
}
var submitId = unchecked((uint)Interlocked.Increment(ref _nextAioSubmitId));
_aioResults[submitId] = 0;
if (outIdAddress != 0)
{
Span<byte> idBuffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(idBuffer, submitId);
_ = ctx.Memory.TryWrite(outIdAddress, idBuffer);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static long KernelAioTransfer(CpuContext ctx, int fd, long offset, int nbyte, ulong buf, bool write)
{
if (nbyte <= 0 || buf == 0 || offset < 0)
{
return 0;
}
var stream = GetOpenFile(fd);
if (stream is null)
{
return -1;
}
var scratch = GC.AllocateUninitializedArray<byte>(nbyte);
try
{
if (write)
{
if (!ctx.Memory.TryRead(buf, scratch))
{
return -1;
}
RandomAccess.Write(stream.SafeFileHandle, scratch, offset);
return nbyte;
}
var read = RandomAccess.Read(stream.SafeFileHandle, scratch, offset);
if (read > 0 && !ctx.Memory.TryWrite(buf, scratch.AsSpan(0, read)))
{
return -1;
}
return read;
}
catch (IOException)
{
return -1;
}
}
[SysAbiExport(Nid = "o7O4z3jwKzo", ExportName = "sceKernelAioPollRequests",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioPollRequests(CpuContext ctx) => KernelAioComplete(ctx);
[SysAbiExport(Nid = "lgK+oIWkJyA", ExportName = "sceKernelAioWaitRequests",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioWaitRequests(CpuContext ctx) => KernelAioComplete(ctx);
private static int KernelAioComplete(CpuContext ctx)
{
// Submission already performed the I/O synchronously, so every request
// reports completed. Rsi points at the state-out array, Rdx = count.
var statesAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (statesAddress != 0 && count > 0 && count <= 0x10000)
{
Span<byte> state = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(state, AioStateCompleted);
for (var i = 0; i < count; i++)
{
_ = ctx.Memory.TryWrite(statesAddress + (ulong)(i * sizeof(uint)), state);
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "fR521KIGgb8", ExportName = "sceKernelAioCancelRequest",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioCancelRequest(CpuContext ctx)
{
// Already completed; report the completed state if an out pointer given.
var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress != 0)
{
Span<byte> state = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(state, AioStateCompleted);
_ = ctx.Memory.TryWrite(stateAddress, state);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "5TgME6AYty4", ExportName = "sceKernelAioDeleteRequest",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioDeleteRequest(CpuContext ctx)
{
var submitId = unchecked((uint)ctx[CpuRegister.Rdi]);
_aioResults.TryRemove(submitId, out _);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
File diff suppressed because it is too large Load Diff
@@ -9,8 +9,16 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelModuleRegistry
{
public enum ModuleStartState
{
NotStarted,
Starting,
Started,
}
private static readonly object _gate = new();
private static readonly Dictionary<int, ModuleEntry> _modulesByHandle = new();
private static readonly Dictionary<int, Dictionary<string, ulong>> _symbolsByHandle = new();
private static readonly Dictionary<string, int> _handleByPath = new(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, int> _handleByName = new(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<int, int> _sysmoduleHandleById = new();
@@ -32,6 +40,11 @@ public static class KernelModuleRegistry
ulong BaseAddress,
ulong EndAddress,
ulong EntryPoint,
ulong InitEntryPoint,
ulong EhFrameHeaderAddress,
ulong EhFrameAddress,
ulong EhFrameSize,
ModuleStartState StartState,
bool IsMain,
bool IsSystemModule);
@@ -40,6 +53,7 @@ public static class KernelModuleRegistry
lock (_gate)
{
_modulesByHandle.Clear();
_symbolsByHandle.Clear();
_handleByPath.Clear();
_handleByName.Clear();
_sysmoduleHandleById.Clear();
@@ -52,6 +66,10 @@ public static class KernelModuleRegistry
ulong baseAddress,
ulong size,
ulong entryPoint,
ulong initEntryPoint,
ulong ehFrameHeaderAddress,
ulong ehFrameAddress,
ulong ehFrameSize,
bool isMain,
bool isSystemModule = false)
{
@@ -67,6 +85,10 @@ public static class KernelModuleRegistry
BaseAddress = baseAddress,
EndAddress = ComputeEnd(baseAddress, size),
EntryPoint = entryPoint,
InitEntryPoint = initEntryPoint,
EhFrameHeaderAddress = ehFrameHeaderAddress,
EhFrameAddress = ehFrameAddress,
EhFrameSize = ehFrameSize,
IsMain = existing.IsMain || isMain,
IsSystemModule = existing.IsSystemModule || isSystemModule,
};
@@ -84,6 +106,11 @@ public static class KernelModuleRegistry
BaseAddress: baseAddress,
EndAddress: ComputeEnd(baseAddress, size),
EntryPoint: entryPoint,
InitEntryPoint: initEntryPoint,
EhFrameHeaderAddress: ehFrameHeaderAddress,
EhFrameAddress: ehFrameAddress,
EhFrameSize: ehFrameSize,
StartState: ModuleStartState.NotStarted,
IsMain: isMain,
IsSystemModule: isSystemModule);
_modulesByHandle[handle] = entry;
@@ -119,6 +146,11 @@ public static class KernelModuleRegistry
BaseAddress: 0,
EndAddress: 0,
EntryPoint: 0,
InitEntryPoint: 0,
EhFrameHeaderAddress: 0,
EhFrameAddress: 0,
EhFrameSize: 0,
StartState: ModuleStartState.Started,
IsMain: false,
IsSystemModule: isSystemModule);
_modulesByHandle[handle] = entry;
@@ -203,6 +235,97 @@ public static class KernelModuleRegistry
}
}
/// <summary>
/// Atomically claims a module initializer. A module can be observed while
/// it is starting (for recursive loader calls), but its DT_INIT routine is
/// executed at most once after a successful start.
/// </summary>
public static bool TryBeginModuleStart(int handle, out ModuleEntry module)
{
lock (_gate)
{
if (!_modulesByHandle.TryGetValue(handle, out module))
{
return false;
}
if (module.StartState != ModuleStartState.NotStarted)
{
return false;
}
if (module.InitEntryPoint < 0x10000)
{
module = module with { StartState = ModuleStartState.Started };
_modulesByHandle[handle] = module;
return false;
}
module = module with { StartState = ModuleStartState.Starting };
_modulesByHandle[handle] = module;
return true;
}
}
public static void CompleteModuleStart(int handle, bool succeeded)
{
lock (_gate)
{
if (!_modulesByHandle.TryGetValue(handle, out var module) ||
module.StartState != ModuleStartState.Starting)
{
return;
}
_modulesByHandle[handle] = module with
{
StartState = succeeded ? ModuleStartState.Started : ModuleStartState.NotStarted,
};
}
}
public static void RegisterModuleSymbols(int handle, IReadOnlyDictionary<string, ulong> symbols)
{
ArgumentNullException.ThrowIfNull(symbols);
lock (_gate)
{
if (!_modulesByHandle.ContainsKey(handle))
{
return;
}
if (!_symbolsByHandle.TryGetValue(handle, out var destination))
{
destination = new Dictionary<string, ulong>(StringComparer.Ordinal);
_symbolsByHandle[handle] = destination;
}
foreach (var (name, address) in symbols)
{
if (!string.IsNullOrWhiteSpace(name) && address >= 0x10000)
{
destination.TryAdd(name, address);
}
}
}
}
public static bool TryResolveModuleSymbol(int handle, string symbolName, out ulong address)
{
address = 0;
if (string.IsNullOrWhiteSpace(symbolName))
{
return false;
}
lock (_gate)
{
return _symbolsByHandle.TryGetValue(handle, out var symbols) &&
symbols.TryGetValue(symbolName, out address) &&
address >= 0x10000;
}
}
public static bool TryFindByPathOrName(string? modulePathOrName, out ModuleEntry module)
{
module = default;
File diff suppressed because it is too large Load Diff
@@ -2,8 +2,10 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -15,6 +17,8 @@ public static class KernelPthreadExtendedCompatExports
private const int DefaultDetachState = 0;
private const ulong DefaultGuardSize = 0x1000UL;
private const ulong DefaultStackSize = 0x1_00000UL;
private const ulong NativeGuestStackSize = 0x20_0000UL;
private const ulong NativeGuestStackStride = 0x100_0000UL;
private const int DefaultInheritSched = 4;
private const int DefaultSchedPolicy = 1;
private const int DefaultSchedPriority = DefaultThreadPriority;
@@ -223,6 +227,13 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "+U1R4WtXvoc",
ExportName = "pthread_detach",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadDetach(CpuContext ctx) => PthreadDetach(ctx);
[SysAbiExport(
Nid = "How7B8Oet6k",
ExportName = "scePthreadGetname",
@@ -273,6 +284,7 @@ public static class KernelPthreadExtendedCompatExports
state.Attributes = state.Attributes with { AffinityMask = mask };
}
_ = GuestThreadExecution.Scheduler?.TrySetGuestThreadAffinity(thread, mask);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -326,7 +338,7 @@ public static class KernelPthreadExtendedCompatExports
priority = GetOrCreateThreadStateLocked(thread).Priority;
}
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -354,6 +366,9 @@ public static class KernelPthreadExtendedCompatExports
GetOrCreateThreadStateLocked(thread).Priority = priority;
}
// Apply to the live scheduler thread so runtime priority changes take
// effect, not just the local bookkeeping snapshot.
_ = GuestThreadExecution.Scheduler?.TrySetGuestThreadPriority(thread, priority);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -373,7 +388,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -408,9 +423,9 @@ public static class KernelPthreadExtendedCompatExports
public static int PthreadGetschedparam(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var outPolicyAddress = ctx[CpuRegister.Rsi];
var outSchedParamAddress = ctx[CpuRegister.Rdx];
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
var policyAddress = ctx[CpuRegister.Rsi];
var schedParamAddress = ctx[CpuRegister.Rdx];
if (thread == 0 || policyAddress == 0 || schedParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
@@ -424,8 +439,8 @@ public static class KernelPthreadExtendedCompatExports
priority = state.Priority;
}
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
!ctx.TryWriteInt32(outSchedParamAddress, priority))
if (!TryWriteInt32(ctx, policyAddress, policy) ||
!TryWriteInt32(ctx, schedParamAddress, priority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -528,6 +543,22 @@ public static class KernelPthreadExtendedCompatExports
lock (_stateGate)
{
var threadState = GetOrCreateThreadStateLocked(thread);
// The native executor maps guest pthread stacks itself, after the
// kernel-facing thread object has been created. Report that live
// mapping when a thread asks for its own attributes. IL2CPP's
// conservative collector uses these two fields to register the stack;
// returning the default null address lets it recycle objects that are
// still reachable only from guest registers/stack frames.
if (thread == KernelPthreadState.GetCurrentThreadHandle() &&
TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out var stackAddress))
{
threadState.Attributes = threadState.Attributes with
{
StackAddress = stackAddress,
StackSize = NativeGuestStackSize,
};
}
_attrStates[outAttrAddress] = threadState.Attributes;
}
@@ -535,6 +566,28 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryInferNativeGuestStack(ulong stackPointer, out ulong stackAddress)
{
stackAddress = 0;
var candidate = stackPointer & ~(NativeGuestStackStride - 1);
if (stackPointer - candidate >= NativeGuestStackSize)
{
return false;
}
var highestStack = OperatingSystem.IsWindows()
? 0x00007FFF_F000_0000UL
: 0x00006FFF_F000_0000UL;
var lowestStack = highestStack - (63 * NativeGuestStackStride);
if (candidate < lowestStack || candidate > highestStack)
{
return false;
}
stackAddress = candidate;
return true;
}
[SysAbiExport(
Nid = "8+s5BzZjxSg",
ExportName = "scePthreadAttrGetaffinity",
@@ -584,7 +637,7 @@ public static class KernelPthreadExtendedCompatExports
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -827,7 +880,7 @@ public static class KernelPthreadExtendedCompatExports
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -850,7 +903,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1207,7 +1260,7 @@ public static class KernelPthreadExtendedCompatExports
}
}
if (!ctx.TryWriteInt32(outKeyAddress, key))
if (!TryWriteInt32(ctx, outKeyAddress, key))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1315,6 +1368,72 @@ public static class KernelPthreadExtendedCompatExports
LibraryName = "libKernel")]
public static int OrbisPthreadGetspecific(CpuContext ctx) => PosixPthreadGetspecific(ctx);
private const int PthreadDestructorIterations = 4;
/// <summary>
/// Runs the current thread's pthread TLS-key destructors, as POSIX
/// requires on thread exit. Each key holding a non-null value with a
/// registered destructor has its value cleared first and the destructor
/// then invoked with the previous value; this repeats up to
/// PTHREAD_DESTRUCTOR_ITERATIONS times so destructors that set new
/// thread-local values are themselves cleaned up. Called on the exiting
/// guest thread while it is still executable.
/// </summary>
public static void RunThreadLocalDestructors(CpuContext ctx)
{
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return;
}
var threadHandle = KernelPthreadState.GetCurrentThreadHandle();
if (!_threadLocalSpecific.TryGetValue(threadHandle, out var values))
{
return;
}
for (var iteration = 0; iteration < PthreadDestructorIterations; iteration++)
{
var ranAny = false;
foreach (var entry in values)
{
var value = entry.Value;
if (value == 0 ||
!_tlsKeys.TryGetValue(entry.Key, out var keyState) ||
keyState.Destructor == 0)
{
continue;
}
// Clear before invoking, per POSIX, so a destructor that
// re-sets the key is handled on the next iteration.
if (!values.TryUpdate(entry.Key, 0, value))
{
continue;
}
ranAny = true;
_ = scheduler.TryCallGuestFunction(
ctx,
keyState.Destructor,
value,
0,
0,
0,
"pthread_tls_destructor",
out _);
}
if (!ranAny)
{
break;
}
}
_threadLocalSpecific.TryRemove(threadHandle, out _);
}
private static int PthreadRwlockLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
{
if (rwlockAddress == 0)
@@ -1680,4 +1799,24 @@ public static class KernelPthreadExtendedCompatExports
payload[^1] = 0;
return ctx.Memory.TryWrite(address, payload);
}
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
@@ -13,9 +14,6 @@ public static class KernelSemaphoreCompatExports
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
private static int _nextSemaphoreHandle = 1;
[ThreadStatic]
private static int _semaPollBackoffCount;
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
@@ -25,35 +23,9 @@ public static class KernelSemaphoreCompatExports
public required int MaxCount { get; init; }
public int Count { get; set; }
public int WaitingThreads { get; set; }
public int CancelEpoch { get; set; }
public bool Deleted { get; set; }
public object Gate { get; } = new();
}
private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
{
public required KernelSemaphoreState Semaphore { get; init; }
public required int NeedCount { get; init; }
public required int CancelEpochAtBlock { get; init; }
public bool Timed { get; init; }
// Timed-wait completion state; unused when Timed is false.
public CpuContext? Ctx { get; init; }
public ulong TimeoutAddress { get; init; }
public long DeadlineTimestamp { get; init; }
// Written and read only under the owning semaphore's Gate.
public int? Result { get; set; }
public int Resume() => Timed
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
: CompleteBlockedSemaWait(Semaphore, this);
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
}
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
[SysAbiExport(
Nid = "188x57JYp0g",
ExportName = "sceKernelCreateSema",
@@ -76,12 +48,12 @@ public static class KernelSemaphoreCompatExports
initialCount > maxCount ||
optionAddress != 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
@@ -90,7 +62,7 @@ public static class KernelSemaphoreCompatExports
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
var state = new KernelSemaphoreState
_semaphores[handle] = new KernelSemaphoreState
{
Name = name,
WakeKey = GetSemaphoreWakeKey(handle),
@@ -98,28 +70,15 @@ public static class KernelSemaphoreCompatExports
MaxCount = maxCount,
Count = initialCount,
};
_semaphores[handle] = state;
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
// Handles are sequential and guest-predictable, so a hostile guest can
// race a WaitSema onto the handle between publication above and this
// rollback. Strand-proof that waiter exactly like DeleteSema does.
lock (state.Gate)
{
state.Deleted = true;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (_traceSema)
{
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -135,125 +94,144 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var pollTimedOut = false;
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
if (_traceSema)
if (timeoutAddress != 0)
{
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
_ = TryWriteUInt32(ctx, timeoutAddress, timeoutUsec);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
semaphore.WaitingThreads++;
}
// Block cooperatively: the wake predicate atomically acquires the
// tokens (so a wake commits the acquisition), while the resume
// handler distinguishes a real acquisition from a deadline expiry.
var acquired = false;
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
bool WakePredicate()
{
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
acquired = true;
return true;
}
return false;
}
}
int ResumeWait()
{
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (timeoutMicros == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
pollTimedOut = true;
}
else
{
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
var timedWaiter = new SemaphoreWaiter
{
Semaphore = semaphore,
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
Timed = true,
Ctx = ctx,
TimeoutAddress = timeoutAddress,
DeadlineTimestamp = deadline,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
semaphore.WakeKey,
timedWaiter,
blockDeadlineTimestamp: deadline))
{
semaphore.WaitingThreads++;
if (_traceSema)
{
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Host-owned threads cannot park in the guest scheduler; degrade to the
// immediate-timeout poll the callers already tolerate.
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
pollTimedOut = true;
}
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
if (!pollTimedOut)
if (acquired)
{
var waiter = new SemaphoreWaiter
{
Semaphore = semaphore,
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
};
if (!GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
semaphore.WakeKey,
waiter))
{
if (_traceSema)
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++;
if (_traceSema)
{
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"wait-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
lock (semaphore.Gate)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
}
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
if ((++_semaPollBackoffCount & 255) == 0)
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
ResumeWait,
WakePredicate,
deadline))
{
Thread.Sleep(0);
}
else
{
Thread.Yield();
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
// Not a guest thread (or no scheduler): fall back to a host-thread
// wait so the semantics still hold on non-cooperative callers.
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
}
private static int WaitSemaphoreOnHostThread(
CpuContext ctx,
KernelSemaphoreState semaphore,
uint handle,
int needCount,
ulong timeoutAddress,
uint timeoutUsec)
{
var deadlineMs = timeoutAddress != 0
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
: long.MaxValue;
lock (semaphore.Gate)
{
TraceSemaphore(
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
while (semaphore.Count < needCount)
{
var remaining = deadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
}
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore(
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
private static string GetSemaphoreWakeKey(uint handle) => $"sceKernelWaitSema:{handle:X8}";
[SysAbiExport(
Nid = "12wOHk8ywb0",
ExportName = "sceKernelPollSema",
@@ -263,31 +241,25 @@ public static class KernelSemaphoreCompatExports
{
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count < needCount)
{
if (_traceSema)
{
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
semaphore.Count -= needCount;
if (_traceSema)
{
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -300,34 +272,31 @@ public static class KernelSemaphoreCompatExports
{
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (signalCount <= 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count > semaphore.MaxCount - signalCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
semaphore.Count += signalCount;
if (_traceSema)
{
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
// Wake host-thread waiters parked in the fallback path.
Monitor.PulseAll(semaphore.Gate);
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
}
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
// whose needCount exceeds the remaining count stays parked while a smaller
// waiter can proceed.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
// Wake cooperatively-blocked guest threads; their wake predicate
// acquires the tokens atomically, so this respects the new count.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -339,35 +308,29 @@ public static class KernelSemaphoreCompatExports
{
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (setCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.CancelEpoch++;
// WaitingThreads is NOT zeroed here: each canceled waiter decrements it
// exactly once in its wake handler. Zeroing here as well would double-count
// and silently absorb the increment of a waiter that parks between this
// gate release and the wake-all below.
if (_traceSema)
{
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
}
semaphore.WaitingThreads = 0;
Monitor.PulseAll(semaphore.Gate);
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -380,138 +343,238 @@ public static class KernelSemaphoreCompatExports
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
if (!_semaphores.TryRemove(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
// Delete succeeds even with blocked waiters; they wake with the deleted
// result (the SCE kernel wakes waiters with the EACCES-class code).
lock (semaphore.Gate)
{
semaphore.Deleted = true;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
if (_traceSema)
{
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Wake handler: runs under the scheduler's guest-thread gate (lock order:
// scheduler gate -> semaphore gate). Returns true iff the waiter has a final
// result and should be re-readied; false leaves it parked.
private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
[SysAbiExport(
Nid = "pDuPEf3m4fI",
ExportName = "sem_init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemInit(CpuContext ctx)
{
lock (semaphore.Gate)
var semaphoreAddress = ctx[CpuRegister.Rdi];
var initialCountValue = ctx[CpuRegister.Rdx];
if (semaphoreAddress == 0 || initialCountValue > int.MaxValue)
{
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
if (handle == 0)
{
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
var initialCount = unchecked((int)initialCountValue);
_semaphores[handle] = new KernelSemaphoreState
{
Name = $"posix@0x{semaphoreAddress:X16}",
WakeKey = GetSemaphoreWakeKey(handle),
InitialCount = initialCount,
MaxCount = int.MaxValue,
Count = initialCount,
};
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSemaphore($"posix-init address=0x{semaphoreAddress:X16} handle=0x{handle:X8} count={initialCount}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
[SysAbiExport(
Nid = "YCV5dGGBcCo",
ExportName = "sem_wait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemWait(CpuContext ctx)
{
if (waiter.Result is not null)
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
return true;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (semaphore.Deleted)
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
}
return true;
}
if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
}
return true;
}
if (semaphore.Count >= waiter.NeedCount)
{
semaphore.Count -= waiter.NeedCount;
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
return true;
}
return false;
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = 1;
ctx[CpuRegister.Rdx] = 0;
return KernelWaitSema(ctx);
}
// Resume handler: runs on the woken guest thread outside the scheduler gate;
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
[SysAbiExport(
Nid = "WBWzsRifCEA",
ExportName = "sem_trywait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemTryWait(CpuContext ctx)
{
lock (semaphore.Gate)
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
// Nothing readies a parked semaphore waiter without the wake handler
// resolving it, so reaching here means the scheduler contract changed.
Console.Error.WriteLine(
$"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
}
return waiter.Result!.Value;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = 1;
return KernelPollSema(ctx, handle, 1);
}
private static int CompleteBlockedTimedSemaWait(
CpuContext ctx,
KernelSemaphoreState semaphore,
SemaphoreWaiter waiter,
ulong timeoutAddress,
long deadlineTimestamp)
[SysAbiExport(
Nid = "w5IHyvahg-o",
ExportName = "sem_timedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemTimedWait(CpuContext ctx)
{
int result;
lock (semaphore.Gate)
var timeoutAddress = ctx[CpuRegister.Rsi];
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
}
result = waiter.Result!.Value;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = 1;
ctx[CpuRegister.Rdx] = timeoutAddress;
return KernelWaitSema(ctx);
}
[SysAbiExport(
Nid = "IKP8typ0QUk",
ExportName = "sem_post",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemPost(CpuContext ctx)
{
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = 1;
return KernelSignalSema(ctx, handle, 1);
}
[SysAbiExport(
Nid = "Bq+LRV-N6Hk",
ExportName = "sem_getvalue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemGetValue(CpuContext ctx)
{
var semaphoreAddress = ctx[CpuRegister.Rdi];
var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0 ||
!TryGetPosixSemaphoreHandle(ctx, semaphoreAddress, out var handle) ||
!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
int count;
lock (semaphore.Gate)
{
count = semaphore.Count;
}
return TryWriteUInt32(ctx, valueAddress, unchecked((uint)count))
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "cDW233RAwWo",
ExportName = "sem_destroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemDestroy(CpuContext ctx)
{
var semaphoreAddress = ctx[CpuRegister.Rdi];
if (!TryGetPosixSemaphoreHandle(ctx, semaphoreAddress, out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rdi] = handle;
var result = KernelDeleteSema(ctx);
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteUInt32(ctx, semaphoreAddress, 0);
}
return result;
}
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
{
handle = 0;
return semaphoreAddress != 0 &&
TryReadUInt32(ctx, semaphoreAddress, out handle) &&
handle != 0;
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0 || maxLength <= 0)
{
return false;
}
var bytes = new byte[Math.Min(maxLength, 4096)];
Span<byte> current = stackalloc byte[1];
for (var i = 0; i < bytes.Length; i++)
{
if (!ctx.Memory.TryRead(address + (ulong)i, current))
{
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, i);
return true;
}
bytes[i] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
// Call sites must check this before building the interpolated message; the trace
// strings would otherwise be allocated on every semaphore op even with tracing off.
private static readonly bool _traceSema =
@@ -521,4 +584,10 @@ public static class KernelSemaphoreCompatExports
{
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
}
private static string FormatCallSite(CpuContext ctx)
{
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnAddress);
return $"guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{returnAddress:X16}";
}
}
+16 -2
View File
@@ -14,9 +14,9 @@ public static class LibcStdioExports
private const int MaxPathLength = 4096;
private const int MaxModeLength = 16;
private const int ReadChunkSize = 1024 * 1024;
private const ulong GuestFileObjectSize = 0x100;
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
private static long _nextHandle = 0x1000 - 8;
private static readonly bool _traceStdio =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
@@ -96,7 +96,21 @@ public static class LibcStdioExports
stream.Seek(0, SeekOrigin.End);
}
var handle = unchecked((ulong)Interlocked.Add(ref _nextHandle, 8));
// A FILE* can cross between HLE stdio and a title's bundled libc.
// Back the handle with zeroed guest memory so native helpers such
// as _Lockfilelock can safely inspect the FILE object instead of
// dereferencing the old small integer token (0x1000, 0x1008...).
if (!KernelMemoryCompatExports.TryAllocateHleData(
ctx,
GuestFileObjectSize,
alignment: 0x10,
out var handle))
{
stream.Dispose();
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
_fileHandles[handle] = stream;
if (_traceStdio)
+305 -1
View File
@@ -3,6 +3,9 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using SharpEmu.HLE;
@@ -12,13 +15,29 @@ public static class NetExports
{
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
private const int NetErrnoBadFileDescriptor = 9;
private const int NetErrnoInvalidArgument = 22;
private const int NetErrnoWouldBlock = 35;
private const int NetErrnoAddressInUse = 48;
private const int NetErrnoNotInitialized = 200;
private const int MaxNameLength = 256;
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
private static int _nextPoolId;
private static int _nextResolverId = 0x2000;
private static bool _initialized;
private static int _nextSocketId = 0x4000;
// The platform networking module is usable immediately after it is loaded.
// Games and middleware (notably FMOD) can create internal sockets before an
// explicit sceNetInit call reaches application code.
private static bool _initialized = true;
[ThreadStatic]
private static nint _errnoAddress;
private sealed record NetPool(string Name, int Size, int Flags);
@@ -46,10 +65,232 @@ public static class NetExports
_initialized = false;
_pools.Clear();
_resolvers.Clear();
foreach (var socket in _sockets.Values)
{
socket.Dispose();
}
_sockets.Clear();
TraceNet("term", 0, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "Q4qBuN-c0ZM",
ExportName = "sceNetSocket",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSocket(CpuContext ctx)
{
if (!_initialized)
{
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
}
var nameAddress = ctx[CpuRegister.Rdi];
var family = unchecked((int)ctx[CpuRegister.Rsi]);
var type = unchecked((int)ctx[CpuRegister.Rdx]);
var protocol = unchecked((int)ctx[CpuRegister.Rcx]);
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
if (!TryTranslateSocketParameters(family, type, protocol, out var addressFamily, out var socketType, out var protocolType))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
try
{
var socket = new Socket(addressFamily, socketType, protocolType);
var id = Interlocked.Increment(ref _nextSocketId);
_sockets[id] = socket;
TraceNet("socket.create", id, unchecked((ulong)family), unchecked((ulong)type), unchecked((ulong)protocol));
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "45ggEzakPJQ",
ExportName = "sceNetSocketClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSocketClose(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryRemove(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
socket.Dispose();
TraceNet("socket.close", id, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "2mKX2Spso7I",
ExportName = "sceNetSetsockopt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSetsockopt(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var level = unchecked((int)ctx[CpuRegister.Rsi]);
var option = unchecked((int)ctx[CpuRegister.Rdx]);
var valueAddress = ctx[CpuRegister.Rcx];
var valueLength = unchecked((int)ctx[CpuRegister.R8]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
// ORBIS_NET_SOL_SOCKET / ORBIS_NET_SO_NBIO. This is the first option
// used by FMOD's discovery socket and maps directly to host blocking.
if (level == 0xFFFF && option == 0x1200)
{
Span<byte> value = stackalloc byte[sizeof(int)];
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
socket.Blocking = BinaryPrimitives.ReadInt32LittleEndian(value) == 0;
TraceNet("socket.nonblocking", id, socket.Blocking ? 0UL : 1UL, 0, 0);
return ctx.SetReturn(0);
}
// ORBIS_NET_SO_REUSEADDR uses the BSD value 0x0004.
if (level == 0xFFFF && option == 0x0004)
{
Span<byte> value = stackalloc byte[sizeof(int)];
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
socket.SetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress,
BinaryPrimitives.ReadInt32LittleEndian(value) != 0);
TraceNet("socket.reuseaddr", id, BinaryPrimitives.ReadUInt32LittleEndian(value), 0, 0);
return ctx.SetReturn(0);
}
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
[SysAbiExport(
Nid = "bErx49PgxyY",
ExportName = "sceNetBind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetBind(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
if (!TryReadSocketAddress(ctx, ctx[CpuRegister.Rsi], unchecked((int)ctx[CpuRegister.Rdx]), out var endpoint))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
try
{
socket.Bind(endpoint);
TraceNet("socket.bind", id, unchecked((ulong)endpoint.Port), 0, 0);
return ctx.SetReturn(0);
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
return SetNetError(ctx, NetErrorAddressInUse, NetErrnoAddressInUse);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "kOj1HiAGE54",
ExportName = "sceNetListen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetListen(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
try
{
socket.Listen(Math.Max(0, unchecked((int)ctx[CpuRegister.Rsi])));
TraceNet("socket.listen", id, ctx[CpuRegister.Rsi], 0, 0);
return ctx.SetReturn(0);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "PIWqhn9oSxc",
ExportName = "sceNetAccept",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetAccept(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
try
{
var accepted = socket.Accept();
var acceptedId = Interlocked.Increment(ref _nextSocketId);
_sockets[acceptedId] = accepted;
TraceNet("socket.accept", acceptedId, unchecked((ulong)id), 0, 0);
ctx[CpuRegister.Rax] = unchecked((ulong)acceptedId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (SocketException ex) when (ex.SocketErrorCode is SocketError.WouldBlock or SocketError.IOPending)
{
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "HQOwnfMGipQ",
ExportName = "sceNetErrnoLoc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetErrnoLoc(CpuContext ctx)
{
if (_errnoAddress == 0)
{
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
Marshal.WriteInt32(_errnoAddress, 0);
}
ctx[CpuRegister.Rax] = unchecked((ulong)_errnoAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "dgJBaeJnGpo",
ExportName = "sceNetPoolCreate",
@@ -207,6 +448,69 @@ public static class NetExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetNetError(CpuContext ctx, int result, int errno)
{
if (_errnoAddress == 0)
{
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
}
Marshal.WriteInt32(_errnoAddress, errno);
return ctx.SetReturn(result);
}
private static bool TryTranslateSocketParameters(
int family,
int type,
int protocol,
out AddressFamily addressFamily,
out SocketType socketType,
out ProtocolType protocolType)
{
addressFamily = family switch
{
2 => AddressFamily.InterNetwork,
28 => AddressFamily.InterNetworkV6,
_ => AddressFamily.Unspecified,
};
socketType = type switch
{
1 => SocketType.Stream,
2 => SocketType.Dgram,
_ => SocketType.Unknown,
};
protocolType = protocol switch
{
0 when socketType == SocketType.Stream => ProtocolType.Tcp,
0 when socketType == SocketType.Dgram => ProtocolType.Udp,
6 => ProtocolType.Tcp,
17 => ProtocolType.Udp,
_ => ProtocolType.Unknown,
};
return addressFamily != AddressFamily.Unspecified &&
socketType != SocketType.Unknown &&
protocolType != ProtocolType.Unknown;
}
private static bool TryReadSocketAddress(CpuContext ctx, ulong address, int length, out IPEndPoint endpoint)
{
endpoint = new IPEndPoint(IPAddress.Any, 0);
if (address == 0 || length < 16)
{
return false;
}
Span<byte> bytes = stackalloc byte[16];
if (!ctx.Memory.TryRead(address, bytes) || bytes[1] != 2)
{
return false;
}
var port = BinaryPrimitives.ReadUInt16BigEndian(bytes[2..4]);
endpoint = new IPEndPoint(new IPAddress(bytes[4..8]), port);
return true;
}
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
+189 -84
View File
@@ -4,6 +4,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.Ngs2;
@@ -13,7 +14,7 @@ public static class Ngs2Exports
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
private const ulong HandleStorageSize = 0x1000;
private const ulong HandleStorageSize = 0x20;
private const int RenderBufferInfoSize = 0x18;
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
@@ -38,13 +39,13 @@ public static class Ngs2Exports
var outHandleAddress = ctx[CpuRegister.Rdx];
if (outHandleAddress == 0)
{
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
}
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
!ctx.TryWriteUInt64(outHandleAddress, handle))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
lock (StateGate)
@@ -52,7 +53,7 @@ public static class Ngs2Exports
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -67,7 +68,7 @@ public static class Ngs2Exports
{
if (!Systems.Remove(handle))
{
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
}
var rackHandles = Racks
@@ -80,7 +81,7 @@ public static class Ngs2Exports
}
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -97,19 +98,19 @@ public static class Ngs2Exports
{
if (!Systems.ContainsKey(systemHandle))
{
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
}
}
if (outHandleAddress == 0)
{
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
}
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
!ctx.TryWriteUInt64(outHandleAddress, handle))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
lock (StateGate)
@@ -117,7 +118,7 @@ public static class Ngs2Exports
Racks[handle] = new RackState(systemHandle, rackId);
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -132,13 +133,13 @@ public static class Ngs2Exports
{
if (!Racks.ContainsKey(handle))
{
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
}
RemoveRackLocked(handle);
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -155,7 +156,7 @@ public static class Ngs2Exports
{
if (!Racks.ContainsKey(rackHandle))
{
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
}
var existing = Voices.FirstOrDefault(
@@ -163,20 +164,20 @@ public static class Ngs2Exports
if (existing.Key != 0)
{
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
if (outHandleAddress == 0)
{
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
}
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
!ctx.TryWriteUInt64(outHandleAddress, handle))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
lock (StateGate)
@@ -184,7 +185,7 @@ public static class Ngs2Exports
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -196,19 +197,12 @@ public static class Ngs2Exports
{
lock (StateGate)
{
return ctx.SetReturn(
Voices.ContainsKey(ctx[CpuRegister.Rdi])
? 0
: OrbisNgs2ErrorInvalidVoiceHandle);
return SetReturn(
ctx,
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle);
}
}
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
// their real libraries; the NIDs below are verified against the hash.
[SysAbiExport(
Nid = "AbYvTOZ8Pts",
ExportName = "sceNgs2VoiceRunCommands",
@@ -216,48 +210,6 @@ public static class Ngs2Exports
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
[SysAbiExport(
Nid = "-TOuuAQ-buE",
ExportName = "sceNgs2VoiceGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "rEh728kXk3w",
ExportName = "sceNgs2VoiceGetStateFlags",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "xa8oL9dmXkM",
ExportName = "sceNgs2PanInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "1WsleK-MTkE",
ExportName = "sceNgs2GeomCalcListener",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "0lbbayqDNoE",
ExportName = "sceNgs2GeomResetSourceParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "7Lcfo8SmpsU",
ExportName = "sceNgs2GeomResetListenerParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "i0VnXM-C9fc",
ExportName = "sceNgs2SystemRender",
@@ -272,13 +224,13 @@ public static class Ngs2Exports
{
if (!Systems.ContainsKey(systemHandle))
{
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
}
}
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
for (uint i = 0; i < bufferInfoCount; i++)
@@ -287,14 +239,14 @@ public static class Ngs2Exports
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (bufferAddress != 0 && bufferSize != 0)
{
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
}
@@ -306,11 +258,138 @@ public static class Ngs2Exports
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "pgFAiLR5qT4",
ExportName = "sceNgs2SystemQueryBufferSize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2SystemQueryBufferSize(CpuContext ctx) => WriteBufferSize(ctx, ctx[CpuRegister.Rsi]);
[SysAbiExport(
Nid = "0eFLVCfWVds",
ExportName = "sceNgs2RackQueryBufferSize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2RackQueryBufferSize(CpuContext ctx) => WriteBufferSize(ctx, ctx[CpuRegister.Rdx]);
// Report a fixed working-memory footprint for the requested object. The
// out struct (SceNgs2BufferAllocator-style) begins with the size field.
private static int WriteBufferSize(CpuContext ctx, ulong outAddress)
{
if (outAddress == 0)
{
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
}
Span<byte> info = stackalloc byte[RenderBufferInfoSize];
info.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(info[0..8], 0x10000);
BinaryPrimitives.WriteUInt64LittleEndian(info[8..16], 0x100);
return ctx.Memory.TryWrite(outAddress, info)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "l4Q2dWEH6UM",
ExportName = "sceNgs2SystemSetGrainSamples",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2SystemSetGrainSamples(CpuContext ctx) => ValidateSystem(ctx);
[SysAbiExport(
Nid = "-tbc2SxQD60",
ExportName = "sceNgs2SystemSetSampleRate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2SystemSetSampleRate(CpuContext ctx) => ValidateSystem(ctx);
[SysAbiExport(
Nid = "gThZqM5PYlQ",
ExportName = "sceNgs2SystemLock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2SystemLock(CpuContext ctx) => ValidateSystem(ctx);
[SysAbiExport(
Nid = "JXRC5n0RQls",
ExportName = "sceNgs2SystemUnlock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2SystemUnlock(CpuContext ctx) => ValidateSystem(ctx);
[SysAbiExport(
Nid = "-TOuuAQ-buE",
ExportName = "sceNgs2VoiceGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetState(CpuContext ctx)
{
var voiceHandle = ctx[CpuRegister.Rdi];
var stateAddress = ctx[CpuRegister.Rsi];
var stateSize = (int)Math.Min(ctx[CpuRegister.Rdx], 0x400);
lock (StateGate)
{
if (!Voices.ContainsKey(voiceHandle))
{
return SetReturn(ctx, OrbisNgs2ErrorInvalidVoiceHandle);
}
}
// Report an idle (not-in-use) voice: all-zero state block.
if (stateAddress != 0 && stateSize > 0)
{
if (!TryClearGuestBuffer(ctx, stateAddress, (ulong)stateSize))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "rEh728kXk3w",
ExportName = "sceNgs2VoiceGetStateFlags",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetStateFlags(CpuContext ctx)
{
var voiceHandle = ctx[CpuRegister.Rdi];
var flagsAddress = ctx[CpuRegister.Rsi];
lock (StateGate)
{
if (!Voices.ContainsKey(voiceHandle))
{
return SetReturn(ctx, OrbisNgs2ErrorInvalidVoiceHandle);
}
}
// No flags set: voice is idle.
if (flagsAddress != 0 && !ctx.TryWriteUInt64(flagsAddress, 0))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetReturn(ctx, 0);
}
private static int ValidateSystem(CpuContext ctx)
{
lock (StateGate)
{
return SetReturn(
ctx,
Systems.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidSystemHandle);
}
}
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
{
handle = 0;
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
{
return false;
@@ -318,18 +397,11 @@ public static class Ngs2Exports
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
data.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle);
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
if (!ctx.Memory.TryWrite(handle, data))
{
return false;
}
// Offset 0 doubles as a vtable slot for guest code that treats this handle as a C++
// object; stamp it with the shared dummy vtable instead of a self-referential value.
KernelMemoryCompatExports.TryWriteDummyVtable(ctx, handle);
return true;
return ctx.Memory.TryWrite(handle, data);
}
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
@@ -367,4 +439,37 @@ public static class Ngs2Exports
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
"1",
StringComparison.Ordinal);
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
[SysAbiExport(
Nid = "xa8oL9dmXkM",
ExportName = "sceNgs2PanInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "1WsleK-MTkE",
ExportName = "sceNgs2GeomCalcListener",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "0lbbayqDNoE",
ExportName = "sceNgs2GeomResetSourceParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "7Lcfo8SmpsU",
ExportName = "sceNgs2GeomResetListenerParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
}
+78 -30
View File
@@ -10,7 +10,7 @@ public static class NpManagerExports
{
private const int NpTitleIdSize = 16;
private const int NpTitleSecretSize = 128;
private const int NpReachabilityStateUnavailable = 0;
private const int NpErrorInvalidArgument = unchecked((int)0x80550003);
[SysAbiExport(
Nid = "3Zl8BePTh9Y",
@@ -58,35 +58,6 @@ public static class NpManagerExports
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
}
[SysAbiExport(
Nid = "rbknaUjpqWo",
ExportName = "sceNpGetAccountIdA",
Target = Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetOnlineIdA(CpuContext ctx)
{
// The A variant takes a user ID before OnlineId.
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
}
[SysAbiExport(
Nid = "e-ZuhGEoeC4",
ExportName = "sceNpGetNpReachabilityState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetNpReachabilityState(CpuContext ctx)
{
var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return ctx.TryWriteInt32(stateAddress, NpReachabilityStateUnavailable)
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "VfRSmPmj8Q8",
ExportName = "sceNpRegisterStateCallback",
@@ -140,6 +111,77 @@ public static class NpManagerExports
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "rbknaUjpqWo",
ExportName = "sceNpGetAccountIdA",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetAccountIdA(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var accountIdAddress = ctx[CpuRegister.Rsi];
if (userId == -1 || accountIdAddress == 0)
{
return SetReturn(ctx, NpErrorInvalidArgument);
}
// The offline profile exposed by sceNpGetState is signed in. Keep the
// account query consistent with that state: Unity's PSN integration
// treats SIGNED_OUT as an exceptional state and retries it every frame.
// A stable local-only id is sufficient for titles which only use the
// value as a profile key.
Span<byte> accountId = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(accountId, 1);
return ctx.Memory.TryWrite(accountIdAddress, accountId)
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "JT+t00a3TxA",
ExportName = "sceNpGetAccountCountryA",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetAccountCountryA(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var countryAddress = ctx[CpuRegister.Rsi];
if (userId == -1 || countryAddress == 0)
{
return SetReturn(ctx, NpErrorInvalidArgument);
}
Span<byte> country = stackalloc byte[4];
country[0] = (byte)'U';
country[1] = (byte)'S';
country[2] = 0;
country[3] = 0;
return ctx.Memory.TryWrite(countryAddress, country)
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "e-ZuhGEoeC4",
ExportName = "sceNpGetNpReachabilityState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetNpReachabilityState(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var stateAddress = ctx[CpuRegister.Rsi];
if (userId == -1 || stateAddress == 0)
{
return SetReturn(ctx, NpErrorInvalidArgument);
}
Span<byte> state = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(state, 0); // Unavailable while offline.
return ctx.Memory.TryWrite(stateAddress, state)
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "Ec63y59l9tw",
ExportName = "sceNpSetNpTitleId",
@@ -166,6 +208,12 @@ public static class NpManagerExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
{
var length = 0;
+5
View File
@@ -40,6 +40,11 @@ public static class HostWindowInput
{
keyboard.KeyDown += (_, key, _) =>
{
if (key == Key.F1)
{
VideoOut.PerfOverlay.Toggle();
}
lock (Gate)
{
Pressed.Add(key);
+41
View File
@@ -0,0 +1,41 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Pad;
public static class ImeExports
{
private const int PrimaryUserId = 0x10000000;
private const int ImeErrorInvalidAddress = unchecked((int)0x80BC0001);
private const int ImeErrorInvalidUserId = unchecked((int)0x80BC0010);
private const int ImeErrorNotOpened = unchecked((int)0x80BC0005);
private static bool _keyboardOpen;
public static int ImeKeyboardOpen(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (parameterAddress == 0)
{
return SetReturn(ctx, ImeErrorInvalidAddress);
}
if (userId != PrimaryUserId)
{
return SetReturn(ctx, ImeErrorInvalidUserId);
}
_keyboardOpen = true;
return SetReturn(ctx, 0);
}
public static int ImeUpdate(CpuContext ctx) =>
SetReturn(ctx, _keyboardOpen ? 0 : ImeErrorNotOpened);
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Diagnostics;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Pad;
public static class MouseExports
{
private const int PrimaryUserId = 0x10000000;
private const int MouseDataSize = 0x28;
private const int MouseErrorInvalidArgument = unchecked((int)0x80020002);
private const int MouseErrorInvalidHandle = unchecked((int)0x80020003);
private const int MouseErrorNotInitialized = unchecked((int)0x80020005);
private const int MouseErrorAlreadyOpened = unchecked((int)0x80020008);
private static readonly bool[] OpenHandles = new bool[2];
private static bool _initialized;
public static int MouseInit(CpuContext ctx)
{
_initialized = true;
Array.Clear(OpenHandles);
return SetReturn(ctx, 0);
}
public static int MouseOpen(CpuContext ctx)
{
if (!_initialized)
{
return SetReturn(ctx, MouseErrorNotInitialized);
}
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
if (userId != PrimaryUserId || type != 0 || index is < 0 or > 1)
{
return SetReturn(ctx, MouseErrorInvalidArgument);
}
if (OpenHandles[index])
{
return SetReturn(ctx, MouseErrorAlreadyOpened);
}
OpenHandles[index] = true;
return SetReturn(ctx, index);
}
public static int MouseRead(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (dataAddress == 0 || count is < 1 or > 64)
{
return SetReturn(ctx, MouseErrorInvalidArgument);
}
if (handle is < 0 or > 1 || !OpenHandles[handle])
{
return SetReturn(ctx, MouseErrorInvalidHandle);
}
Span<byte> data = stackalloc byte[MouseDataSize];
data.Clear();
var ticks = Stopwatch.GetTimestamp();
var timestamp =
((ulong)(ticks / Stopwatch.Frequency) * 1_000_000UL) +
((ulong)(ticks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
BinaryPrimitives.WriteUInt64LittleEndian(data, timestamp);
data[0x08] = 0; // No host mouse is exposed to the guest yet.
return ctx.Memory.TryWrite(dataAddress, data)
? SetReturn(ctx, 1)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "cAnT0Rw-IwU",
ExportName = "sceMouseClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMouse")]
public static int MouseClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (handle is < 0 or > 1 || !OpenHandles[handle])
{
return SetReturn(ctx, MouseErrorInvalidHandle);
}
OpenHandles[handle] = false;
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
+5 -1
View File
@@ -14,7 +14,11 @@ public static class PadExports
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
private const int PrimaryUserId = 1000;
// Keep the pad session on the same retail user id returned by
// libSceUserService. A mismatched emulator-local id makes games pass a
// valid 0x10000000 user to scePadOpen and receive DEVICE_NOT_CONNECTED,
// leaving every later keyboard/gamepad read on an invalid handle.
private const int PrimaryUserId = 0x10000000;
private const int StandardPortType = 0;
private const int PrimaryPadHandle = 1;
private const int ControllerInformationSize = 0x1C;
+168 -129
View File
@@ -30,19 +30,15 @@ public static class SaveDataExports
private const uint MountModeCreate2 = 1u << 5;
private const int MountResultSize = 0x40;
private static readonly object _stateGate = new();
private static readonly HashSet<int> _transactionResources = [];
private static readonly HashSet<int> _preparedTransactionResources = [];
private static string? _titleId;
private static int _nextTransactionResource;
public static void ConfigureApplicationInfo(string? titleId)
{
lock (_stateGate)
{
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
_transactionResources.Clear();
_preparedTransactionResources.Clear();
_nextTransactionResource = 0;
}
}
@@ -56,15 +52,15 @@ public static class SaveDataExports
try
{
Directory.CreateDirectory(ResolveSaveDataRoot());
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
catch (IOException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
}
@@ -79,18 +75,18 @@ public static class SaveDataExports
var resultAddress = ctx[CpuRegister.Rsi];
if (condAddress == 0 || resultAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
!TryReadSearchResult(ctx, resultAddress, out var result))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
try
@@ -102,7 +98,7 @@ public static class SaveDataExports
}
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
@@ -114,21 +110,21 @@ public static class SaveDataExports
var setNum = result.DirNamesNum == 0
? 0
: Math.Min(result.DirNamesNum, entries.Count);
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (setNum == 0)
{
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
if (result.DirNamesAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
for (var i = 0; i < setNum; i++)
@@ -144,20 +140,20 @@ public static class SaveDataExports
(result.InfosAddress != 0 &&
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
catch (IOException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
}
@@ -172,25 +168,25 @@ public static class SaveDataExports
var resultAddress = ctx[CpuRegister.Rsi];
if (mountAddress == 0 || resultAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!ctx.TryReadInt32(mountAddress, out var userId) ||
if (!TryReadInt32(ctx, mountAddress, out var userId) ||
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
!ctx.TryReadUInt32(mountAddress + 0x20, out var mountMode) ||
!ctx.TryReadUInt32(mountAddress + 0x24, out var resource) ||
!ctx.TryReadUInt32(mountAddress + 0x28, out var mode) ||
!TryReadUInt32(ctx, mountAddress + 0x20, out var mountMode) ||
!TryReadUInt32(ctx, mountAddress + 0x24, out var resource) ||
!TryReadUInt32(ctx, mountAddress + 0x28, out var mode) ||
dirNameAddress == 0 ||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
try
@@ -205,12 +201,12 @@ public static class SaveDataExports
if (!existed && !create && !createIfMissing)
{
return ctx.SetReturn(OrbisSaveDataErrorNotFound);
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
}
if (existed && create)
{
return ctx.SetReturn(OrbisSaveDataErrorExists);
return SetReturn(ctx, OrbisSaveDataErrorExists);
}
if (!existed)
@@ -227,29 +223,30 @@ public static class SaveDataExports
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSaveData(
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
catch (IOException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (ArgumentException)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
}
private static int _nextTransactionResource;
[SysAbiExport(
Nid = "gjRZNnw0JPE",
ExportName = "sceSaveDataCreateTransactionResource",
@@ -257,16 +254,39 @@ public static class SaveDataExports
LibraryName = "libSceSaveData")]
public static int SaveDataCreateTransactionResource(CpuContext ctx)
{
var memorySize = ctx[CpuRegister.Rdi];
int resource;
lock (_stateGate)
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var reserved = ctx[CpuRegister.Rsi];
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
// The resource-out pointer's argument slot varies by SDK revision: some
// callers pass it in rdx, others in rcx (a 4-arg form where rdx holds a
// count/flag). Void Terrarium passes rdx=0x1 (not a pointer) and the
// real out-pointer in rcx. Probe the plausible candidates and write the
// handle to the first writable one instead of faulting on a bad rdx.
// This is a stub-level create (matches shadPS4's return-OK semantics);
// never return MEMORY_FAULT for it, or the guest treats savedata init as
// failed and never advances.
var resourceAddress = 0UL;
foreach (var candidate in new[]
{
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
})
{
resource = ++_nextTransactionResource;
_transactionResources.Add(resource);
if (candidate != 0 && TryWriteUInt32(ctx, candidate, id))
{
resourceAddress = candidate;
break;
}
}
TraceSaveData($"create_transaction_resource memory_size=0x{memorySize:X} resource={resource}");
return ctx.SetReturn(resource);
TraceSaveData(
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -279,70 +299,11 @@ public static class SaveDataExports
var resource = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{
_transactionResources.Remove(resource);
_preparedTransactionResources.Remove(resource);
}
TraceSaveData($"delete_transaction_resource resource={resource}");
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "sDCBrmc61XU",
ExportName = "sceSaveDataPrepare",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataPrepare(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rdi];
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
if (mountPointAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (string.IsNullOrWhiteSpace(mountPoint))
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
lock (_stateGate)
{
if (resource != 0)
{
_preparedTransactionResources.Add(resource);
}
}
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "ie7qhZ4X0Cc",
ExportName = "sceSaveDataCommit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataCommit(CpuContext ctx)
{
var commitAddress = ctx[CpuRegister.Rdi];
if (commitAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
lock (_stateGate)
{
_preparedTransactionResources.Clear();
}
TraceSaveData($"commit commit=0x{commitAddress:X16}");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -352,40 +313,21 @@ public static class SaveDataExports
LibraryName = "libSceSaveData")]
public static int SaveDataUmount2(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rdi];
if (mountPointAddress == 0)
{
mountPointAddress = ctx[CpuRegister.Rsi];
}
if (mountPointAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (string.IsNullOrWhiteSpace(mountPoint))
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
var unmounted = KernelMemoryCompatExports.TryUnregisterGuestPathMount(mountPoint);
TraceSaveData($"umount2 mount_point={mountPoint} unregistered={unmounted}");
return ctx.SetReturn(0);
// Unmounting a save directory always succeeds in the stub filesystem;
// returning an error here makes the game's save flow stall before it
// hands control to the title/gameplay state.
TraceSaveData($"umount2 user={unchecked((int)ctx[CpuRegister.Rdi])}");
return SetReturn(ctx, 0);
}
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
{
cond = default;
if (!ctx.TryReadInt32(address, out var userId) ||
if (!TryReadInt32(ctx, address, out var userId) ||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
!ctx.TryReadUInt32(address + 0x1C, out var sortOrder))
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
{
return false;
}
@@ -408,7 +350,7 @@ public static class SaveDataExports
{
result = default;
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
!ctx.TryReadUInt32(address + ResultDirNamesNumOffset, out var dirNamesNum) ||
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
{
@@ -609,6 +551,45 @@ public static class SaveDataExports
}
}
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSaveData(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
@@ -631,4 +612,62 @@ public static class SaveDataExports
ulong InfosAddress);
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
[SysAbiExport(
Nid = "sDCBrmc61XU",
ExportName = "sceSaveDataPrepare",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataPrepare(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rdi];
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
if (mountPointAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (string.IsNullOrWhiteSpace(mountPoint))
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
lock (_stateGate)
{
if (resource != 0)
{
_preparedTransactionResources.Add(resource);
}
}
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "ie7qhZ4X0Cc",
ExportName = "sceSaveDataCommit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataCommit(CpuContext ctx)
{
var commitAddress = ctx[CpuRegister.Rdi];
if (commitAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
lock (_stateGate)
{
_preparedTransactionResources.Clear();
}
TraceSaveData($"commit commit=0x{commitAddress:X16}");
return ctx.SetReturn(0);
}
}
@@ -0,0 +1,62 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Stubs;
/// <summary>
/// Success stubs for trophy, character-encoding and telemetry ABI calls that
/// Void Terrarium (and other titles) invoke during startup. They were
/// previously unresolved and returned NOT_FOUND, and a title that gates its
/// UI/text initialization on these succeeding then skips ahead and never draws
/// its content (a black screen with only a clear pass). These return success
/// (and a non-zero handle where an out pointer is expected) so init proceeds.
/// </summary>
public static class GameServiceStubs
{
private static int Ok(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
// Writes a small non-zero handle to the pointer in the given register so
// the caller treats the object as created; returns success.
private static int OkWithHandle(CpuContext ctx, CpuRegister outPointerRegister)
{
var outAddress = ctx[outPointerRegister];
if (outAddress != 0)
{
Span<byte> handle = stackalloc byte[sizeof(int)];
System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(handle, 1);
_ = ctx.Memory.TryWrite(outAddress, handle);
}
return Ok(ctx);
}
// ---- NpTrophy2: trophy context/handle registration at boot ----
public static int NpTrophy2CreateContext(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
public static int NpTrophy2CreateHandle(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
public static int NpTrophy2RegisterContext(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "4IzqhhUQ3nk", ExportName = "sceNpTrophy2GetGameInfo",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2GetGameInfo(CpuContext ctx) => Ok(ctx);
// ---- CES: Shift-JIS <-> Unicode conversion setup (Japanese text) ----
[SysAbiExport(Nid = "ZiDCxUUGbec", ExportName = "sceCesUcsProfileInitSJis1997Cp932",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceLibcInternal")]
public static int CesUcsProfileInitSJis1997Cp932(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "538bRGc6Zo8", ExportName = "sceCesMbcsUcsContextInit",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceLibcInternal")]
public static int CesMbcsUcsContextInit(CpuContext ctx) => Ok(ctx);
// ---- NpUniversalDataSystem: gameplay telemetry events ----
public static int NpUniversalDataSystemCreateEvent(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
public static int NpUniversalDataSystemPostEvent(CpuContext ctx) => Ok(ctx);
public static int NpUniversalDataSystemDestroyEvent(CpuContext ctx) => Ok(ctx);
}
@@ -13,6 +13,7 @@ public static class SystemServiceExports
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
private const int SystemServiceStatusSize = 0x0C;
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
private const int HdrToneMapLuminanceSize = sizeof(float) * 3;
[SysAbiExport(
Nid = "fZo48un7LK4",
@@ -42,6 +43,35 @@ public static class SystemServiceExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "SsC-m-S9JTA",
ExportName = "sceSystemServiceParamGetString",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceParamGetString(CpuContext ctx)
{
_ = unchecked((int)ctx[CpuRegister.Rdi]); // parameter id (nickname, etc.)
var bufferAddress = ctx[CpuRegister.Rsi];
var bufferSize = unchecked((int)ctx[CpuRegister.Rdx]);
if (bufferAddress == 0 || bufferSize <= 0)
{
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
}
// String params are typically the user nickname. Callers that gate UI
// or text setup on a successful read (and skip it on failure) stall on
// a black screen when this returns NOT_FOUND, so return a neutral
// non-empty default and success.
var value = System.Text.Encoding.UTF8.GetBytes("SharpEmu");
var writeLength = Math.Min(value.Length, bufferSize - 1);
Span<byte> output = stackalloc byte[writeLength + 1];
value.AsSpan(0, writeLength).CopyTo(output);
output[writeLength] = 0;
return ctx.Memory.TryWrite(bufferAddress, output)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "rPo6tV8D9bM",
ExportName = "sceSystemServiceGetStatus",
@@ -87,6 +117,28 @@ public static class SystemServiceExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "mPpPxv5CZt4",
ExportName = "sceSystemServiceGetHdrToneMapLuminance",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceGetHdrToneMapLuminance(CpuContext ctx)
{
var luminanceAddress = ctx[CpuRegister.Rdi];
if (luminanceAddress == 0)
{
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
}
Span<byte> luminance = stackalloc byte[HdrToneMapLuminanceSize];
BinaryPrimitives.WriteSingleLittleEndian(luminance, 1000.0f);
BinaryPrimitives.WriteSingleLittleEndian(luminance[sizeof(float)..], 1000.0f);
BinaryPrimitives.WriteSingleLittleEndian(luminance[(sizeof(float) * 2)..], 0.01f);
return ctx.Memory.TryWrite(luminanceAddress, luminance)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "Vo5V8KAwCmk",
ExportName = "sceSystemServiceHideSplashScreen",
@@ -4,6 +4,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Threading;
namespace SharpEmu.Libs.UserService;
@@ -13,15 +14,14 @@ public static class UserServiceExports
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
// Retail user-service IDs begin at 0x3E8.
private const int PrimaryUserId = 1000;
// Retail user ids encode their local user slot in the 0x10000000 range;
// the first signed-in user maps to slot 0. Small emulator-local ids do
// not pass Unity's user-id-to-slot conversion and become slot -1.
private const int PrimaryUserId = 0x10000000;
private const int InvalidUserId = -1;
private const string PrimaryUserName = "SharpEmu";
private static int _loginEventDelivered;
private static readonly bool _traceUserService =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"), "1", StringComparison.Ordinal);
[SysAbiExport(
Nid = "j3YMu1MVNNo",
ExportName = "sceUserServiceInitialize",
@@ -29,6 +29,7 @@ public static class UserServiceExports
LibraryName = "libSceUserService")]
public static int UserServiceInitialize(CpuContext ctx)
{
Trace("initialize");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -43,14 +44,12 @@ public static class UserServiceExports
var userIdAddress = ctx[CpuRegister.Rdi];
if (userIdAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
var result = ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
TraceUserService($"get_initial_user out=0x{userIdAddress:X16} value={PrimaryUserId} result=0x{result:X8}");
return ctx.SetReturn(result);
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
? SetReturnWithTrace(ctx, 0, $"get_initial_user user={PrimaryUserId} out=0x{userIdAddress:X16}")
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -63,7 +62,7 @@ public static class UserServiceExports
var userIdListAddress = ctx[CpuRegister.Rdi];
if (userIdListAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
@@ -72,8 +71,12 @@ public static class UserServiceExports
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
return ctx.Memory.TryWrite(userIdListAddress, userIds)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturnWithTrace(
ctx,
0,
$"get_login_user_id_list users=[{PrimaryUserId},{InvalidUserId},{InvalidUserId},{InvalidUserId}] " +
$"out=0x{userIdListAddress:X16}")
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -86,20 +89,23 @@ public static class UserServiceExports
var eventAddress = ctx[CpuRegister.Rdi];
if (eventAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
{
return ctx.SetReturn(OrbisUserServiceErrorNoEvent);
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
}
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
return ctx.Memory.TryWrite(eventAddress, payload)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturnWithTrace(
ctx,
0,
$"get_event type=login user={PrimaryUserId} out=0x{eventAddress:X16}")
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -112,53 +118,30 @@ public static class UserServiceExports
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var nameAddress = ctx[CpuRegister.Rsi];
var capacity = ctx[CpuRegister.Rdx];
// Zero selects the current user.
if (userId != 0 && userId != PrimaryUserId)
if (userId != PrimaryUserId)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (nameAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
if (capacity <= (ulong)nameBytes.Length)
{
return ctx.SetReturn(OrbisUserServiceErrorBufferTooShort);
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
}
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
nameBytes.CopyTo(output);
var result = ctx.Memory.TryWrite(nameAddress, output)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
TraceUserService(
$"get_user_name user={userId} out=0x{nameAddress:X16} capacity=0x{capacity:X} value='{PrimaryUserName}' result=0x{result:X8}");
return ctx.SetReturn(result);
}
[SysAbiExport(
Nid = "-sD02mFDBh4",
ExportName = "sceUserServiceGetGamePresets",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetGamePresets(CpuContext ctx)
{
// Return deterministic defaults for the offline profile.
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var resultAddress = ctx[CpuRegister.Rcx];
Span<byte> defaults = stackalloc byte[0x18];
if (resultAddress == 0 || !ctx.Memory.TryWrite(resultAddress, defaults))
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
}
TraceUserService(
$"get_game_presets user={userId} title=0x{ctx[CpuRegister.Rsi]:X16} " +
$"key=0x{ctx[CpuRegister.R9]:X} out=0x{resultAddress:X16} result=0x00000000");
return ctx.SetReturn(0);
return ctx.Memory.TryWrite(nameAddress, output)
? SetReturnWithTrace(
ctx,
0,
$"get_user_name user={userId} name='{PrimaryUserName}' out=0x{nameAddress:X16}")
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
@@ -174,50 +157,165 @@ public static class UserServiceExports
var valueAddress = ctx[CpuRegister.Rsi];
if (parameterId != 1000)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (valueAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
return ctx.TryWriteInt32(valueAddress, 0)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return TryWriteInt32(ctx, valueAddress, 0)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
#pragma warning restore SHEM006
[SysAbiExport(
Nid = "woNpu+45RLk",
ExportName = "sceUserServiceGetAgeLevel",
Target = Generation.Gen4 | Generation.Gen5,
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetAgeLevel(CpuContext ctx)
public static int UserServiceGetAgeLevel(CpuContext ctx) =>
WriteUserSettingInt32(ctx, 18, "get_age_level");
[SysAbiExport(
Nid = "-sD02mFDBh4",
ExportName = "sceUserServiceGetGamePresets",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetGamePresets(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var ageLevelAddress = ctx[CpuRegister.Rsi];
if (userId != 1000)
var presetsAddress = ctx[CpuRegister.Rsi];
if (userId != PrimaryUserId)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (ageLevelAddress == 0)
if (presetsAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
// Report an adult account so titles skip parental-restriction paths.
return ctx.TryWriteInt32(ageLevelAddress, 21)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
Span<byte> presets = stackalloc byte[0x28];
presets.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(presets, (ulong)presets.Length);
if (!ctx.Memory.TryWrite(presetsAddress, presets))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetReturnWithTrace(
ctx,
0,
$"get_game_presets user={userId} out=0x{presetsAddress:X16}");
}
private static void TraceUserService(string message)
[SysAbiExport(
Nid = "rnEhHqG-4xo",
ExportName = "sceUserServiceGetAccessibilityChatTranscription",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetAccessibilityChatTranscription(CpuContext ctx) =>
WriteUserSettingInt32(ctx, 0, "get_accessibility_chat_transcription");
[SysAbiExport(
Nid = "ZKJtxdgvzwg",
ExportName = "sceUserServiceGetAccessibilityPressAndHoldDelay",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetAccessibilityPressAndHoldDelay(CpuContext ctx) =>
WriteUserSettingInt32(ctx, 0, "get_accessibility_press_and_hold_delay");
[SysAbiExport(
Nid = "-3Y5GO+-i78",
ExportName = "sceUserServiceGetAccessibilityTriggerEffect",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetAccessibilityTriggerEffect(CpuContext ctx) =>
WriteUserSettingInt32(ctx, 0, "get_accessibility_trigger_effect");
[SysAbiExport(
Nid = "qWYHOFwqCxY",
ExportName = "sceUserServiceGetAccessibilityVibration",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetAccessibilityVibration(CpuContext ctx) =>
WriteUserSettingInt32(ctx, 1, "get_accessibility_vibration");
[SysAbiExport(
Nid = "hD-H81EN9Vg",
ExportName = "sceUserServiceGetAccessibilityZoomEnabled",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetAccessibilityZoomEnabled(CpuContext ctx) =>
WriteUserSettingInt32(ctx, 0, "get_accessibility_zoom_enabled");
[SysAbiExport(
Nid = "O6IW1-Dwm-w",
ExportName = "sceUserServiceGetAccessibilityZoomFollowFocus",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetAccessibilityZoomFollowFocus(CpuContext ctx) =>
WriteUserSettingInt32(ctx, 0, "get_accessibility_zoom_follow_focus");
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
if (_traceUserService)
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int WriteUserSettingInt32(CpuContext ctx, int value, string operation)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var valueAddress = ctx[CpuRegister.Rsi];
if (userId != PrimaryUserId)
{
Console.Error.WriteLine($"[LOADER][TRACE] user_service.{message}");
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (valueAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
if (!TryWriteInt32(ctx, valueAddress, value))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetReturnWithTrace(
ctx,
0,
$"{operation} user={userId} value={value} out=0x{valueAddress:X16}");
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static int SetReturnWithTrace(CpuContext ctx, int result, string message)
{
Trace(message);
return SetReturn(ctx, result);
}
private static void Trace(string message)
{
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"),
"1",
StringComparison.Ordinal))
{
var returnRip = GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
? frame.ReturnRip
: 0;
Console.Error.WriteLine(
$"[LOADER][TRACE] user_service.{message} ret=0x{returnRip:X16}");
}
}
}
+363
View File
@@ -0,0 +1,363 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.Libs.VideoOut;
/// <summary>
/// In-window performance HUD. The panel is rasterized on the CPU into a
/// small BGRA buffer each frame (embedded 5x7 font, no assets) and the
/// presenter blits it onto the swapchain image, so it needs no pipelines,
/// descriptors or blending state. Toggled with F1; SHARPEMU_OVERLAY=0
/// starts it hidden.
/// </summary>
public static class PerfOverlay
{
public const int PanelWidth = 376;
public const int PanelHeight = 176;
private const int GlyphColumns = 5;
private const int GlyphRows = 7;
private const int Scale = 2;
private const int CellWidth = (GlyphColumns + 1) * Scale;
private const int LineHeight = (GlyphRows + 2) * Scale;
private const int FrameHistorySize = 128;
private static volatile bool _enabled = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_OVERLAY"),
"0",
StringComparison.Ordinal);
private static long _lastPresentTimestamp;
private static readonly double[] _frameMilliseconds = new double[FrameHistorySize];
private static int _frameHistoryIndex;
private static long _presentedInWindow;
private static long _submittedInWindow;
private static long _drawsInWindow;
// Refreshed once per second so per-frame fills never allocate.
private static long _statsWindowStart = Stopwatch.GetTimestamp();
private static double _fps;
private static double _submittedFps;
private static double _drawsPerSecond;
private static double _averageFrameMs;
private static double _allocatedMbPerSecond;
private static long _lastAllocatedBytes = GC.GetTotalAllocatedBytes(precise: false);
private static int _lastGen0 = GC.CollectionCount(0);
private static int _lastGen1 = GC.CollectionCount(1);
private static int _lastGen2 = GC.CollectionCount(2);
private static int _gen0PerWindow;
private static int _gen1PerWindow;
private static int _gen2PerWindow;
private static double _cpuPercent;
private static TimeSpan _lastCpuTime = GetProcessCpuTime();
private static string _line1 = string.Empty;
private static string _line2 = string.Empty;
private static string _line3 = string.Empty;
private static string _line4 = string.Empty;
public static bool Enabled => _enabled;
public static void Toggle() => _enabled = !_enabled;
/// <summary>Called by the presenter after each successful present.</summary>
public static void RecordPresent()
{
var now = Stopwatch.GetTimestamp();
var last = _lastPresentTimestamp;
_lastPresentTimestamp = now;
Interlocked.Increment(ref _presentedInWindow);
if (last != 0)
{
var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
if (milliseconds < 1000.0)
{
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
}
}
}
/// <summary>Called on every guest flip submission.</summary>
public static void RecordSubmit() => Interlocked.Increment(ref _submittedInWindow);
/// <summary>Called per translated draw/dispatch executed.</summary>
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
/// <summary>
/// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight.
/// Runs on the render thread.
/// </summary>
public static void Fill(Span<byte> bgra, int pendingWork, int inFlightSubmissions)
{
RefreshStatsIfDue(pendingWork, inFlightSubmissions);
// Background: opaque dark slate.
for (var i = 0; i < bgra.Length; i += 4)
{
bgra[i] = 0x20;
bgra[i + 1] = 0x18;
bgra[i + 2] = 0x14;
bgra[i + 3] = 0xFF;
}
var y = 6;
DrawString(bgra, 8, y, _line1, 0x60, 0xFF, 0x60);
y += LineHeight;
DrawString(bgra, 8, y, _line2, 0xFF, 0xFF, 0xFF);
y += LineHeight;
DrawString(bgra, 8, y, _line3, 0xB0, 0xD0, 0xFF);
y += LineHeight;
DrawString(bgra, 8, y, _line4, 0xB0, 0xB0, 0xB0);
y += LineHeight + 4;
DrawFrameGraph(bgra, 8, y, PanelWidth - 16, PanelHeight - y - 6);
}
private static void RefreshStatsIfDue(int pendingWork, int inFlightSubmissions)
{
var now = Stopwatch.GetTimestamp();
var elapsedTicks = now - _statsWindowStart;
if (elapsedTicks >= Stopwatch.Frequency)
{
var seconds = (double)elapsedTicks / Stopwatch.Frequency;
_statsWindowStart = now;
_fps = Interlocked.Exchange(ref _presentedInWindow, 0) / seconds;
_submittedFps = Interlocked.Exchange(ref _submittedInWindow, 0) / seconds;
_drawsPerSecond = Interlocked.Exchange(ref _drawsInWindow, 0) / seconds;
double totalMs = 0;
var samples = 0;
foreach (var ms in _frameMilliseconds)
{
if (ms > 0)
{
totalMs += ms;
samples++;
}
}
_averageFrameMs = samples > 0 ? totalMs / samples : 0;
var allocated = GC.GetTotalAllocatedBytes(precise: false);
_allocatedMbPerSecond = (allocated - _lastAllocatedBytes) / seconds / (1024.0 * 1024.0);
_lastAllocatedBytes = allocated;
var gen0 = GC.CollectionCount(0);
var gen1 = GC.CollectionCount(1);
var gen2 = GC.CollectionCount(2);
_gen0PerWindow = gen0 - _lastGen0;
_gen1PerWindow = gen1 - _lastGen1;
_gen2PerWindow = gen2 - _lastGen2;
_lastGen0 = gen0;
_lastGen1 = gen1;
_lastGen2 = gen2;
var cpuTime = GetProcessCpuTime();
_cpuPercent = (cpuTime - _lastCpuTime).TotalSeconds / seconds * 100.0;
_lastCpuTime = cpuTime;
var drawsPerFrame = _fps > 0.5 ? _drawsPerSecond / _fps : 0;
_line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS";
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
_line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE";
}
}
private static TimeSpan GetProcessCpuTime()
{
var usage = Environment.CpuUsage;
return usage.UserTime + usage.PrivilegedTime;
}
private static void DrawFrameGraph(Span<byte> bgra, int x, int y, int width, int height)
{
if (height < 8)
{
return;
}
// Reference lines at 16.7ms and 33.3ms of a 40ms-tall scale.
const double maxMs = 40.0;
DrawHorizontalLine(bgra, x, y + height - (int)(16.7 / maxMs * height), width, 0x30, 0x60, 0x30);
DrawHorizontalLine(bgra, x, y + height - (int)(33.3 / maxMs * height), width, 0x30, 0x30, 0x60);
var barWidth = Math.Max(1, width / FrameHistorySize);
for (var i = 0; i < FrameHistorySize; i++)
{
var ms = _frameMilliseconds[(_frameHistoryIndex + i) % FrameHistorySize];
if (ms <= 0)
{
continue;
}
var barHeight = (int)Math.Min(height, ms / maxMs * height);
var barX = x + i * barWidth;
if (barX + barWidth > x + width)
{
break;
}
byte r, g, b;
if (ms <= 17.5)
{
(r, g, b) = ((byte)0x40, (byte)0xE0, (byte)0x40);
}
else if (ms <= 34.0)
{
(r, g, b) = ((byte)0xE0, (byte)0xC0, (byte)0x30);
}
else
{
(r, g, b) = ((byte)0xE8, (byte)0x40, (byte)0x40);
}
for (var py = 0; py < barHeight; py++)
{
var rowY = y + height - 1 - py;
for (var px = 0; px < barWidth; px++)
{
SetPixel(bgra, barX + px, rowY, r, g, b);
}
}
}
}
private static void DrawHorizontalLine(Span<byte> bgra, int x, int y, int width, byte r, byte g, byte b)
{
if (y < 0 || y >= PanelHeight)
{
return;
}
for (var px = 0; px < width; px++)
{
SetPixel(bgra, x + px, y, r, g, b);
}
}
private static void DrawString(Span<byte> bgra, int x, int y, string text, byte r, byte g, byte b)
{
var penX = x;
foreach (var rawChar in text)
{
var c = char.ToUpperInvariant(rawChar);
if (c < ' ' || c > 'Z')
{
c = '?';
}
var glyph = Font.Slice((c - ' ') * GlyphColumns, GlyphColumns);
for (var column = 0; column < GlyphColumns; column++)
{
var bits = glyph[column];
for (var row = 0; row < GlyphRows; row++)
{
if ((bits & (1 << row)) == 0)
{
continue;
}
for (var sy = 0; sy < Scale; sy++)
{
for (var sx = 0; sx < Scale; sx++)
{
SetPixel(
bgra,
penX + column * Scale + sx,
y + row * Scale + sy,
r,
g,
b);
}
}
}
}
penX += CellWidth;
if (penX + CellWidth > PanelWidth)
{
break;
}
}
}
private static void SetPixel(Span<byte> bgra, int x, int y, byte r, byte g, byte b)
{
if ((uint)x >= PanelWidth || (uint)y >= PanelHeight)
{
return;
}
var offset = (y * PanelWidth + x) * 4;
bgra[offset] = b;
bgra[offset + 1] = g;
bgra[offset + 2] = r;
bgra[offset + 3] = 0xFF;
}
// Classic 5x7 column-encoded font (bit 0 = top row), ASCII 32..90.
private static ReadOnlySpan<byte> Font =>
[
0x00, 0x00, 0x00, 0x00, 0x00, // ' '
0x00, 0x00, 0x5F, 0x00, 0x00, // '!'
0x00, 0x07, 0x00, 0x07, 0x00, // '"'
0x14, 0x7F, 0x14, 0x7F, 0x14, // '#'
0x24, 0x2A, 0x7F, 0x2A, 0x12, // '$'
0x23, 0x13, 0x08, 0x64, 0x62, // '%'
0x36, 0x49, 0x55, 0x22, 0x50, // '&'
0x00, 0x05, 0x03, 0x00, 0x00, // '''
0x00, 0x1C, 0x22, 0x41, 0x00, // '('
0x00, 0x41, 0x22, 0x1C, 0x00, // ')'
0x08, 0x2A, 0x1C, 0x2A, 0x08, // '*'
0x08, 0x08, 0x3E, 0x08, 0x08, // '+'
0x00, 0x50, 0x30, 0x00, 0x00, // ','
0x08, 0x08, 0x08, 0x08, 0x08, // '-'
0x00, 0x60, 0x60, 0x00, 0x00, // '.'
0x20, 0x10, 0x08, 0x04, 0x02, // '/'
0x3E, 0x51, 0x49, 0x45, 0x3E, // '0'
0x00, 0x42, 0x7F, 0x40, 0x00, // '1'
0x42, 0x61, 0x51, 0x49, 0x46, // '2'
0x21, 0x41, 0x45, 0x4B, 0x31, // '3'
0x18, 0x14, 0x12, 0x7F, 0x10, // '4'
0x27, 0x45, 0x45, 0x45, 0x39, // '5'
0x3C, 0x4A, 0x49, 0x49, 0x30, // '6'
0x01, 0x71, 0x09, 0x05, 0x03, // '7'
0x36, 0x49, 0x49, 0x49, 0x36, // '8'
0x06, 0x49, 0x49, 0x29, 0x1E, // '9'
0x00, 0x36, 0x36, 0x00, 0x00, // ':'
0x00, 0x56, 0x36, 0x00, 0x00, // ';'
0x00, 0x08, 0x14, 0x22, 0x41, // '<'
0x14, 0x14, 0x14, 0x14, 0x14, // '='
0x41, 0x22, 0x14, 0x08, 0x00, // '>'
0x02, 0x01, 0x51, 0x09, 0x06, // '?'
0x32, 0x49, 0x79, 0x41, 0x3E, // '@'
0x7E, 0x11, 0x11, 0x11, 0x7E, // 'A'
0x7F, 0x49, 0x49, 0x49, 0x36, // 'B'
0x3E, 0x41, 0x41, 0x41, 0x22, // 'C'
0x7F, 0x41, 0x41, 0x22, 0x1C, // 'D'
0x7F, 0x49, 0x49, 0x49, 0x41, // 'E'
0x7F, 0x09, 0x09, 0x09, 0x01, // 'F'
0x3E, 0x41, 0x49, 0x49, 0x7A, // 'G'
0x7F, 0x08, 0x08, 0x08, 0x7F, // 'H'
0x00, 0x41, 0x7F, 0x41, 0x00, // 'I'
0x20, 0x40, 0x41, 0x3F, 0x01, // 'J'
0x7F, 0x08, 0x14, 0x22, 0x41, // 'K'
0x7F, 0x40, 0x40, 0x40, 0x40, // 'L'
0x7F, 0x02, 0x0C, 0x02, 0x7F, // 'M'
0x7F, 0x04, 0x08, 0x10, 0x7F, // 'N'
0x3E, 0x41, 0x41, 0x41, 0x3E, // 'O'
0x7F, 0x09, 0x09, 0x09, 0x06, // 'P'
0x3E, 0x41, 0x51, 0x21, 0x5E, // 'Q'
0x7F, 0x09, 0x19, 0x29, 0x46, // 'R'
0x46, 0x49, 0x49, 0x49, 0x31, // 'S'
0x01, 0x01, 0x7F, 0x01, 0x01, // 'T'
0x3F, 0x40, 0x40, 0x40, 0x3F, // 'U'
0x1F, 0x20, 0x40, 0x20, 0x1F, // 'V'
0x3F, 0x40, 0x38, 0x40, 0x3F, // 'W'
0x63, 0x14, 0x08, 0x14, 0x63, // 'X'
0x07, 0x08, 0x70, 0x08, 0x07, // 'Y'
0x61, 0x51, 0x49, 0x45, 0x43, // 'Z'
];
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff