mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-19 01:16:12 +08:00
[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:
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user