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