[AGC] Quake rendering progress: WAIT_REG_MEM, draw fixes, VideoOut, and HLE improvements (#68)

* [agc] WAIT_REG_MEM suspend/resume, draw packet fixes, new HLE exports, debug cleanup

Rebased onto upstream 79a7437 (par274/sharpemu, rewritten history).

- GpuWaitRegistry: DCBs suspended on unsatisfied WAIT_REG_MEM are re-polled
  against guest memory on every submit; fixed 64-bit and standard packet parse
  offsets, apply the mask, treat PM4 compare function 0 as "always".
- TryReadSubmittedDrawCount: accept the 5-dword ItDrawIndex2 form emitted by
  DcbDrawIndex (count at +4); menu draws were silently discarded before.
- sceAgcDriverSubmitMultiDcbs: reversed ABI (rdi=address array, rsi=dword
  sizes, rdx=count).
- VideoOut: vblank events, sceVideoOutGetFlipStatus, buffers registered via
  sceVideoOutRegisterBuffers are valid flip targets.
- New HLE: libc stdio (fopen/fread/fseek/ftell/fclose/fgets), Dinkumware
  _Getpctype ctype table, NpTrophy2 stubs, AMPR PAK sequential-read tracker,
  MsgDialog lifecycle, NGS2 alt NIDs + dummy vtable for handle objects,
  guarded memset intrinsic, abort()/strcasecmp null-arg recovery.
- Removed investigation-only code (INT3 breakpoints, qfont/mcpp dumps,
  error-candidate printf traces, unconditional debug logs).

First rendered frame: Quake (PPSA01880) presents a 1920x1080 guest frame.

* Implemented a guarded native intrinsic (rep movsb) in DirectExecutionBackend to bypass HLE dispatch overhead, while preserving memory safety checks.
This commit is contained in:
Foued Attar
2026-07-11 23:22:48 +02:00
committed by GitHub
parent de4fc1e1a8
commit e1cf5b13ef
21 changed files with 2204 additions and 191 deletions
@@ -860,7 +860,12 @@ public sealed partial class DirectExecutionBackend
"vz+pg2zdopI" or // sceKernelGetEventUserData
"mJ7aghmgvfc" or // sceKernelGetEventId
"23CPPI1tyBY" or // sceKernelGetEventFilter
"kwGyyjohI50"; // sceKernelGetEventData
"kwGyyjohI50" or // sceKernelGetEventData
// _Getpctype is a per-character ctype table lookup; the embedded mcpp preprocessor
// re-fetches the table pointer on every isdigit()/isalpha() check, so classifying a
// large input legitimately calls it hundreds of thousands of times back-to-back -
// real, finite work that would otherwise trip the loop guard.
"sUP1hBaouOw"; // _Getpctype
}
private long NextImportDispatchIndex()
@@ -1122,21 +1122,72 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
0x75, 0xF5,
0xC3,
],
// memcpy: guarded native copy. The first "rep movsb" intrinsic had no bounds/null
// checking and crashed with a read at -1 right after a null-dst memset recovery in
// the NGS2 audio streaming code path, so it was temporarily pulled in favor of the
// safe C# HLE memcpy. That detour costs a full import dispatch per call - far too
// slow for a function this hot - so this stub keeps the native leaf path and adds
// the same guards as memset below: it silently returns dst without copying when dst
// or src is null/low-page (< 0x10000) or outside canonical user space, or when len
// is 0 or absurd (> 512MB).
"Q3VBxCXhUHs" =>
[
0x48, 0x89, 0xF8,
0x48, 0x89, 0xD1,
0xF3, 0xA4,
0xC3,
0x48, 0x89, 0xF8, // mov rax, rdi (return dst)
0x48, 0x81, 0xFF, 0x00, 0x00, 0x01, 0x00, // cmp rdi, 0x10000
0x72, 0x31, // jb done
0x49, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // mov r8, 0x800000000000
0x4C, 0x39, 0xC7, // cmp rdi, r8
0x73, 0x22, // jae done
0x48, 0x81, 0xFE, 0x00, 0x00, 0x01, 0x00, // cmp rsi, 0x10000
0x72, 0x19, // jb done
0x4C, 0x39, 0xC6, // cmp rsi, r8
0x73, 0x14, // jae done
0x48, 0x81, 0xFA, 0x00, 0x00, 0x00, 0x20, // cmp rdx, 0x20000000
0x77, 0x0B, // ja done
0x48, 0x85, 0xD2, // test rdx, rdx
0x74, 0x06, // jz done
0x48, 0x89, 0xD1, // mov rcx, rdx
0xFC, // cld
0xF3, 0xA4, // rep movsb
0xC3, // done: ret
],
// memset: guarded native fill. An earlier unguarded version crashed with a write AV
// at address 0 (NGS2 audio streaming init memsets a never-populated buffer field),
// so this one mirrors the HLE guards and silently returns dst without writing when
// dst is null/low-page (< 0x10000), dst is outside canonical user space, or len is
// absurd (> 512MB, e.g. the 0x27060035 / sign-extended values NGS2 passes). Routing
// memset through the HLE trampoline instead is not viable: parse/streaming loops
// issue hundreds of thousands of small memsets back-to-back, which crawls at
// dispatch speed and looks like a repeating-import hang to the loop guard.
// _sigprocmask: the HLE handler (KernelRuntimeCompatExports.Sigprocmask) is a pure
// no-op returning 0 that never writes oldset, so this is behavior-identical. The
// game's bundled libc queries the mask (set=NULL) once per iteration in its font/
// parse loops - hundreds of thousands of back-to-back calls that both crawl at
// dispatch speed and read as a repeating-import hang to the loop guard.
"6xVpy0Fdq+I" =>
[
0x31, 0xC0, // xor eax, eax
0xC3, // ret
],
"8zTFvBIAIN8" =>
[
0x49, 0x89, 0xF8,
0x48, 0x89, 0xF0,
0x48, 0x89, 0xD1,
0xF3, 0xAA,
0x4C, 0x89, 0xC0,
0xC3,
0x48, 0x89, 0xF8, // mov rax, rdi (return dst)
0x48, 0x81, 0xFF, 0x00, 0x00, 0x01, 0x00, // cmp rdi, 0x10000
0x72, 0x2B, // jb done
0x49, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // mov r8, 0x800000000000
0x4C, 0x39, 0xC7, // cmp rdi, r8
0x73, 0x1C, // jae done
0x48, 0x81, 0xFA, 0x00, 0x00, 0x00, 0x20, // cmp rdx, 0x20000000
0x77, 0x13, // ja done
0x48, 0x85, 0xD2, // test rdx, rdx
0x74, 0x0E, // jz done
0x48, 0x89, 0xD1, // mov rcx, rdx
0x49, 0x89, 0xF9, // mov r9, rdi
0x89, 0xF0, // mov eax, esi
0xFC, // cld
0xF3, 0xAA, // rep stosb
0x4C, 0x89, 0xC8, // mov rax, r9
0xC3, // done: ret
],
_ => default,
};
@@ -1355,10 +1406,25 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
return exportName switch
{
"memcpy" or
"memmove" or
"memset" or
"memcmp" => true,
// memset/memcpy excluded: the raw LLE routines have no null/bounds guard and crash
// with an access violation on bad pointers (observed hit during Quake's CL_Init,
// where a still-unidentified upstream bug calls memcpy/memset with a null
// destination). Both are instead served by the guarded native intrinsics in
// TryCreateNativeImportIntrinsic, which fail safely without leaving the leaf path.
"memcmp" or
// _Getpctype must come from the game's own Dinkumware libc when one is bundled:
// it returns a pointer to that CRT's ctype bitmask table, whose bit layout
// (_DI=0x20, _SP=0x04, _BB=0x80, ...) differs from the MSVC-style table the HLE
// fallback used to serve. Serving the wrong layout made the bundled printf engine
// render every Sys_Error message as an empty string (isdigit misfired during
// %-directive parsing) and made mcpp drop 'a'-'f' from identifiers ("texture" ->
// "txtur", the 0x80 bit reads as _BB/control there). _Getptolower/_Getptoupper
// already resolve to the bundled module because no HLE export shadows them; this
// keeps _Getpctype consistent with them. It is a pure accessor returning a pointer
// to a const table, so it is also the cheapest possible LLE call - important
// because parsers hit it once per input character.
"_Getpctype" => true,
_ => false,
};
}
+278 -106
View File
@@ -179,7 +179,6 @@ public static class AgcExports
private static long _createShaderTraceCount;
private static long _packetPayloadTraceCount;
private static bool _tracedMissingPixelShaderBindings;
private static long _unsatisfiedWaitTraceCount;
private static long _shaderTranslationMissTraceCount;
private static long _translatedDrawTraceCount;
private static long _standardDmaTraceCount;
@@ -2001,6 +2000,57 @@ public static class AgcExports
lock (gpuState.Gate)
{
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ABI (reversed from Quake): rdi = array of DCB base addresses (u64 each),
// rsi = array of DCB sizes in dwords (u32 each), rdx = buffer count.
[SysAbiExport(
Nid = "6UzEidRZwkg",
ExportName = "sceAgcDriverSubmitMultiDcbs",
Target = Generation.Gen5,
LibraryName = "libSceAgcDriver")]
public static int DriverSubmitMultiDcbs(CpuContext ctx)
{
var addressArray = ctx[CpuRegister.Rdi];
var sizeArray = ctx[CpuRegister.Rsi];
var bufferCount = (uint)ctx[CpuRegister.Rdx];
if (addressArray == 0 || sizeArray == 0 || bufferCount == 0 || bufferCount > 4096)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var tracePackets = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal);
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (gpuState.Gate)
{
for (uint i = 0; i < bufferCount; i++)
{
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
commandAddress == 0 ||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
dwordCount == 0)
{
continue;
}
if (tracePackets)
{
TraceAgc(
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
}
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
}
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
@@ -2049,6 +2099,7 @@ public static class AgcExports
}
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
@@ -2170,6 +2221,63 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress);
}
// WAIT_REG_MEM packets whose condition is not met suspend their DCB into
// GpuWaitRegistry. Each submit re-checks every suspended DCB against current guest
// memory (labels are advanced by ReleaseMem/WriteData/DmaData packets or by direct
// CPU writes) and resumes the ones whose condition is now satisfied. A resumed DCB
// can itself write labels that unblock others, so loop until a fixed point.
private static void DrainResumableDcbs(
CpuContext ctx,
SubmittedGpuState gpuState,
bool tracePackets)
{
for (var pass = 0; pass < 256; pass++)
{
var woken = GpuWaitRegistry.CollectSatisfied((address, is64Bit) =>
{
if (is64Bit)
{
return ctx.TryReadUInt64(address, out var value64)
? value64
: (ulong?)null;
}
return ctx.TryReadUInt32(address, out var value32)
? value32
: (ulong?)null;
});
if (woken is null)
{
return;
}
foreach (var waiter in woken)
{
var remainingDwords = waiter.TotalDwords - waiter.ResumeOffset;
if (remainingDwords == 0)
{
continue;
}
if (tracePackets)
{
TraceAgc(
$"agc.dcb.resumed addr=0x{waiter.WaitAddress:X16} " +
$"resume=0x{waiter.ResumeAddress:X16} dwords={remainingDwords}");
}
var state = waiter.State as SubmittedDcbState ?? gpuState.Graphics;
ParseSubmittedDcb(
ctx,
gpuState,
state,
waiter.ResumeAddress,
remainingDwords,
tracePackets);
}
}
}
private static void ParseSubmittedDcb(
CpuContext ctx,
SubmittedGpuState gpuState,
@@ -2201,7 +2309,6 @@ public static class AgcExports
$"agc.dcb.packet dw={offset} addr=0x{currentAddress:X16} " +
$"header=0x{header:X8} len=1 type=2");
}
offset++;
continue;
}
@@ -2311,16 +2418,91 @@ public static class AgcExports
state.InstanceCount = Math.Max(instanceCount, 1);
}
if (op == ItNop &&
register is RWaitMem32 or RWaitMem64 &&
// WAIT_REG_MEM (AGC NOP-encapsulated 32/64-bit variants): when the condition is
// not yet satisfied, suspend this DCB; DrainResumableDcbs resumes it once the
// watched memory advances.
if (op == ItNop && register is RWaitMem32 or RWaitMem64 &&
length >= (register == RWaitMem32 ? 6u : 9u))
{
ObserveSubmittedWaitRegMem(ctx, currentAddress, register == RWaitMem64, tracePackets);
if (TryParseWaitRegMem(ctx, currentAddress, register == RWaitMem64,
out var waitAddr, out var refVal, out var waitMask, out var cmpFunc))
{
ulong curVal = 0;
bool hasCurVal;
if (register == RWaitMem64)
{
hasCurVal = ctx.TryReadUInt64(waitAddr, out curVal);
}
else if (ctx.TryReadUInt32(waitAddr, out var curVal32))
{
curVal = curVal32;
hasCurVal = true;
}
else
{
hasCurVal = false;
}
var waiter = new GpuWaitRegistry.WaitingDcb
{
CommandBufferAddress = commandAddress,
ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)),
TotalDwords = dwordCount,
ResumeOffset = offset + length,
ReferenceValue = refVal,
Mask = waitMask,
CompareFunction = cmpFunc,
Is64Bit = register == RWaitMem64,
State = state,
};
if (hasCurVal && !GpuWaitRegistry.Compare(waiter, curVal))
{
GpuWaitRegistry.Register(waitAddr, waiter);
if (tracePackets)
{
TraceAgc(
$"agc.dcb.suspended addr=0x{waitAddr:X16} ref=0x{refVal:X16} " +
$"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}");
}
return; // suspend parsing of this DCB
}
}
}
if (op == ItWaitRegMem && length >= 7)
{
ObserveSubmittedStandardWaitRegMem(ctx, currentAddress, tracePackets);
if (TryParseStandardWaitRegMem(ctx, currentAddress,
out var waitAddr, out var refVal, out var waitMask, out var cmpFunc) &&
ctx.TryReadUInt32(waitAddr, out var curVal))
{
var waiter = new GpuWaitRegistry.WaitingDcb
{
CommandBufferAddress = commandAddress,
ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)),
TotalDwords = dwordCount,
ResumeOffset = offset + length,
ReferenceValue = refVal,
Mask = waitMask,
CompareFunction = cmpFunc,
Is64Bit = false,
State = state,
};
if (!GpuWaitRegistry.Compare(waiter, curVal))
{
GpuWaitRegistry.Register(waitAddr, waiter);
if (tracePackets)
{
TraceAgc(
$"agc.dcb.suspended_std addr=0x{waitAddr:X16} ref=0x{refVal:X16} " +
$"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}");
}
return;
}
}
}
if (TryReadSubmittedDrawCount(
@@ -2641,104 +2823,6 @@ public static class AgcExports
}
}
private static void ObserveSubmittedWaitRegMem(
CpuContext ctx,
ulong packetAddress,
bool is64Bit,
bool tracePacket)
{
if (!ctx.TryReadUInt64(packetAddress + 4, out var address) ||
!ctx.TryReadUInt32(packetAddress + (is64Bit ? 28u : 16u), out var control))
{
return;
}
ulong mask;
ulong reference;
ulong value;
if (is64Bit)
{
if (!ctx.TryReadUInt64(packetAddress + 12, out mask) ||
!ctx.TryReadUInt64(packetAddress + 20, out reference) ||
!ctx.TryReadUInt64(address, out value))
{
return;
}
}
else
{
if (!ctx.TryReadUInt32(packetAddress + 12, out var mask32) ||
!ctx.TryReadUInt32(packetAddress + 20, out var reference32) ||
!ctx.TryReadUInt32(address, out var value32))
{
return;
}
mask = mask32;
reference = reference32;
value = value32;
}
var compareFunction = control & 0xFFu;
TraceSubmittedWait(
address,
value,
mask,
reference,
compareFunction,
is64Bit ? 64 : 32,
tracePacket);
}
private static void ObserveSubmittedStandardWaitRegMem(
CpuContext ctx,
ulong packetAddress,
bool tracePacket)
{
if (!ctx.TryReadUInt32(packetAddress + 4, out var control) ||
!ctx.TryReadUInt64(packetAddress + 8, out var address) ||
!ctx.TryReadUInt32(packetAddress + 16, out var reference) ||
!ctx.TryReadUInt32(packetAddress + 20, out var mask) ||
!ctx.TryReadUInt32(address, out var value))
{
return;
}
TraceSubmittedWait(address, value, mask, reference, control & 0x7u, 32, tracePacket);
}
private static void TraceSubmittedWait(
ulong address,
ulong value,
ulong mask,
ulong reference,
uint compareFunction,
int bits,
bool tracePacket)
{
var maskedValue = value & mask;
var satisfied = compareFunction switch
{
0 => false,
1 => maskedValue < reference,
2 => maskedValue <= reference,
3 => maskedValue == reference,
4 => maskedValue != reference,
5 => maskedValue >= reference,
6 => maskedValue > reference,
_ => true,
};
if (!tracePacket && (satisfied || !ShouldTraceHotPath(ref _unsatisfiedWaitTraceCount)))
{
return;
}
TraceAgc(
$"agc.dcb.wait_reg_mem bits={bits} addr=0x{address:X16} " +
$"value=0x{value:X16} mask=0x{mask:X16} ref=0x{reference:X16} " +
$"compare={compareFunction} satisfied={satisfied}");
}
private static void ApplySubmittedReleaseMem(
CpuContext ctx,
ulong packetAddress,
@@ -2756,10 +2840,11 @@ public static class AgcExports
var dataSelection = (control >> 16) & 0xFFu;
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
var data = ((ulong)dataHi << 32) | dataLo;
var wroteData = dataSelection switch
{
1 or 2 => ctx.TryWriteUInt32(destinationAddress, dataLo),
3 => ctx.TryWriteUInt64(destinationAddress, data),
1 => ctx.TryWriteUInt32(destinationAddress, dataLo),
2 or 3 => ctx.TryWriteUInt64(destinationAddress, data),
_ => false,
};
@@ -2855,6 +2940,11 @@ public static class AgcExports
case ItDrawIndex2 when packetLength >= 6:
state.DrawIndexOffset = 0;
return ctx.TryReadUInt32(packetAddress + 16, out drawCount);
// 5-dword form emitted by DcbDrawIndex (count at +4, ItIndexBase/
// ItIndexBufferSize carried by separate preceding packets).
case ItDrawIndex2 when packetLength >= 5:
state.DrawIndexOffset = 0;
return ctx.TryReadUInt32(packetAddress + 4, out drawCount);
case ItDrawIndexOffset2 when packetLength >= 5:
if (!ctx.TryReadUInt32(packetAddress + 8, out var indexOffset))
{
@@ -5635,4 +5725,86 @@ public static class AgcExports
Console.Error.WriteLine(
$"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}");
}
// Packet layouts mirror what DcbWaitRegMem emits.
// 32-bit (ItNop/RWaitMem32): +4 addrLo, +8 addrHi, +12 mask, +16 cmp|op<<8, +20 ref.
// 64-bit (ItNop/RWaitMem64): +4 addrLo, +8 addrHi, +12 maskLo, +16 maskHi,
// +20 refLo, +24 refHi, +28 cmp|op<<8, +32 poll.
private static bool TryParseWaitRegMem(
CpuContext ctx,
ulong addr,
bool is64,
out ulong waitAddr,
out ulong refVal,
out ulong mask,
out uint cmpFunc)
{
waitAddr = refVal = mask = 0;
cmpFunc = 0;
if (!ctx.TryReadUInt32(addr + 4, out var lo) ||
!ctx.TryReadUInt32(addr + 8, out var hi))
{
return false;
}
waitAddr = ((ulong)hi << 32) | lo;
if (!is64)
{
if (!ctx.TryReadUInt32(addr + 12, out var mask32) ||
!ctx.TryReadUInt32(addr + 16, out var cmpRaw) ||
!ctx.TryReadUInt32(addr + 20, out var refVal32))
{
return false;
}
mask = mask32;
refVal = refVal32;
cmpFunc = cmpRaw & 0x7;
return true;
}
if (!ctx.TryReadUInt32(addr + 12, out var maskLo) ||
!ctx.TryReadUInt32(addr + 16, out var maskHi) ||
!ctx.TryReadUInt32(addr + 20, out var refLo) ||
!ctx.TryReadUInt32(addr + 24, out var refHi) ||
!ctx.TryReadUInt32(addr + 28, out var cmpRaw64))
{
return false;
}
mask = ((ulong)maskHi << 32) | maskLo;
refVal = ((ulong)refHi << 32) | refLo;
cmpFunc = cmpRaw64 & 0x7;
return true;
}
// Standard ItWaitRegMem (7 dwords, 32-bit compare):
// +4 cmp|(op&1)<<8, +8 addrLo, +12 addrHi, +16 ref, +20 mask, +24 poll.
private static bool TryParseStandardWaitRegMem(
CpuContext ctx,
ulong addr,
out ulong waitAddr,
out ulong refVal,
out ulong mask,
out uint cmpFunc)
{
waitAddr = refVal = mask = 0;
cmpFunc = 0;
if (!ctx.TryReadUInt32(addr + 4, out var cmpRaw) ||
!ctx.TryReadUInt32(addr + 8, out var lo) ||
!ctx.TryReadUInt32(addr + 12, out var hi) ||
!ctx.TryReadUInt32(addr + 16, out var reference) ||
!ctx.TryReadUInt32(addr + 20, out var mask32))
{
return false;
}
cmpFunc = cmpRaw & 0x7;
waitAddr = ((ulong)hi << 32) | lo;
refVal = reference;
mask = mask32;
return true;
}
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
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).
internal static class GpuWaitRegistry
{
public struct WaitingDcb
{
public ulong CommandBufferAddress;
public ulong ResumeAddress;
public uint TotalDwords;
public uint ResumeOffset;
public ulong WaitAddress;
public ulong ReferenceValue;
public ulong Mask;
public uint CompareFunction;
public bool Is64Bit;
public object? State;
}
private static readonly object _gate = new();
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
public static void Register(ulong address, WaitingDcb waiter)
{
waiter.WaitAddress = address;
lock (_gate)
{
if (!_waiters.TryGetValue(address, out var list))
{
list = new List<WaitingDcb>();
_waiters.Add(address, list);
}
list.Add(waiter);
}
}
// 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)
{
List<WaitingDcb>? woken = null;
lock (_gate)
{
List<ulong>? emptied = null;
foreach (var (address, list) in _waiters)
{
for (var i = list.Count - 1; i >= 0; i--)
{
var value = readValue(address, list[i].Is64Bit);
if (value is null || !Compare(list[i], value.Value))
{
continue;
}
woken ??= new List<WaitingDcb>();
woken.Add(list[i]);
list.RemoveAt(i);
}
if (list.Count == 0)
{
emptied ??= new List<ulong>();
emptied.Add(address);
}
}
if (emptied != null)
{
foreach (var address in emptied)
{
_waiters.Remove(address);
}
}
}
return woken;
}
public static bool Compare(in WaitingDcb waiter, ulong value)
{
var masked = value & 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.
_ => true,
};
}
}
+38 -4
View File
@@ -267,19 +267,53 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
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))
{
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;
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;
}
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
// 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);
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;
@@ -0,0 +1,156 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ampr;
/// <summary>
/// sceAmprAprCommandBufferReadFile carries -1 instead of an absolute offset whenever the guest
/// wants "the next sequential chunk" of a file. For an id Software PACK archive that sequence is
/// always header -> directory table -> individual archived files, and the guest never tells us
/// which archived file it wants next - only the byte count it expects, taken from the very
/// directory entry it already parsed on its side. This tracks a per-fileId read cursor, and once
/// the directory table has streamed past, parses it so later reads can be matched back to their
/// real absolute offset by the size the guest asks for.
/// </summary>
internal static class PakDirectoryTracker
{
private const int DirectoryEntrySize = 64;
private const int DirectoryEntryNameLength = 56;
private static readonly bool _trace =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal) ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
private sealed class DirectoryEntry(string name, uint filePos, uint fileLen)
{
public string Name { get; } = name;
public uint FilePos { get; } = filePos;
public uint FileLen { get; } = fileLen;
public bool Consumed { get; set; }
}
private sealed class FileState
{
public ulong NextOffset;
public bool ExpectingDirectory;
public List<DirectoryEntry>? Directory;
}
private static readonly ConcurrentDictionary<uint, FileState> _state = new();
/// <summary>
/// Resolves what "read the next sequential chunk of size <paramref name="requestedSize"/>"
/// really means for this fileId: an unconsumed directory entry whose length matches, if the
/// directory has already been parsed, otherwise the tracked linear cursor.
/// </summary>
public static ulong ResolveSequentialOffset(uint fileId, ulong requestedSize)
{
var state = _state.GetOrAdd(fileId, static _ => new FileState());
if (state.Directory is { } entries)
{
foreach (var entry in entries)
{
if (!entry.Consumed && entry.FileLen == requestedSize)
{
entry.Consumed = true;
if (_trace)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
}
return entry.FilePos;
}
}
}
return state.NextOffset;
}
/// <summary>
/// Feeds back a completed read so the tracker can advance its cursor, recognize a PACK
/// header and jump straight to its directory table, or parse the directory table itself
/// once it streams past.
/// </summary>
public static void OnReadCompleted(CpuContext ctx, uint fileId, ulong destination, ulong fileOffset, ulong bytesRead)
{
if (bytesRead == 0)
{
return;
}
var state = _state.GetOrAdd(fileId, static _ => new FileState());
if (state.ExpectingDirectory && fileOffset == state.NextOffset)
{
state.Directory = TryParseDirectory(ctx, destination, bytesRead);
state.ExpectingDirectory = false;
state.NextOffset = fileOffset + bytesRead;
if (_trace)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pak.directory_parsed: id=0x{fileId:X8} entries={state.Directory?.Count ?? 0}");
}
return;
}
if (fileOffset == 0 && bytesRead >= 12 && TryReadPackHeader(ctx, destination, out var dirOffset))
{
state.NextOffset = dirOffset;
state.ExpectingDirectory = true;
return;
}
state.NextOffset = fileOffset + bytesRead;
}
private static bool TryReadPackHeader(CpuContext ctx, ulong destination, out ulong dirOffset)
{
dirOffset = 0;
Span<byte> header = stackalloc byte[12];
if (!ctx.Memory.TryRead(destination, header) ||
header[0] != (byte)'P' || header[1] != (byte)'A' || header[2] != (byte)'C' || header[3] != (byte)'K')
{
return false;
}
dirOffset = BinaryPrimitives.ReadUInt32LittleEndian(header[4..8]);
return true;
}
private static List<DirectoryEntry>? TryParseDirectory(CpuContext ctx, ulong destination, ulong length)
{
var count = (int)(length / DirectoryEntrySize);
if (count <= 0)
{
return null;
}
var entries = new List<DirectoryEntry>(count);
Span<byte> record = stackalloc byte[DirectoryEntrySize];
for (var i = 0; i < count; i++)
{
if (!ctx.Memory.TryRead(destination + (ulong)(i * DirectoryEntrySize), record))
{
break;
}
var nameBytes = record[..DirectoryEntryNameLength];
var nullIndex = nameBytes.IndexOf((byte)0);
var name = Encoding.ASCII.GetString(nullIndex >= 0 ? nameBytes[..nullIndex] : nameBytes);
var filePos = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(56, 4));
var fileLen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(60, 4));
entries.Add(new DirectoryEntry(name, filePos, fileLen));
}
return entries;
}
}
@@ -35,6 +35,42 @@ public static class AvPlayerExports
return unchecked((int)handle);
}
[SysAbiExport(
Nid = "JdksQu8pNdQ",
ExportName = "sceAvPlayerGetVideoDataEx",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerGetVideoDataEx(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// "UbQoYawOsfY" is sceAvPlayerIsActive, not sceAvPlayerGetVideoDataEx (the previous NID here
// was wrong - verified by hashing both names against scripts/ps5_names.txt). No player is
// ever actually active in this HLE implementation, so report false/inactive.
[SysAbiExport(
Nid = "UbQoYawOsfY",
ExportName = "sceAvPlayerIsActive",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerIsActive(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "Wnp1OVcrZgk",
ExportName = "sceAvPlayerGetAudioData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerGetAudioData(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "HD1YKVU26-M",
ExportName = "sceAvPlayerPostInit",
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.CommonDialog;
public static class CommonDialogExports
{
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
private static int _initialized;
[SysAbiExport(
@@ -18,11 +17,12 @@ public static class CommonDialogExports
LibraryName = "libSceCommonDialog")]
public static int CommonDialogInitialize(CpuContext ctx)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0
? 0
: AlreadySystemInitialized;
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
// Games can initialize the common-dialog service defensively from more than one
// subsystem. Keeping this HLE initialization idempotent avoids turning an error
// reporting path into a second, unrelated failure.
Interlocked.Exchange(ref _initialized, 1);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -8,8 +8,13 @@ namespace SharpEmu.Libs.CommonDialog;
public static class MsgDialogExports
{
private const int AlreadyInitialized = unchecked((int)0x80B80002);
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusFinished = 3;
private const int ResultSize = 0x20;
private static int _initialized;
private static int _status;
[SysAbiExport(
Nid = "lDqxaY1UbEo",
@@ -18,9 +23,162 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogInitialize(CpuContext ctx)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0
? 0
: AlreadyInitialized;
// Treat repeated initialization as success. The dialog service is process-global in
// this HLE implementation and has no per-call resources to recreate.
Interlocked.Exchange(ref _initialized, 1);
Interlocked.Exchange(ref _status, StatusInitialized);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ePw-kqZmelo",
ExportName = "sceMsgDialogTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogTerminate(CpuContext ctx)
{
Interlocked.Exchange(ref _initialized, 0);
Interlocked.Exchange(ref _status, StatusNone);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "b06Hh0DPEaE",
ExportName = "sceMsgDialogOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogOpen(CpuContext ctx)
{
LogDialogMessage(ctx, ctx[CpuRegister.Rdi]);
// There is no host popup to actually show. Complete immediately with "finished" so a
// guest polling loop (GetStatus/UpdateStatus -> GetResult -> Close) sees a dismissed
// dialog on its very first poll instead of spinning forever waiting for user input.
Interlocked.Exchange(ref _status, StatusFinished);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
// log even though no host dialog is shown. The PS5 SceMsgDialogParam layout is not
// fully known, so chase every qword in the struct that points at readable text - one
// level deep, then a second level for nested sub-param structs.
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
{
if (paramAddress == 0)
{
Console.Error.WriteLine("[LOADER][INFO] sceMsgDialogOpen: param=NULL");
return;
}
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
Span<byte> head = stackalloc byte[0xA0];
if (ctx.Memory.TryRead(paramAddress, head))
{
DumpPointerStrings(ctx, paramAddress, head, "param", chaseNested: true);
}
}
private static void DumpPointerStrings(CpuContext ctx, ulong baseAddress, ReadOnlySpan<byte> bytes, string label, bool chaseNested)
{
Span<byte> nested = stackalloc byte[0x40];
for (var offset = 0; offset + 8 <= bytes.Length; offset += 8)
{
var value = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(bytes[offset..]);
if (value < 0x10000 || value == baseAddress)
{
continue;
}
var text = TryReadPrintableText(ctx, value);
if (text is not null)
{
Console.Error.WriteLine($"[LOADER][INFO] {label}+0x{offset:X2} -> 0x{value:X12} text=\"{text}\"");
}
else if (chaseNested && ctx.Memory.TryRead(value, nested))
{
DumpPointerStrings(ctx, value, nested, $"{label}+0x{offset:X2}", chaseNested: false);
}
}
}
private static string? TryReadPrintableText(CpuContext ctx, ulong address)
{
Span<byte> bytes = stackalloc byte[256];
if (!ctx.Memory.TryRead(address, bytes))
{
return null;
}
var nullIndex = bytes.IndexOf((byte)0);
if (nullIndex < 2)
{
return null;
}
var candidate = bytes[..nullIndex];
foreach (var b in candidate)
{
if (b is < 0x20 or > 0x7E && b != (byte)'\n' && b != (byte)'\r' && b != (byte)'\t')
{
return null;
}
}
return System.Text.Encoding.ASCII.GetString(candidate).Replace("\n", "\\n").Replace("\r", "\\r");
}
[SysAbiExport(
Nid = "CWVW78Qc3fI",
ExportName = "sceMsgDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "6fIC3XKt2k0",
ExportName = "sceMsgDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "Lr8ovHH9l6A",
ExportName = "sceMsgDialogGetResult",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogGetResult(CpuContext ctx)
{
var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> result = stackalloc byte[ResultSize];
result.Clear();
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "HTrcDKlFKuM",
ExportName = "sceMsgDialogClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogClose(CpuContext ctx)
{
Interlocked.Exchange(ref _status, StatusFinished);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
+2 -2
View File
@@ -93,7 +93,7 @@ public static class CxaGuardExports
}
[SysAbiExport(
Nid = "",
Nid = "9rAeANT2tyE",
ExportName = "__cxa_guard_release",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
@@ -142,7 +142,7 @@ public static class CxaGuardExports
}
[SysAbiExport(
Nid = "",
Nid = "2emaaluWzUw",
ExportName = "__cxa_guard_abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
+81 -12
View File
@@ -73,6 +73,21 @@ public static class KernelExports
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)
{
// 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;
}
[SysAbiExport(
Nid = "XKRegsFpEpk",
ExportName = "catchReturnFromMain",
@@ -263,20 +278,14 @@ public static class KernelExports
ExportName = "pthread_create",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreate(CpuContext ctx)
{
return PthreadCreate(ctx);
}
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
[SysAbiExport(
Nid = "Jmi+9w9u0E4",
ExportName = "pthread_create_name_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreateNameNp(CpuContext ctx)
{
return PthreadCreate(ctx);
}
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
[SysAbiExport(
Nid = "3kg7rT0NQIs",
@@ -353,10 +362,7 @@ public static class KernelExports
ExportName = "pthread_join",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadJoin(CpuContext ctx)
{
return PthreadJoin(ctx);
}
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
[SysAbiExport(
Nid = "wuCroIGjt2g",
@@ -454,4 +460,67 @@ public static class KernelExports
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Unidentified kernel NIDs observed at runtime; stubbed to return 0 until they are
// resolved against scripts/ps5_names.txt.
[SysAbiExport(
Nid = "9T2pDF2Ryqg",
ExportName = "sceKernelUnknown9T2p",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknown9T2p(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "TU-d9PfIHPM",
ExportName = "sceKernelUnknownTU",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownTU(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "iWQWrwiSt8A",
ExportName = "sceKernelUnknowniWQW",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknowniWQW(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "KuOmgKoqCdY",
ExportName = "sceKernelUnknownKuOmg",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownKuOmg(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "RenI1lL1WFk",
ExportName = "sceKernelUnknownRenI",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownRenI(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "pQGpHYopAIY",
ExportName = "sceKernelUnknownpQGp",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownpQGp(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "Rbvt+5Y2iEw",
ExportName = "sceKernelUnknownRbvt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownRbvt(CpuContext ctx) => SetZeroReturn(ctx);
// 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.
private static int SetZeroReturn(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
}
@@ -53,6 +53,8 @@ public static class KernelMemoryCompatExports
private const int OrbisVirtualQueryInfoSize = 72;
private const int OrbisKernelMaximumNameLength = 32;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint MemRelease = 0x8000;
private const uint HostPageNoAccess = 0x01;
private const uint HostPageReadOnly = 0x02;
private const uint HostPageReadWrite = 0x04;
@@ -121,6 +123,7 @@ public static class KernelMemoryCompatExports
private static int _hostMemoryWriteFallbackCount;
private static int _hostMemoryReadFallbackCount;
private static int _nullWcscpyRecoveryCount;
private static int _nullStrcasecmpRecoveryCount;
private static string? _cachedApp0Root;
private static string? _cachedDownload0Root;
@@ -143,6 +146,13 @@ public static class KernelMemoryCompatExports
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
private sealed class OpenDirectory
{
public required string Path { get; init; }
@@ -151,7 +161,7 @@ public static class KernelMemoryCompatExports
}
private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType);
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment);
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment, bool IsGuarded);
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
@@ -186,6 +196,14 @@ public static class KernelMemoryCompatExports
ulong length,
ulong alignment,
out ulong address)
=> TryAllocateHleData(ctx, length, alignment, OrbisProtCpuReadWrite, out address);
internal static bool TryAllocateHleData(
CpuContext ctx,
ulong length,
ulong alignment,
int protection,
out ulong address)
{
address = 0;
if (length == 0 || length > int.MaxValue)
@@ -200,7 +218,7 @@ public static class KernelMemoryCompatExports
var desiredAddress = AlignUp(
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
effectiveAlignment);
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, effectiveAlignment, out address) ||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, protection, effectiveAlignment, out address) ||
address == 0)
{
return false;
@@ -210,7 +228,7 @@ public static class KernelMemoryCompatExports
_mappedRegions[address] = new MappedRegion(
address,
mappedLength,
OrbisProtCpuReadWrite,
protection,
IsFlexible: false,
IsDirect: false,
DirectStart: 0);
@@ -231,6 +249,69 @@ public static class KernelMemoryCompatExports
return true;
}
private static ulong _dummyVtableAddress;
private const int DummyVtableSlotCount = 64;
// Some KexEngine objects (mutex wrappers, NGS2 system/rack/voice handles, etc.) are treated
// by guest code as C++ instances with a vtable pointer at offset 0. When an HLE constructor
// leaves that slot zeroed, the game's own virtual dispatch (call [ [obj]+N ]) reads a null
// vtable and jumps through address N, crashing (e.g. "CALL qword ptr [RAX+0x58]" with RAX=0).
// This allocates one shared, executable no-op stub plus a vtable of pointers to it, so any
// HLE object constructor can make virtual calls on its output land safely.
internal static bool TryWriteDummyVtable(CpuContext ctx, ulong objectAddress)
{
if (objectAddress == 0 || !TryEnsureDummyVtable(ctx, out var vtableAddress))
{
return false;
}
return ctx.TryWriteUInt64(objectAddress, vtableAddress);
}
private static bool TryEnsureDummyVtable(CpuContext ctx, out ulong vtableAddress)
{
lock (_memoryGate)
{
if (_dummyVtableAddress != 0)
{
vtableAddress = _dummyVtableAddress;
return true;
}
const int executableReadWrite = OrbisProtCpuRead | OrbisProtCpuWrite | OrbisProtCpuExec;
if (!TryAllocateHleData(ctx, 0x1000, 0x1000, executableReadWrite, out var block))
{
vtableAddress = 0;
return false;
}
// xor eax, eax; ret - every dummy virtual method just returns 0 and does nothing else.
if (!ctx.Memory.TryWrite(block, new byte[] { 0x31, 0xC0, 0xC3 }))
{
vtableAddress = 0;
return false;
}
var table = new byte[DummyVtableSlotCount * sizeof(ulong)];
for (var i = 0; i < DummyVtableSlotCount; i++)
{
BinaryPrimitives.WriteUInt64LittleEndian(table.AsSpan(i * sizeof(ulong), sizeof(ulong)), block);
}
var tableAddress = block + 0x100;
if (!ctx.Memory.TryWrite(tableAddress, table))
{
vtableAddress = 0;
return false;
}
Console.Error.WriteLine($"[LOADER][INFO] Dummy vtable ready: stub=0x{block:X16} vtable=0x{tableAddress:X16} slots={DummyVtableSlotCount}");
_dummyVtableAddress = tableAddress;
vtableAddress = tableAddress;
return true;
}
}
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
{
if (address == 0 || length == 0)
@@ -281,6 +362,18 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Longer null-dst memsets are unrecoverable, but RAX must still be set - leaving it
// stale here previously let callers that do `buf = memset(...)` carry on with a
// garbage "buffer" pointer instead of a clean NULL, causing a *different*,
// confusingly-located crash further downstream.
var largeRecoveryIndex = Interlocked.Increment(ref _nullMemsetRecoveryCount);
if (largeRecoveryIndex <= 8)
{
Console.Error.WriteLine(
$"[LOADER][WARNING] memset null-dst (len>0x20) recovery#{largeRecoveryIndex}: rip=0x{ctx.Rip:X16} len=0x{length:X} val=0x{value:X2}");
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
@@ -304,6 +397,7 @@ public static class KernelMemoryCompatExports
Console.WriteLine("!!! CRITICAL: Bad Memset Call !!!");
Console.WriteLine($"Called from RIP: 0x{ctx.Rip:X}");
Console.WriteLine($"dst=0x{destination:X} val=0x{value:X2} len=0x{length:X}");
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -536,6 +630,80 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "AV6ipCNa4Rw",
ExportName = "strcasecmp",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Strcasecmp(CpuContext ctx)
{
var left = ctx[CpuRegister.Rdi];
var right = ctx[CpuRegister.Rsi];
if (left == 0 || right == 0)
{
var recoveryIndex = Interlocked.Increment(ref _nullStrcasecmpRecoveryCount);
if (recoveryIndex <= 16)
{
var otherAddress = left == 0 ? right : left;
var otherText = otherAddress != 0 && TryReadNullTerminatedUtf8(ctx, otherAddress, 256, out var text)
? text
: "<unreadable>";
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
Console.Error.WriteLine(
$"[LOADER][WARNING] strcasecmp null-arg recovery#{recoveryIndex}: ret=0x{returnRip:X16} left=0x{left:X16} right=0x{right:X16} other=\"{otherText}\"");
}
// Real strcasecmp(NULL, x) is undefined behaviour and previously crashed inside the
// LLE-routed implementation. Treat it as "not equal" instead so callers doing
// `if (strcasecmp(a, b) == 0)` degrade gracefully rather than taking down the guest.
ctx[CpuRegister.Rax] = left == right ? 0uL : 1uL;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryCompareStringsCaseInsensitive(ctx, left, right, limit: ulong.MaxValue, out var compare))
{
ctx[CpuRegister.Rax] = 1;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryCompareStringsCaseInsensitive(CpuContext ctx, ulong left, ulong right, ulong limit, out int compare)
{
compare = 0;
if (left == 0 || right == 0)
{
return false;
}
var max = limit == ulong.MaxValue ? 1_048_576UL : Math.Min(limit, 1_048_576UL);
Span<byte> leftByte = stackalloc byte[1];
Span<byte> rightByte = stackalloc byte[1];
for (ulong i = 0; i < max; i++)
{
if (!TryReadCompat(ctx, left + i, leftByte) ||
!TryReadCompat(ctx, right + i, rightByte))
{
return false;
}
var leftLower = ToAsciiLower(leftByte[0]);
var rightLower = ToAsciiLower(rightByte[0]);
compare = leftLower - rightLower;
if (compare != 0 || leftByte[0] == 0 || rightByte[0] == 0)
{
return true;
}
}
compare = 0;
return true;
}
private static byte ToAsciiLower(byte value) => value is >= (byte)'A' and <= (byte)'Z' ? (byte)(value + 32) : value;
[SysAbiExport(
Nid = "0nV21JjYCH8",
ExportName = "wcsncpy",
@@ -587,6 +755,54 @@ public static class KernelMemoryCompatExports
return VsnprintfCore(ctx);
}
// sprintf/vsprintf are served from the same HLE formatting engine as snprintf/vsnprintf
// instead of falling through to the game's bundled libc.
[SysAbiExport(
Nid = "tcVi5SivF7Q",
ExportName = "sprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Sprintf(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var format = Encoding.UTF8.GetString(formatBytes);
var rendered = FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
}
[SysAbiExport(
Nid = "jbz9I9vkqkk",
ExportName = "vsprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Vsprintf(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
var vaListAddress = ctx[CpuRegister.Rdx];
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var format = Encoding.UTF8.GetString(formatBytes);
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
{
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, formatBytes);
}
var rendered = FormatString(ctx, format, ref vaCursor);
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
}
[SysAbiExport(
Nid = "nJz16JE1txM",
ExportName = "swprintf",
@@ -945,15 +1161,27 @@ public static class KernelMemoryCompatExports
{
var destination = ctx[CpuRegister.Rdi];
var source = ctx[CpuRegister.Rsi];
var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
if (count < 0)
var rawCount = ctx[CpuRegister.Rdx];
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
// upstream bug that also feeds bad lengths to memset) must not reach
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
// exception. Reject anything above a sane bound before allocating.
const ulong maxSaneCount = 512UL * 1024 * 1024;
if (rawCount > maxSaneCount)
{
Console.Error.WriteLine($"[LOADER][WARNING] memcpy oversized count rejected: dst=0x{destination:X16} src=0x{source:X16} count=0x{rawCount:X}");
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var count = (int)rawCount;
var payload = GC.AllocateUninitializedArray<byte>(count);
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
{
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1196,6 +1424,24 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Was unresolved (returning the 0x80020002 sentinel, then crashing when the guest
// dereferenced it) - the game's own heap instrumentation calls this hook when it
// detects a corrupted/invalid block, not the emulator's allocator, so this is purely
// a diagnostic sink: log what was reported and return success so the caller continues.
[SysAbiExport(
Nid = "al3JzFI9MQ0",
ExportName = "sceLibcInternalHeapErrorReportForGame",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceLibcInternal")]
public static int LibcInternalHeapErrorReportForGame(CpuContext ctx)
{
Console.Error.WriteLine(
$"[LOADER][WARN] sceLibcInternalHeapErrorReportForGame: rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16}");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "DfivPArhucg",
ExportName = "memcmp",
@@ -4151,7 +4397,7 @@ public static class KernelMemoryCompatExports
return FileMode.Open;
}
private static string ResolveGuestPath(string guestPath)
public static string ResolveGuestPath(string guestPath)
{
if (string.IsNullOrWhiteSpace(guestPath))
{
@@ -4475,7 +4721,7 @@ public static class KernelMemoryCompatExports
private static bool IsMutatingOpen(int flags) =>
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
private static bool IsReadOnlyGuestMutationPath(string guestPath)
public static bool IsReadOnlyGuestMutationPath(string guestPath)
{
var normalized = NormalizeGuestStatCachePath(guestPath);
return normalized is not null &&
@@ -4754,7 +5000,7 @@ public static class KernelMemoryCompatExports
return destination == 0 || destinationCount == 0 || TryWriteWideTerminator(ctx, destination);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
public static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0 || maxLength <= 0)
@@ -5543,6 +5789,26 @@ public static class KernelMemoryCompatExports
alignment = NormalizeLibcAlignment(alignment);
var actualSize = requestedSize == 0 ? 1u : requestedSize;
// This intentionally uses host guard pages for allocations made through the libc HLE.
// It turns an overrun that reaches the next page into an immediate access violation at
// the offending guest instruction, rather than allowing it to poison a later import.
var guardHeap = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_GUARD_HEAP"),
"1",
StringComparison.Ordinal);
if (guardHeap && TryAllocateGuardedLibcHeap(actualSize, alignment, zeroFill, out address))
{
return true;
}
// Do not silently fall back to an unguarded allocation when guard heap was explicitly
// requested: a failed setup must remain visible to the caller/reproducer.
if (guardHeap)
{
return false;
}
nuint totalSize;
try
{
@@ -5578,7 +5844,7 @@ public static class KernelMemoryCompatExports
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)IntPtr.Size, (ulong)alignment);
lock (_libcAllocGate)
{
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment);
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: false);
}
try
@@ -5598,6 +5864,51 @@ public static class KernelMemoryCompatExports
return true;
}
private static unsafe bool TryAllocateGuardedLibcHeap(nuint actualSize, nuint alignment, bool zeroFill, out ulong address)
{
address = 0;
const nuint pageSize = 0x1000;
try
{
var effectiveAlignment = Math.Max(alignment, pageSize);
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
if (baseAddress == 0)
{
return false;
}
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
var guardAddress = alignedAddress + (ulong)usableSize;
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
{
_ = VirtualFree(baseAddress, 0, MemRelease);
return false;
}
lock (_libcAllocGate)
{
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: true);
}
if (zeroFill)
{
NativeMemory.Clear((void*)alignedAddress, actualSize);
}
Console.Error.WriteLine(
$"[LOADER][TRACE] guard-heap alloc: ptr=0x{alignedAddress:X16} size=0x{actualSize:X} guard=0x{guardAddress:X16}");
address = alignedAddress;
return true;
}
catch (OverflowException)
{
return false;
}
}
private static unsafe bool TryReallocateLibcHeap(ulong existingAddress, ulong requestedSize, out ulong resizedAddress)
{
resizedAddress = 0;
@@ -5706,7 +6017,14 @@ public static class KernelMemoryCompatExports
}
}
Marshal.FreeHGlobal(allocation.BaseAddress);
if (allocation.IsGuarded)
{
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
}
else
{
Marshal.FreeHGlobal(allocation.BaseAddress);
}
}
private static bool TryMultiplyAllocationSize(ulong left, ulong right, out nuint size)
@@ -6312,4 +6630,15 @@ public static class KernelMemoryCompatExports
sum = left + right;
return sum >= left;
}
[SysAbiExport(
Nid = "KMcEa+rHsIo",
ExportName = "sceKernelMapMemory",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelMapMemory(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+467
View File
@@ -0,0 +1,467 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
namespace SharpEmu.Libs.LibcStdio;
public static class LibcStdioExports
{
private const int MaxPathLength = 4096;
private const int MaxModeLength = 16;
private const int ReadChunkSize = 1024 * 1024;
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);
private const int CtypeTableLowerBound = -128;
private const int CtypeTableUpperBound = 255;
private const int CtypeTableEntryCount = CtypeTableUpperBound - CtypeTableLowerBound + 1; // 384 entries * 2 bytes = 768 bytes
// Mirrors the Dinkumware ctype bitmask layout (the CRT Sony's libc is based on;
// _Getpctype/_Getptolower/_Getptoupper are Dinkumware accessor names). This is NOT the
// MSVC/UCRT layout: e.g. Dinkumware puts digit at 0x20 where UCRT puts control, so
// serving a UCRT-shaped table made the game's bundled printf engine misparse every
// %-directive (fatal-error messages rendered empty) and made its mcpp preprocessor
// treat 'a'-'f' as control characters (UCRT _HEX=0x80 reads as Dinkumware _BB) and
// drop them from identifiers ("texture" -> "txtur"). Titles that bundle their own
// libc bypass this table entirely (see IsSafeLleLibcExport in DirectExecutionBackend);
// this fallback only serves titles that import _Getpctype without shipping one.
private const ushort CtypeXDigit = 0x001; // _XD '0'-'9', 'A'-'F', 'a'-'f'
private const ushort CtypeUpper = 0x002; // _UP 'A'-'Z'
private const ushort CtypeSpace = 0x004; // _SP ' ' (isspace = _CN|_SP|_XS)
private const ushort CtypePunct = 0x008; // _PU punctuation
private const ushort CtypeLower = 0x010; // _LO 'a'-'z'
private const ushort CtypeDigit = 0x020; // _DI '0'-'9'
private const ushort CtypeControlSpace = 0x040; // _CN '\t','\n','\v','\f','\r'
private const ushort CtypeControl = 0x080; // _BB other control characters
private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t'
private static readonly object _ctypeTableGate = new();
private static nint _ctypeTableBase;
[SysAbiExport(
Nid = "xeYO4u7uyJ0",
ExportName = "fopen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fopen(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var modeAddress = ctx[CpuRegister.Rsi];
if (pathAddress == 0 || modeAddress == 0 ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
{
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> PERMISSION_DENIED (read-only path)");
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
try
{
if (fileAccess != FileAccess.Read)
{
var parentDirectory = Path.GetDirectoryName(hostPath);
if (!string.IsNullOrWhiteSpace(parentDirectory))
{
Directory.CreateDirectory(parentDirectory);
}
}
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
{
stream.Seek(0, SeekOrigin.End);
}
var handle = unchecked((ulong)Interlocked.Add(ref _nextHandle, 8));
_fileHandles[handle] = stream;
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X} length={stream.Length}");
}
ctx[CpuRegister.Rax] = handle;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
}
ctx[CpuRegister.Rax] = 0;
return ex is UnauthorizedAccessException
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
}
[SysAbiExport(
Nid = "rQFVBXp-Cxg",
ExportName = "fseek",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fseek(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var offset = unchecked((long)ctx[CpuRegister.Rsi]);
var whence = unchecked((int)ctx[CpuRegister.Rdx]);
if (!_fileHandles.TryGetValue(handle, out var stream) || !TryGetSeekOrigin(whence, out var origin))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
stream.Seek(offset, origin);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "Qazy8LmXTvw",
ExportName = "ftell",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Ftell(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
ctx[CpuRegister.Rax] = unchecked((ulong)stream.Position);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "uodLYyUip20",
ExportName = "fclose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fclose(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_fileHandles.TryRemove(handle, out var stream))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
stream.Dispose();
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "lbB+UlZqVG0",
ExportName = "fread",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fread(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var elementSize = ctx[CpuRegister.Rsi];
var elementCount = ctx[CpuRegister.Rdx];
var handle = ctx[CpuRegister.Rcx];
if (elementSize == 0 || elementCount == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (destination == 0 || !_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var totalRequested = elementSize * elementCount;
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
ulong totalRead = 0;
try
{
while (totalRead < totalRequested)
{
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalRead);
var read = stream.Read(buffer, 0, request);
if (read <= 0)
{
break;
}
if (!ctx.Memory.TryWrite(destination + totalRead, buffer.AsSpan(0, read)))
{
ctx[CpuRegister.Rax] = totalRead / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
totalRead += (ulong)read;
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = totalRead / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fread: handle=0x{handle:X} requested={totalRequested} read={totalRead} pos={stream.Position}");
}
ctx[CpuRegister.Rax] = totalRead / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "KdP-nULpuGw",
ExportName = "fgets",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fgets(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var maxCount = unchecked((int)ctx[CpuRegister.Rsi]);
var handle = ctx[CpuRegister.Rdx];
if (destination == 0 || maxCount <= 0 || !_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var buffer = ArrayPool<byte>.Shared.Rent(maxCount - 1);
var count = 0;
try
{
while (count < maxCount - 1)
{
var b = stream.ReadByte();
if (b < 0)
{
break;
}
buffer[count++] = (byte)b;
if (b == '\n')
{
break;
}
}
if (count == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
Span<byte> withNul = stackalloc byte[count + 1];
buffer.AsSpan(0, count).CopyTo(withNul);
withNul[count] = 0;
if (!ctx.Memory.TryWrite(destination, withNul))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sUP1hBaouOw",
ExportName = "_Getpctype",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int GetPctype(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeTable());
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static unsafe nint EnsureCtypeTable()
{
lock (_ctypeTableGate)
{
if (_ctypeTableBase != 0)
{
return _ctypeTableBase;
}
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
for (var i = 0; i < CtypeTableEntryCount; i++)
{
var c = i + CtypeTableLowerBound;
entries[i] = c is >= 0 and <= 0x7F ? ComputeCtypeFlags(c) : (ushort)0;
}
// Table is indexed as base[c] for c in [-128, 255], so the pointer handed to the
// guest must point at the c == 0 entry, not the start of the allocation.
_ctypeTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
return _ctypeTableBase;
}
}
private static ushort ComputeCtypeFlags(int c)
{
var isUpper = c is >= 'A' and <= 'Z';
var isLower = c is >= 'a' and <= 'z';
var isDigit = c is >= '0' and <= '9';
var isHex = isDigit || (c is >= 'A' and <= 'F') || (c is >= 'a' and <= 'f');
var isAlnum = isUpper || isLower || isDigit;
// In the Dinkumware table '\t'..'\r' carry _CN (isspace and iscntrl both match via
// _CN) while plain ' ' carries _SP; keeping them distinct is what keeps isprint(' ')
// true and isprint('\t') false.
var isControlSpace = c is >= 0x09 and <= 0x0D;
var isControl = (c is >= 0x00 and <= 0x08) || (c is >= 0x0E and <= 0x1F) || c == 0x7F;
var isPrintable = c is >= 0x20 and <= 0x7E;
var isPunct = isPrintable && !isAlnum && c != ' ';
ushort flags = 0;
if (isUpper) flags |= CtypeUpper;
if (isLower) flags |= CtypeLower;
if (isDigit) flags |= CtypeDigit;
if (isHex) flags |= CtypeXDigit;
if (c == ' ') flags |= CtypeSpace | CtypeBlank;
if (c == '\t') flags |= CtypeBlank;
if (isControlSpace) flags |= CtypeControlSpace;
if (isPunct) flags |= CtypePunct;
if (isControl) flags |= CtypeControl;
return flags;
}
private static bool TryParseFopenMode(string mode, out FileMode fileMode, out FileAccess fileAccess)
{
fileMode = FileMode.Open;
fileAccess = FileAccess.Read;
if (string.IsNullOrEmpty(mode))
{
return false;
}
var plus = mode.Contains('+');
switch (mode[0])
{
case 'r':
fileMode = FileMode.Open;
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Read;
return true;
case 'w':
fileMode = FileMode.Create;
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Write;
return true;
case 'a':
if (plus)
{
fileMode = FileMode.OpenOrCreate;
fileAccess = FileAccess.ReadWrite;
}
else
{
fileMode = FileMode.Append;
fileAccess = FileAccess.Write;
}
return true;
default:
return false;
}
}
private static bool TryGetSeekOrigin(int whence, out SeekOrigin origin)
{
switch (whence)
{
case 0:
origin = SeekOrigin.Begin;
return true;
case 1:
origin = SeekOrigin.Current;
return true;
case 2:
origin = SeekOrigin.End;
return true;
default:
origin = default;
return false;
}
}
}
+40 -6
View File
@@ -13,7 +13,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 = 0x20;
private const ulong HandleStorageSize = 0x1000;
private const int RenderBufferInfoSize = 0x18;
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
@@ -197,12 +197,19 @@ public static class Ngs2Exports
lock (StateGate)
{
return ctx.SetReturn(
Voices.ContainsKey(ctx[CpuRegister.Rdi])
? 0
Voices.ContainsKey(ctx[CpuRegister.Rdi])
? 0
: OrbisNgs2ErrorInvalidVoiceHandle);
}
}
[SysAbiExport(
Nid = "-4GCfYdNF1s",
ExportName = "sceNgs2VoiceControl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceControlAlt(CpuContext ctx) => Ngs2VoiceControl(ctx);
[SysAbiExport(
Nid = "AbYvTOZ8Pts",
ExportName = "sceNgs2VoiceRunCommands",
@@ -210,6 +217,27 @@ public static class Ngs2Exports
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
[SysAbiExport(
Nid = "-TOuuAQ-buE",
ExportName = "sceNgs2VoiceRunCommands",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceRunCommandsAlt(CpuContext ctx) => Ngs2VoiceControl(ctx);
[SysAbiExport(
Nid = "x8qnXqh-tiM",
ExportName = "sceNgs2VoiceGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "wPJGwI2RM2I",
ExportName = "sceNgs2VoiceGetStateFlags",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "i0VnXM-C9fc",
ExportName = "sceNgs2SystemRender",
@@ -263,7 +291,6 @@ public static class Ngs2Exports
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;
@@ -271,11 +298,18 @@ 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);
return ctx.Memory.TryWrite(handle, data);
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;
}
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
+108
View File
@@ -0,0 +1,108 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Np;
public static class NpTrophy2Exports
{
private static int _nextContext = 1;
private static int _nextHandle = 1;
[SysAbiExport(
Nid = "Bagshr7OQ6Q",
ExportName = "sceNpTrophy2CreateContext",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2CreateContext(CpuContext ctx)
{
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextContext);
}
[SysAbiExport(
Nid = "Gz1rmUZpROM",
ExportName = "sceNpTrophy2CreateHandle",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2CreateHandle(CpuContext ctx)
{
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextHandle);
}
[SysAbiExport(
Nid = "sysY2FHYff4",
ExportName = "sceNpTrophy2DestroyContext",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2DestroyContext(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "d8P11CI40KE",
ExportName = "sceNpTrophy2DestroyHandle",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2DestroyHandle(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "fYapWA9xVmA",
ExportName = "sceNpTrophy2AbortHandle",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2AbortHandle(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "bIDov3wBu5Q",
ExportName = "sceNpTrophy2RegisterContext",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2RegisterContext(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "sUXGfNMalIo",
ExportName = "sceNpTrophy2RegisterUnlockCallback",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2RegisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "wVqxM58sIKs",
ExportName = "sceNpTrophy2UnregisterUnlockCallback",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2UnregisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "EHQEDVXZ0TI",
ExportName = "sceNpTrophy2ShowTrophyList",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
{
if (outAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> idBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(idBytes, nextId);
if (!ctx.Memory.TryWrite(outAddress, idBytes))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
nextId++;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int ReturnOk(CpuContext ctx) => SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
}
@@ -72,4 +72,15 @@ public static class SystemGestureExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "1MMK0W-kMgA",
ExportName = "sceSystemGestureAppendTouchRecognizer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureAppendTouchRecognizer(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -96,4 +96,11 @@ public static class SystemServiceExports
VulkanVideoPresenter.HideSplashScreen();
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "3s8cHiCBKBE",
ExportName = "sceSystemServiceReportAbnormalTermination",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceReportAbnormalTermination(CpuContext ctx) => ctx.SetReturn(0);
}
@@ -18,6 +18,9 @@ public static class UserServiceExports
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",
@@ -42,9 +45,11 @@ public static class UserServiceExports
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
}
return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
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);
}
[SysAbiExport(
@@ -124,9 +129,12 @@ public static class UserServiceExports
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
nameBytes.CopyTo(output);
return ctx.Memory.TryWrite(nameAddress, output)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
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(
@@ -152,4 +160,12 @@ public static class UserServiceExports
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static void TraceUserService(string message)
{
if (_traceUserService)
{
Console.Error.WriteLine($"[LOADER][TRACE] user_service.{message}");
}
}
}
+169 -5
View File
@@ -38,6 +38,7 @@ public static class VideoOutExports
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
private const ulong SceVideoOutInternalEventVblank = 0x5;
private const ulong SceVideoOutInternalEventFlip = 0x6;
private const short OrbisKernelEventFilterVideoOut = -13;
@@ -53,9 +54,15 @@ public static class VideoOutExports
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
"1",
StringComparison.Ordinal);
private static readonly bool _logVideoOutSync = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
"1",
StringComparison.Ordinal);
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
private static long _submittedFrameCount;
private static long _presentedFrameCount;
private static long _vblankSignalCount;
private static long _flipSubmitCount;
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
{
@@ -99,6 +106,7 @@ public static class VideoOutExports
public float Gamma { get; set; } = 1.0f;
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
public List<FlipEventRegistration> VblankEvents { get; } = new();
public List<FlipEventRegistration> FlipEvents { get; } = new();
}
@@ -312,11 +320,48 @@ public static class VideoOutExports
}
Thread.Sleep(1);
lock (_stateGate)
SignalVblank(port);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Xru92wHJRmg",
ExportName = "sceVideoOutAddVblankEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutAddVblankEvent(CpuContext ctx)
{
var equeue = ctx[CpuRegister.Rdi];
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
if (!TryGetPort(handle, out var port))
{
port.VblankCount++;
return OrbisVideoOutErrorInvalidHandle;
}
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
{
return OrbisVideoOutErrorInvalidEventQueue;
}
var userData = ctx[CpuRegister.Rdx];
lock (_stateGate)
{
var existingIndex = port.VblankEvents.FindIndex(registration => registration.Equeue == equeue);
if (existingIndex >= 0)
{
port.VblankEvents[existingIndex] = new FlipEventRegistration(equeue, userData);
}
else
{
port.VblankEvents.Add(new FlipEventRegistration(equeue, userData));
}
}
// Some engines wait on this queue before issuing their first flip. Provide a first
// edge now; later calls to WaitVblank advance the same notification sequence.
SignalVblank(port);
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -371,6 +416,53 @@ public static class VideoOutExports
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: true);
}
// Struct layout matches the classic SceVideoOutFlipStatus (40 bytes):
// count, processTime, tsc, flipArg, currentBuffer, flipPendingNum.
[SysAbiExport(
Nid = "SbU3dwp80lQ",
ExportName = "sceVideoOutGetFlipStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutGetFlipStatus(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
VideoOutPortState? port;
lock (_stateGate)
{
_ports.TryGetValue(handle, out port);
}
if (port is null)
{
return OrbisVideoOutErrorInvalidHandle;
}
ulong count;
long flipArg;
uint currentBuffer;
lock (_stateGate)
{
count = port.FlipCount;
flipArg = 0;
currentBuffer = unchecked((uint)port.CurrentBuffer);
}
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x00, count);
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x08, 0);
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0);
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "zgXifHT9ErY",
ExportName = "sceVideoOutIsFlipPending",
@@ -407,7 +499,8 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
if (filter != OrbisKernelEventFilterVideoOut ||
ident is not (SceVideoOutInternalEventVblank or SceVideoOutInternalEventFlip))
{
return OrbisVideoOutErrorInvalidEvent;
}
@@ -436,7 +529,8 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
if (filter != OrbisKernelEventFilterVideoOut ||
ident is not (SceVideoOutInternalEventVblank or SceVideoOutInternalEventFlip))
{
return OrbisVideoOutErrorInvalidEvent;
}
@@ -756,6 +850,38 @@ public static class VideoOutExports
return groupIndex < 0 ? groupIndex : setIndex;
}
private static void SignalVblank(VideoOutPortState port)
{
List<FlipEventRegistration> vblankEvents;
ulong eventHint;
lock (_stateGate)
{
port.VblankCount++;
eventHint = SceVideoOutInternalEventVblank |
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
vblankEvents = new List<FlipEventRegistration>(port.VblankEvents);
}
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
foreach (var vblankEvent in vblankEvents)
{
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
vblankEvent.Equeue,
SceVideoOutInternalEventVblank,
OrbisKernelEventFilterVideoOut,
eventHint,
vblankEvent.UserData);
}
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
{
Console.Error.WriteLine(
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
$"queues={vblankEvents.Count} hint=0x{eventHint:X16}");
}
}
private static int SubmitFlip(
CpuContext ctx,
int handle,
@@ -790,11 +916,14 @@ public static class VideoOutExports
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
}
var guestImageSubmitted = false;
ulong guestImageAddress = 0;
if (submitGpuImage &&
bufferIndex >= 0 &&
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
{
_ = VulkanVideoPresenter.TrySubmitGuestImage(
guestImageAddress = displayBuffer.Address;
guestImageSubmitted = VulkanVideoPresenter.TrySubmitGuestImage(
displayBuffer.Address,
displayBuffer.Width,
displayBuffer.Height,
@@ -819,6 +948,15 @@ public static class VideoOutExports
flipEvent.UserData);
}
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
if (_logVideoOutSync && (flipCount <= 8 || flipCount % 60 == 0))
{
Console.Error.WriteLine(
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
$"flipQueues={flipEvents.Count}");
}
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
ReportFrameRate(presented: false);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -902,6 +1040,16 @@ public static class VideoOutExports
TraceVideoOut(
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
if (guestFormat != 0)
{
foreach (var address in addresses)
{
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
}
}
return groupIndex;
}
}
@@ -1123,6 +1271,22 @@ public static class VideoOutExports
? 4u
: 0u;
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
// VulkanVideoPresenter._availableGuestImages keys on (see VulkanVideoPresenter.
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
NormalizePixelFormat(pixelFormat) switch
{
SceVideoOutPixelFormatA8R8G8B8Srgb or
SceVideoOutPixelFormatA8B8G8R8Srgb or
SceVideoOutPixelFormatB8G8R8A8Unorm or
SceVideoOutPixelFormatR8G8B8A8Unorm => 56u,
SceVideoOutPixelFormatA2R10G10B10 or
SceVideoOutPixelFormatA2R10G10B10Srgb or
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq => 9u,
_ => 0u,
};
private static ulong NormalizePixelFormat(ulong pixelFormat)
{
if (GetBytesPerPixel(pixelFormat) != 0)
@@ -182,6 +182,13 @@ internal static unsafe class VulkanVideoPresenter
private static long _enqueuedGuestWorkSequence;
private static long _completedGuestWorkSequence;
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
{
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
return string.Equals(mode, "1", StringComparison.Ordinal) ||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
}
public static void EnsureStarted(uint width, uint height)
{
if (width == 0 || height == 0)
@@ -279,6 +286,11 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=Submit {width}x{height}");
}
lock (_gate)
{
if (_closed)
@@ -319,6 +331,11 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=SubmitGuestDraw({drawKind}) {width}x{height}");
}
lock (_gate)
{
if (_closed ||
@@ -376,6 +393,12 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=SubmitTranslatedDraw {width}x{height} textures={textures.Count}");
}
lock (_gate)
{
if (_closed)
@@ -442,6 +465,13 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
$"target=0x{target.Address:X16} {target.Width}x{target.Height} textures={textures.Count}");
}
lock (_gate)
{
if (_closed)
@@ -569,9 +599,31 @@ internal static unsafe class VulkanVideoPresenter
var traceSubmission = false;
lock (_gate)
{
if (_closed ||
!_availableGuestImages.ContainsKey(address))
var known = _availableGuestImages.ContainsKey(address);
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=TrySubmitGuestImage addr=0x{address:X16} " +
$"{width}x{height} known={known}");
}
if (_closed)
{
return false;
}
// The caller (VideoOutExports.SubmitFlip) reports the flip as successful either
// way, so an unregistered address means the frame is dropped silently; warn once
// per address so that shows up in the log.
if (!known)
{
if (_tracedGuestImageSubmissions.Add((address, width, height)))
{
Console.Error.WriteLine(
$"[LOADER][WARN] vk.submit_guest_image_unknown addr=0x{address:X16} " +
$"{width}x{height} - flip target was never registered as a render output");
}
return false;
}
@@ -588,6 +640,19 @@ internal static unsafe class VulkanVideoPresenter
RequiredGuestWorkSequence: 0,
IsSplash: false,
GuestImageAddress: address);
if (_thread is not null)
{
return true;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
if (traceSubmission)
@@ -601,6 +666,21 @@ internal static unsafe class VulkanVideoPresenter
return true;
}
// Display buffers registered through sceVideoOutRegisterBuffers are valid flip targets
// even when no AGC render-target write to them was ever observed.
internal static void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
{
if (address == 0 || guestFormat == 0)
{
return;
}
lock (_gate)
{
_availableGuestImages[address] = guestFormat;
}
}
internal static bool IsGpuGuestImageAvailable(
ulong address,
uint format,
@@ -5033,14 +5113,19 @@ internal static unsafe class VulkanVideoPresenter
_renderPass,
_extent);
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_firstGuestDrawPresented &&
translatedResources.Textures is
[
{ GuestImage: { } guestImage },
] &&
_tracedGuestImageContents.Add(guestImage.Address))
!_firstGuestDrawPresented)
{
TraceGuestImageContents(guestImage);
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.translated_draw kind={presentation.DrawKind} " +
$"textures={translatedResources.Textures.Length}");
foreach (var boundTexture in translatedResources.Textures)
{
if (boundTexture.GuestImage is { } guestImage &&
_tracedGuestImageContents.Add(guestImage.Address))
{
TraceGuestImageContents(guestImage);
}
}
}
}
catch (Exception exception)
@@ -5810,13 +5895,6 @@ internal static unsafe class VulkanVideoPresenter
return false;
}
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
{
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
return string.Equals(mode, "1", StringComparison.Ordinal) ||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldTraceVulkanResources() =>
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"),