mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37b3e787de | |||
| 528985cda0 | |||
| de0816e1b0 | |||
| c583728e1b | |||
| 22a7ca97ed | |||
| 3d4a8803f5 | |||
| 5099816eb9 | |||
| c17c6ed303 | |||
| ca67322243 | |||
| 19dc71ba22 | |||
| 2d72d0e4fc | |||
| de45605e10 |
@@ -42,3 +42,4 @@ ehthumbs.db
|
||||
|
||||
.vs/
|
||||
.idea/
|
||||
packages.lock.json
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
/// <summary>
|
||||
/// The four Intel SHA-extension operations used by SHA-1 code. This is the arithmetic half of
|
||||
/// the direct-execution SIGILL fallback used when an x86-64 guest runs under Rosetta 2, which
|
||||
/// does not expose Intel SHA instructions to translated processes.
|
||||
/// </summary>
|
||||
public static class Sha1InstructionEmulator
|
||||
{
|
||||
public static Sha1Vector MessageSchedule1(Sha1Vector destination, Sha1Vector source) => new(
|
||||
destination.Lane0 ^ source.Lane2,
|
||||
destination.Lane1 ^ source.Lane3,
|
||||
destination.Lane2 ^ destination.Lane0,
|
||||
destination.Lane3 ^ destination.Lane1);
|
||||
|
||||
public static Sha1Vector MessageSchedule2(Sha1Vector destination, Sha1Vector source)
|
||||
{
|
||||
var lane3 = BitOperations.RotateLeft(destination.Lane3 ^ source.Lane2, 1);
|
||||
return new Sha1Vector(
|
||||
BitOperations.RotateLeft(destination.Lane0 ^ lane3, 1),
|
||||
BitOperations.RotateLeft(destination.Lane1 ^ source.Lane0, 1),
|
||||
BitOperations.RotateLeft(destination.Lane2 ^ source.Lane1, 1),
|
||||
lane3);
|
||||
}
|
||||
|
||||
public static Sha1Vector NextE(Sha1Vector destination, Sha1Vector source) => new(
|
||||
source.Lane0,
|
||||
source.Lane1,
|
||||
source.Lane2,
|
||||
unchecked(source.Lane3 + BitOperations.RotateLeft(destination.Lane3, 30)));
|
||||
|
||||
public static Sha1Vector FourRounds(Sha1Vector destination, Sha1Vector source, byte function)
|
||||
{
|
||||
uint a = destination.Lane3;
|
||||
uint b = destination.Lane2;
|
||||
uint c = destination.Lane1;
|
||||
uint d = destination.Lane0;
|
||||
uint e = 0;
|
||||
|
||||
uint constant = (function & 3) switch
|
||||
{
|
||||
0 => 0x5A82_7999u,
|
||||
1 => 0x6ED9_EBA1u,
|
||||
2 => 0x8F1B_BCDCu,
|
||||
_ => 0xCA62_C1D6u,
|
||||
};
|
||||
|
||||
for (var round = 0; round < 4; round++)
|
||||
{
|
||||
uint choose = (function & 3) switch
|
||||
{
|
||||
0 => (b & c) ^ (~b & d),
|
||||
2 => (b & c) ^ (b & d) ^ (c & d),
|
||||
_ => b ^ c ^ d,
|
||||
};
|
||||
uint word = round switch
|
||||
{
|
||||
0 => source.Lane3,
|
||||
1 => source.Lane2,
|
||||
2 => source.Lane1,
|
||||
_ => source.Lane0,
|
||||
};
|
||||
uint next = unchecked(choose + BitOperations.RotateLeft(a, 5) + word + e + constant);
|
||||
e = d;
|
||||
d = c;
|
||||
c = BitOperations.RotateLeft(b, 30);
|
||||
b = a;
|
||||
a = next;
|
||||
}
|
||||
|
||||
return new Sha1Vector(d, c, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The four little-endian 32-bit lanes of an XMM register.</summary>
|
||||
public readonly record struct Sha1Vector(uint Lane0, uint Lane1, uint Lane2, uint Lane3);
|
||||
@@ -1113,7 +1113,12 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
|
||||
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer) =>
|
||||
TryReadHostBytes(address, buffer.AsSpan());
|
||||
|
||||
// Span overload so signal-handler recovery paths can read into a stackalloc
|
||||
// buffer instead of allocating managed arrays inside the handler.
|
||||
private unsafe static bool TryReadHostBytes(ulong address, Span<byte> buffer)
|
||||
{
|
||||
if (address < 65536)
|
||||
{
|
||||
@@ -1137,7 +1142,7 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
try
|
||||
{
|
||||
Marshal.Copy((nint)address, buffer, 0, buffer.Length);
|
||||
new ReadOnlySpan<byte>((void*)address, buffer.Length).CopyTo(buffer);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -8,21 +8,21 @@ using SharpEmu.Core.Cpu.Emulation;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
|
||||
// Software fallback for unsupported guest CPU instructions.
|
||||
//
|
||||
// Guest code runs natively, so the guest and host share the same virtual address space and the
|
||||
// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
|
||||
// one of these extensions it raises #UD instead of executing the opcode; without this the title
|
||||
// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
|
||||
// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
|
||||
// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
|
||||
// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
|
||||
// does not fully model.
|
||||
// the OS to continue. BMI1/BMI2/ABM GPR operations and the Intel SHA-1 XMM operations are handled;
|
||||
// anything else falls through to the existing diagnostics unchanged.
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
// Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
|
||||
// with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
|
||||
private const int CTX_EFLAGS = 68;
|
||||
private const int CTX_XMM0 = 0x1A0;
|
||||
|
||||
// STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
|
||||
private const uint StatusIllegalInstruction = 0xC000001Du;
|
||||
@@ -34,6 +34,8 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private static int _bmiSoftwareFallbackAnnounced;
|
||||
private static long _bmiInstructionsEmulated;
|
||||
private static int _sha1SoftwareFallbackAnnounced;
|
||||
private static long _sha1InstructionsEmulated;
|
||||
|
||||
private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
|
||||
{
|
||||
@@ -42,6 +44,11 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryRecoverSha1Instruction(contextRecord, rip, in instruction))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Op0Kind != OpKind.Register ||
|
||||
!TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
|
||||
{
|
||||
@@ -72,6 +79,139 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverSha1Instruction(
|
||||
void* contextRecord,
|
||||
ulong rip,
|
||||
in Instruction instruction)
|
||||
{
|
||||
if ((!OperatingSystem.IsWindows() && !_posixVectorContextAvailable) ||
|
||||
instruction.Mnemonic is not (Mnemonic.Sha1msg1 or Mnemonic.Sha1msg2 or
|
||||
Mnemonic.Sha1nexte or Mnemonic.Sha1rnds4) ||
|
||||
instruction.Op0Kind != OpKind.Register ||
|
||||
!TryGetXmmIndex(instruction.Op0Register, out var destinationIndex) ||
|
||||
!TryReadSha1VectorOperand(contextRecord, in instruction, 1, out var source))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var destination = ReadXmm(contextRecord, destinationIndex);
|
||||
Sha1Vector result;
|
||||
switch (instruction.Mnemonic)
|
||||
{
|
||||
case Mnemonic.Sha1msg1:
|
||||
result = Sha1InstructionEmulator.MessageSchedule1(destination, source);
|
||||
break;
|
||||
case Mnemonic.Sha1msg2:
|
||||
result = Sha1InstructionEmulator.MessageSchedule2(destination, source);
|
||||
break;
|
||||
case Mnemonic.Sha1nexte:
|
||||
result = Sha1InstructionEmulator.NextE(destination, source);
|
||||
break;
|
||||
case Mnemonic.Sha1rnds4:
|
||||
if (instruction.Op2Kind != OpKind.Immediate8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = Sha1InstructionEmulator.FourRounds(destination, source, instruction.Immediate8);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteXmm(contextRecord, destinationIndex, result);
|
||||
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
|
||||
|
||||
if (!_posixSignalWarmup)
|
||||
{
|
||||
Interlocked.Increment(ref _sha1InstructionsEmulated);
|
||||
if (Interlocked.Exchange(ref _sha1SoftwareFallbackAnnounced, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Host lacks Intel SHA instructions used by the guest; " +
|
||||
"emulating SHA-1 instructions in software.");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryReadSha1VectorOperand(
|
||||
void* contextRecord,
|
||||
in Instruction instruction,
|
||||
int operandIndex,
|
||||
out Sha1Vector value)
|
||||
{
|
||||
switch (instruction.GetOpKind(operandIndex))
|
||||
{
|
||||
case OpKind.Register:
|
||||
if (TryGetXmmIndex(instruction.GetOpRegister(operandIndex), out var sourceIndex))
|
||||
{
|
||||
value = ReadXmm(contextRecord, sourceIndex);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case OpKind.Memory:
|
||||
if (TryComputeMemoryAddress(contextRecord, in instruction, out var address))
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
if (TryReadHostBytes(address, bytes))
|
||||
{
|
||||
value = new Sha1Vector(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(0, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(4, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(8, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(12, 4)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static unsafe Sha1Vector ReadXmm(void* contextRecord, int index)
|
||||
{
|
||||
uint* lanes = (uint*)((byte*)contextRecord + CTX_XMM0 + index * 16);
|
||||
return new Sha1Vector(lanes[0], lanes[1], lanes[2], lanes[3]);
|
||||
}
|
||||
|
||||
private static unsafe void WriteXmm(void* contextRecord, int index, Sha1Vector value)
|
||||
{
|
||||
uint* lanes = (uint*)((byte*)contextRecord + CTX_XMM0 + index * 16);
|
||||
lanes[0] = value.Lane0;
|
||||
lanes[1] = value.Lane1;
|
||||
lanes[2] = value.Lane2;
|
||||
lanes[3] = value.Lane3;
|
||||
}
|
||||
|
||||
private static bool TryGetXmmIndex(Register register, out int index)
|
||||
{
|
||||
switch (register)
|
||||
{
|
||||
case Register.XMM0: index = 0; return true;
|
||||
case Register.XMM1: index = 1; return true;
|
||||
case Register.XMM2: index = 2; return true;
|
||||
case Register.XMM3: index = 3; return true;
|
||||
case Register.XMM4: index = 4; return true;
|
||||
case Register.XMM5: index = 5; return true;
|
||||
case Register.XMM6: index = 6; return true;
|
||||
case Register.XMM7: index = 7; return true;
|
||||
case Register.XMM8: index = 8; return true;
|
||||
case Register.XMM9: index = 9; return true;
|
||||
case Register.XMM10: index = 10; return true;
|
||||
case Register.XMM11: index = 11; return true;
|
||||
case Register.XMM12: index = 12; return true;
|
||||
case Register.XMM13: index = 13; return true;
|
||||
case Register.XMM14: index = 14; return true;
|
||||
case Register.XMM15: index = 15; return true;
|
||||
default: index = 0; return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool TryEvaluate(
|
||||
void* contextRecord,
|
||||
in Instruction instruction,
|
||||
@@ -194,19 +334,37 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
// A managed allocation inside a signal handler is unsafe: the fault can
|
||||
// interrupt the GC mid-operation, and re-entering the allocator corrupts
|
||||
// the thread's allocation context (observed as a hard "Invalid Program" at
|
||||
// the next managed alloc, amplified by tight SHA-1 recovery loops). These
|
||||
// thread-static objects are created once per thread and reused so the
|
||||
// recovery path never allocates on the hot path.
|
||||
[ThreadStatic]
|
||||
private static byte[]? _decodeBuffer;
|
||||
[ThreadStatic]
|
||||
private static ByteArrayCodeReader? _decodeReader;
|
||||
[ThreadStatic]
|
||||
private static Decoder? _decoder;
|
||||
|
||||
private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
|
||||
{
|
||||
var buffer = _decodeBuffer ??= new byte[MaxInstructionBytes];
|
||||
var reader = _decodeReader ??= new ByteArrayCodeReader(buffer);
|
||||
var decoder = _decoder ??= Decoder.Create(64, reader);
|
||||
|
||||
// Try the full instruction window first, then shrink so a fault near the end of a mapped
|
||||
// page (where fewer than 15 bytes are readable) still decodes.
|
||||
// page (where fewer than 15 bytes are readable) still decodes. The buffer is reused, so
|
||||
// clear the tail past the readable window to keep decoding deterministic.
|
||||
foreach (var attempt in DecodeWindowSizes)
|
||||
{
|
||||
var buffer = new byte[attempt];
|
||||
if (!TryReadHostBytes(rip, buffer))
|
||||
if (!TryReadHostBytes(rip, buffer.AsSpan(0, attempt)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
|
||||
buffer.AsSpan(attempt).Clear();
|
||||
reader.Position = 0;
|
||||
decoder.IP = rip;
|
||||
decoder.Decode(out instruction);
|
||||
if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
|
||||
@@ -246,8 +404,8 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
|
||||
var buffer = new byte[byteCount];
|
||||
if (!TryReadHostBytes(address, buffer))
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
if (!TryReadHostBytes(address, buffer[..byteCount]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -630,27 +630,6 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||
}
|
||||
}
|
||||
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||
out var blockReason,
|
||||
out var blockContinuation,
|
||||
out var hasBlockContinuation,
|
||||
out var blockWakeKey,
|
||||
out var blockWaiter,
|
||||
out var blockDeadlineTimestamp) &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
||||
{
|
||||
if (hasBlockContinuation)
|
||||
{
|
||||
RegisterBlockedGuestThreadContinuation(
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
blockContinuation,
|
||||
blockWakeKey,
|
||||
blockWaiter,
|
||||
blockDeadlineTimestamp);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
}
|
||||
if (flag || flag2 || flag3)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] ImportRet#{num}: nid={importStubEntry.Nid} result={orbisGen2Result} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
|
||||
@@ -1345,35 +1324,13 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
}
|
||||
|
||||
var consumedThreadBlock = GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||
out var blockReason,
|
||||
out var blockContinuation,
|
||||
out var hasBlockContinuation,
|
||||
out var blockWakeKey,
|
||||
out var blockWaiter,
|
||||
out var blockDeadlineTimestamp);
|
||||
if (consumedThreadBlock &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
||||
{
|
||||
if (hasBlockContinuation)
|
||||
{
|
||||
RegisterBlockedGuestThreadContinuation(
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
blockContinuation,
|
||||
blockWakeKey,
|
||||
blockWaiter,
|
||||
blockDeadlineTimestamp);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
}
|
||||
if (probeLeafReturn)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] leaf-return-probe-exit nid={importStubEntry.Nid} " +
|
||||
$"original=0x{returnRip:X16} final=0x{*(ulong*)(argPackPtr + 96):X16} " +
|
||||
$"rsp=0x{leafStackPointer:X16} active_slot=0x{ActiveGuestReturnSlotAddress:X16} " +
|
||||
$"block={consumedThreadBlock} yield={ActiveGuestThreadYieldRequested}");
|
||||
$"yield={ActiveGuestThreadYieldRequested}");
|
||||
}
|
||||
|
||||
result = cpuContext[CpuRegister.Rax];
|
||||
|
||||
@@ -50,6 +50,15 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
private const int LinuxUcontextGregsOffset = 40;
|
||||
private const int LinuxGregsErrOffset = 19 * 8;
|
||||
|
||||
// XMM0 starts after the exception/thread states and the floating-point header on Darwin.
|
||||
// Linux mcontext_t instead stores a pointer to an FXSAVE-compatible _libc_fpstate after
|
||||
// gregs[23], where XMM0 begins at byte 160.
|
||||
private const int DarwinMcontextFloatStateOffset = 16 + 21 * 8;
|
||||
private const int DarwinFloatStateXmmOffset = 168;
|
||||
private const int LinuxFpregsPointerOffset = 23 * 8;
|
||||
private const int LinuxFxsaveXmmOffset = 160;
|
||||
private const int XmmRegisterBlockSize = 16 * 16;
|
||||
|
||||
// Byte offsets of the general registers relative to GetPosixRegisterBase,
|
||||
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
|
||||
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
|
||||
@@ -70,6 +79,8 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _posixSignalHandlerDepth;
|
||||
[ThreadStatic]
|
||||
private static bool _posixVectorContextAvailable;
|
||||
|
||||
private void SetupPosixExceptionHandler()
|
||||
{
|
||||
@@ -121,8 +132,10 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
byte* fakeUcontext = stackalloc byte[512];
|
||||
new Span<byte>(fakeUcontext, 512).Clear();
|
||||
byte* fakeMcontext = stackalloc byte[512];
|
||||
new Span<byte>(fakeMcontext, 512).Clear();
|
||||
// Large enough for the Darwin mcontext64 XMM block the handler now
|
||||
// round-trips (exception+thread state + float state = 708 bytes).
|
||||
byte* fakeMcontext = stackalloc byte[768];
|
||||
new Span<byte>(fakeMcontext, 768).Clear();
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
*(byte**)(fakeUcontext + DarwinUcontextMcontextOffset) = fakeMcontext;
|
||||
@@ -154,6 +167,33 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
record.ExceptionInformation[1] = 0x70000;
|
||||
_ = TryHandleLazyCommittedPage(&record, 0, 0);
|
||||
ChainPreviousPosixAction(0, 0, 0);
|
||||
|
||||
// Rosetta can deliver the first unsupported SHA instruction before the
|
||||
// runtime is in a safe state to JIT the recovery path. Decode and execute
|
||||
// each supported form once now, while signal handlers are not installed.
|
||||
byte[][] shaWarmupOpcodes =
|
||||
{
|
||||
new byte[] { 0x0F, 0x38, 0xC9, 0xC1 }, // sha1msg1 xmm0, xmm1
|
||||
new byte[] { 0x0F, 0x38, 0xCA, 0xC1 }, // sha1msg2 xmm0, xmm1
|
||||
new byte[] { 0x0F, 0x38, 0xC8, 0xC1 }, // sha1nexte xmm0, xmm1
|
||||
new byte[] { 0x0F, 0x3A, 0xCC, 0xC1, 0x00 }, // sha1rnds4 xmm0, xmm1, 0
|
||||
};
|
||||
_posixVectorContextAvailable = true;
|
||||
try
|
||||
{
|
||||
foreach (byte[] opcode in shaWarmupOpcodes)
|
||||
{
|
||||
fixed (byte* instructionBytes = opcode)
|
||||
{
|
||||
WriteCtxU64(contextRecord, CTX_RIP, (ulong)instructionBytes);
|
||||
_ = TryRecoverIllegalInstruction(contextRecord, (ulong)instructionBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_posixVectorContextAvailable = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -251,6 +291,15 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
|
||||
}
|
||||
// Only bridge the XMM block for SIGILL, the sole signal whose recovery
|
||||
// may emulate an XMM instruction (Intel SHA). Copying 256 bytes in and
|
||||
// out of the mcontext on every SIGSEGV (the hot demand-paging path) is
|
||||
// pure overhead and needless surface for corruption.
|
||||
byte* xmmRegisters = signal == PosixSigIll ? GetPosixXmmBase(registers) : null;
|
||||
if (xmmRegisters != null)
|
||||
{
|
||||
CopyXmmRegisterBlock(contextRecord + CTX_XMM0, xmmRegisters);
|
||||
}
|
||||
|
||||
EXCEPTION_RECORD record = default;
|
||||
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
|
||||
@@ -294,13 +343,21 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
// every fault anyway, and recovering here avoids dumping the full
|
||||
// VectoredHandler diagnostics for each recoverable trap.
|
||||
int disposition = 0;
|
||||
if (_posixRawRecoveryEnabled)
|
||||
_posixVectorContextAvailable = xmmRegisters != null;
|
||||
try
|
||||
{
|
||||
disposition = TryRecoverUnresolvedSentinel(&pointers);
|
||||
if (_posixRawRecoveryEnabled)
|
||||
{
|
||||
disposition = TryRecoverUnresolvedSentinel(&pointers);
|
||||
}
|
||||
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
|
||||
{
|
||||
disposition = backend.VectoredHandler(&pointers);
|
||||
}
|
||||
}
|
||||
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
|
||||
finally
|
||||
{
|
||||
disposition = backend.VectoredHandler(&pointers);
|
||||
_posixVectorContextAvailable = false;
|
||||
}
|
||||
if (traceSignal)
|
||||
{
|
||||
@@ -317,6 +374,10 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
|
||||
}
|
||||
if (xmmRegisters != null)
|
||||
{
|
||||
CopyXmmRegisterBlock(xmmRegisters, contextRecord + CTX_XMM0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -335,6 +396,25 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
return (byte*)ucontext + LinuxUcontextGregsOffset;
|
||||
}
|
||||
|
||||
private static byte* GetPosixXmmBase(byte* registers)
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return registers + DarwinMcontextFloatStateOffset + DarwinFloatStateXmmOffset;
|
||||
}
|
||||
|
||||
byte* fpregs = *(byte**)(registers + LinuxFpregsPointerOffset);
|
||||
return fpregs == null ? null : fpregs + LinuxFxsaveXmmOffset;
|
||||
}
|
||||
|
||||
private static void CopyXmmRegisterBlock(byte* destination, byte* source)
|
||||
{
|
||||
for (var offset = 0; offset < XmmRegisterBlockSize; offset += sizeof(ulong))
|
||||
{
|
||||
*(ulong*)(destination + offset) = *(ulong*)(source + offset);
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong GetPosixFaultAddress(nint siginfo, byte* registers)
|
||||
{
|
||||
ulong address = siginfo != 0 ? *(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset) : 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Support for HLE synchronization primitives that block the guest thread's
|
||||
/// host thread in place (inside the HLE call, on a host primitive) instead of
|
||||
/// capturing a continuation and re-scheduling through the cooperative wake-key
|
||||
/// machinery. In-place blocking makes block-and-wake atomic — the host
|
||||
/// primitive owns the race — which removes the lost-wakeup window the
|
||||
/// continuation path had between block registration and wake delivery.
|
||||
/// </summary>
|
||||
public static class GuestThreadBlocking
|
||||
{
|
||||
/// <summary>
|
||||
/// Upper bound on a single host wait while a guest thread is parked. Waits
|
||||
/// are sliced so parked threads observe <see cref="ShutdownRequested"/>
|
||||
/// promptly at teardown; a wake via Monitor.Pulse still lands immediately.
|
||||
/// </summary>
|
||||
public const int WaitSliceMilliseconds = 50;
|
||||
|
||||
private static volatile bool _shutdownRequested;
|
||||
|
||||
// Guest thread handle -> what it is parked on. Populated only while a
|
||||
// thread is blocked (the slow path), read by the stall watchdog so
|
||||
// in-place-blocked threads are not reported as opaque "Running" threads.
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, string> _blockDescriptions = new();
|
||||
|
||||
/// <summary>True once emulator teardown has begun; parked guest threads unwind.</summary>
|
||||
public static bool ShutdownRequested => _shutdownRequested;
|
||||
|
||||
/// <summary>Called by the execution backend when guest execution is being torn down.</summary>
|
||||
public static void RequestShutdown() => _shutdownRequested = true;
|
||||
|
||||
/// <summary>Records what the given guest thread is about to park on (diagnostics only).</summary>
|
||||
public static void NoteBlocked(ulong guestThreadHandle, string description)
|
||||
{
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
_blockDescriptions[guestThreadHandle] = description;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clears the parked-state note recorded by <see cref="NoteBlocked"/>.</summary>
|
||||
public static void NoteUnblocked(ulong guestThreadHandle)
|
||||
{
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
_blockDescriptions.TryRemove(guestThreadHandle, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>What the thread is parked on, or null if it is not parked in place.</summary>
|
||||
public static string? DescribeBlock(ulong guestThreadHandle) =>
|
||||
_blockDescriptions.TryGetValue(guestThreadHandle, out var description) ? description : null;
|
||||
|
||||
/// <summary>All currently parked threads (diagnostics; covers the primary thread too).</summary>
|
||||
public static KeyValuePair<ulong, string>[] SnapshotBlockDescriptions() => _blockDescriptions.ToArray();
|
||||
|
||||
// Interrupt delivery for threads parked in place. A thread blocked inside
|
||||
// an HLE wait keeps its executor busy, so it never reaches the import-return
|
||||
// safe point where queued guest exceptions (IL2CPP stop-the-world suspend)
|
||||
// are delivered. When an exception is queued for such a thread, its handle
|
||||
// is flagged here; each sliced wait loop calls Checkpoint, which — on the
|
||||
// thread's OWN host thread, with the wait's gate released — runs the
|
||||
// registered deliverer (the same safe-point delivery used at import
|
||||
// boundaries), then the loop re-checks its predicate and re-parks. This is
|
||||
// the SA_RESTART-style "signal on top of a blocking wait" a real kernel
|
||||
// provides. Dormant unless an exception is actually pending (empty-check
|
||||
// fast path), so it adds no cost to normal blocking.
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, byte> _interrupted = new();
|
||||
|
||||
/// <summary>Set by the backend: delivers any exception queued for the current guest thread, in place.</summary>
|
||||
public static Action? DeliverInterruptForCurrentThread { get; set; }
|
||||
|
||||
/// <summary>Flags a parked guest thread to deliver a queued exception at its next wait checkpoint.</summary>
|
||||
public static void RequestInterrupt(ulong guestThreadHandle)
|
||||
{
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
_interrupted[guestThreadHandle] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from every sliced wait loop while it holds <paramref name="gate"/>. If an
|
||||
/// exception is pending for the current guest thread, releases the gate, delivers it on
|
||||
/// this host thread, then re-acquires the gate so the loop re-checks its predicate.
|
||||
/// </summary>
|
||||
public static void Checkpoint(ulong guestThreadHandle, object gate)
|
||||
{
|
||||
if (_interrupted.IsEmpty || guestThreadHandle == 0 || !_interrupted.TryRemove(guestThreadHandle, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var deliver = DeliverInterruptForCurrentThread;
|
||||
if (deliver is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Never run guest code (the handler) while holding an HLE gate — the
|
||||
// handler may re-enter this same primitive. Release across delivery.
|
||||
Monitor.Exit(gate);
|
||||
try
|
||||
{
|
||||
deliver();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Enter(gate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,6 @@ public readonly record struct GuestThreadSnapshot(
|
||||
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
|
||||
/// its return value becomes the guest's RAX for the resumed call.
|
||||
/// </summary>
|
||||
public interface IGuestThreadBlockWaiter
|
||||
{
|
||||
int Resume();
|
||||
|
||||
bool TryWake();
|
||||
}
|
||||
|
||||
public interface IGuestThreadScheduler
|
||||
{
|
||||
bool SupportsGuestContextTransfer { get; }
|
||||
@@ -57,10 +50,6 @@ public interface IGuestThreadScheduler
|
||||
out ulong returnValue,
|
||||
out string? error);
|
||||
|
||||
void Pump(CpuContext callerContext, string reason);
|
||||
|
||||
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a new guest scheduling priority to a live thread, mapping it
|
||||
/// onto the host thread if one is running. Returns false when the thread
|
||||
@@ -152,46 +141,12 @@ public readonly record struct GuestCpuContinuation(
|
||||
|
||||
public static class GuestThreadExecution
|
||||
{
|
||||
private sealed class DelegateGuestThreadBlockWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
private readonly Func<int> _resume;
|
||||
private readonly Func<bool> _tryWake;
|
||||
|
||||
public DelegateGuestThreadBlockWaiter(Func<int> resume, Func<bool> tryWake)
|
||||
{
|
||||
_resume = resume;
|
||||
_tryWake = tryWake;
|
||||
}
|
||||
|
||||
public int Resume() => _resume();
|
||||
|
||||
public bool TryWake() => _tryWake();
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentGuestThreadHandle;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentFiberAddress;
|
||||
|
||||
[ThreadStatic]
|
||||
private static string? _pendingBlockReason;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _pendingBlockContinuationValid;
|
||||
|
||||
[ThreadStatic]
|
||||
private static GuestCpuContinuation _pendingBlockContinuation;
|
||||
|
||||
[ThreadStatic]
|
||||
private static string? _pendingBlockWakeKey;
|
||||
|
||||
[ThreadStatic]
|
||||
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _pendingBlockDeadlineTimestamp;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _pendingEntryExit;
|
||||
|
||||
@@ -231,12 +186,6 @@ public static class GuestThreadExecution
|
||||
{
|
||||
var previous = _currentGuestThreadHandle;
|
||||
_currentGuestThreadHandle = threadHandle;
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitValue = 0;
|
||||
_pendingEntryExitReason = null;
|
||||
@@ -252,12 +201,6 @@ public static class GuestThreadExecution
|
||||
public static void RestoreGuestThread(ulong previousThreadHandle)
|
||||
{
|
||||
_currentGuestThreadHandle = previousThreadHandle;
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitValue = 0;
|
||||
_pendingEntryExitReason = null;
|
||||
@@ -281,123 +224,6 @@ public static class GuestThreadExecution
|
||||
_currentFiberAddress = previousFiberAddress;
|
||||
}
|
||||
|
||||
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
|
||||
|
||||
public static bool RequestCurrentThreadBlock(
|
||||
CpuContext? context,
|
||||
string reason,
|
||||
string? wakeKey = null,
|
||||
IGuestThreadBlockWaiter? waiter = null,
|
||||
long blockDeadlineTimestamp = 0)
|
||||
{
|
||||
if (!IsGuestThread)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||
_pendingBlockWaiter = waiter;
|
||||
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||
{
|
||||
_pendingBlockContinuation = continuation;
|
||||
_pendingBlockContinuationValid = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockContinuationValid = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compatibility bridge for exports that still describe blocked work as a
|
||||
// resume/wake delegate pair. New hot paths should provide an
|
||||
// IGuestThreadBlockWaiter directly to avoid allocating closures.
|
||||
public static bool RequestCurrentThreadBlock(
|
||||
CpuContext? context,
|
||||
string reason,
|
||||
string? wakeKey,
|
||||
Func<int> resumeHandler,
|
||||
Func<bool> wakeHandler,
|
||||
long blockDeadlineTimestamp = 0) =>
|
||||
RequestCurrentThreadBlock(
|
||||
context,
|
||||
reason,
|
||||
wakeKey,
|
||||
new DelegateGuestThreadBlockWaiter(resumeHandler, wakeHandler),
|
||||
blockDeadlineTimestamp);
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(
|
||||
out string reason,
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(
|
||||
out reason,
|
||||
out continuation,
|
||||
out hasContinuation,
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(
|
||||
out string reason,
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out IGuestThreadBlockWaiter? waiter)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(
|
||||
out reason,
|
||||
out continuation,
|
||||
out hasContinuation,
|
||||
out wakeKey,
|
||||
out waiter,
|
||||
out _);
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(
|
||||
out string reason,
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out IGuestThreadBlockWaiter? waiter,
|
||||
out long blockDeadlineTimestamp)
|
||||
{
|
||||
reason = _pendingBlockReason ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
{
|
||||
continuation = default;
|
||||
hasContinuation = false;
|
||||
wakeKey = string.Empty;
|
||||
waiter = null;
|
||||
blockDeadlineTimestamp = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
continuation = _pendingBlockContinuation;
|
||||
hasContinuation = _pendingBlockContinuationValid;
|
||||
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||
waiter = _pendingBlockWaiter;
|
||||
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
||||
{
|
||||
if (timeout <= TimeSpan.Zero)
|
||||
@@ -417,45 +243,6 @@ public static class GuestThreadExecution
|
||||
return now + Math.Max(1, ticks);
|
||||
}
|
||||
|
||||
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
|
||||
{
|
||||
if (!TryGetCurrentImportCallFrame(out var frame) ||
|
||||
frame.ReturnRip < 65536 ||
|
||||
frame.ResumeRsp == 0 ||
|
||||
frame.ReturnSlotAddress == 0)
|
||||
{
|
||||
continuation = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
continuation = new GuestCpuContinuation(
|
||||
frame.ReturnRip,
|
||||
frame.ResumeRsp,
|
||||
frame.ReturnSlotAddress,
|
||||
context.Rflags,
|
||||
context.FsBase,
|
||||
context.GsBase,
|
||||
0,
|
||||
context[CpuRegister.Rcx],
|
||||
context[CpuRegister.Rdx],
|
||||
context[CpuRegister.Rbx],
|
||||
context[CpuRegister.Rbp],
|
||||
context[CpuRegister.Rsi],
|
||||
context[CpuRegister.Rdi],
|
||||
context[CpuRegister.R8],
|
||||
context[CpuRegister.R9],
|
||||
context[CpuRegister.R10],
|
||||
context[CpuRegister.R11],
|
||||
context[CpuRegister.R12],
|
||||
context[CpuRegister.R13],
|
||||
context[CpuRegister.R14],
|
||||
context[CpuRegister.R15],
|
||||
context.FpuControlWord,
|
||||
context.Mxcsr,
|
||||
RestoreFullFpuState: false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void RequestCurrentEntryExit(string reason, int status)
|
||||
{
|
||||
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
|
||||
|
||||
@@ -1775,6 +1775,58 @@ public static partial class AgcExports
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// Single-register variant of the SET_SH_REG builders: the register rides
|
||||
// in rsi as a packed struct (low 16 bits = register offset, high dword =
|
||||
// byte offset of this dword within a multi-dword register write) and the
|
||||
// value in edx. Emits the same 3-dword SET_SH_REG packet the plural
|
||||
// sceAgcCbSetShRegistersDirect path produces per run. Hades calls this
|
||||
// ~1k times per boot; leaving it unresolved corrupted its DCB stream.
|
||||
[SysAbiExport(
|
||||
Nid = "pFLArOT53+w",
|
||||
ExportName = "sceAgcDcbSetShRegisterDirect",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbSetShRegisterDirect(CpuContext ctx)
|
||||
{
|
||||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var packedRegister = ctx[CpuRegister.Rsi];
|
||||
var value = (uint)ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
var offset = (uint)(packedRegister & 0xFFFFu) + (uint)((packedRegister >> 32) >> 2);
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 3, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(3, ItSetShReg, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, offset & 0xFFFFu) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 8, value))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
TraceAgc($"agc.dcb_set_sh_register_direct buf=0x{commandBufferAddress:X16} reg=0x{offset:X4} value=0x{value:X8}");
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// Size probe for the wait-on-address writer below: same argument prefix
|
||||
// minus the command buffer, returns the byte size the writer will emit so
|
||||
// the game can reserve DCB space (7 dwords for a standard WAIT_REG_MEM,
|
||||
// 6/9 for the 32/64-bit polled-NOP forms).
|
||||
[SysAbiExport(
|
||||
Nid = "43WJ08sSugE",
|
||||
ExportName = "sceAgcDcbWaitOnAddressGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbWaitOnAddressGetSize(CpuContext ctx)
|
||||
{
|
||||
var size = (uint)(ctx[CpuRegister.Rdi] & 0xFF);
|
||||
var operation = (uint)(ctx[CpuRegister.Rdx] & 0xFF);
|
||||
var packetDwords = operation is 2 or 3 ? 7u : size == 0 ? 6u : 9u;
|
||||
ctx[CpuRegister.Rax] = packetDwords * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VmW0Tdpy420",
|
||||
ExportName = "sceAgcDcbWaitRegMem",
|
||||
@@ -2624,6 +2676,32 @@ public static partial class AgcExports
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Hands the game the driver-side context id its GPU event-queue packets
|
||||
// reference. We key events purely on (equeue, ident, filter), so a single
|
||||
// stable id satisfies the contract; the game only checks the call
|
||||
// succeeded and threads the id back through later driver calls.
|
||||
[SysAbiExport(
|
||||
Nid = "Zw7uUVPulbw",
|
||||
ExportName = "sceAgcDriverGetEqContextId",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgcDriver")]
|
||||
public static int DriverGetEqContextId(CpuContext ctx)
|
||||
{
|
||||
var contextIdAddress = ctx[CpuRegister.Rdi];
|
||||
if (contextIdAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, contextIdAddress, 1))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAgc($"agc.driver_get_eq_context_id out=0x{contextIdAddress:X16} -> 1");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DL2RXaXOy88",
|
||||
ExportName = "sceAgcDriverDeleteEqEvent",
|
||||
|
||||
@@ -228,6 +228,17 @@ internal static partial class MetalVideoPresenter
|
||||
|
||||
private static void RunWindowLoop()
|
||||
{
|
||||
// The Metal Performance HUD attaches to a process at Metal init;
|
||||
// toggling developerHUDProperties on a live layer is inert when the
|
||||
// HUD was never armed. SHARPEMU_METAL_HUD=1 arms it the documented way
|
||||
// (setenv lands in the native environ before the device initializes),
|
||||
// showing the HUD from launch; Cmd+F1 then toggles it per-layer.
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_METAL_HUD"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("MTL_HUD_ENABLED", "1");
|
||||
_metalHudVisible = true;
|
||||
}
|
||||
|
||||
MetalNative.EnsureFrameworksLoaded();
|
||||
|
||||
_device = MetalNative.MTLCreateSystemDefaultDevice();
|
||||
@@ -506,6 +517,14 @@ internal static partial class MetalVideoPresenter
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Metal Performance HUD {(_metalHudVisible ? "shown" : "hidden")} (Cmd+F1).");
|
||||
if (_metalHudVisible &&
|
||||
!string.Equals(Environment.GetEnvironmentVariable("MTL_HUD_ENABLED"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
// The per-layer property only takes effect when the HUD attached at
|
||||
// Metal init; without that the toggle is a no-op on some macOS builds.
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] If the HUD does not appear, relaunch with SHARPEMU_METAL_HUD=1 (arms Apple's HUD at startup).");
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe nint CreateRenderTimerTarget()
|
||||
|
||||
@@ -43,4 +43,25 @@ public static class ImeExports
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// No hardware keyboard is ever connected; zero the caller's info struct so
|
||||
// it reads as "not connected" rather than uninitialized stack.
|
||||
[SysAbiExport(
|
||||
Nid = "VkqLPArfFdc",
|
||||
ExportName = "sceImeKeyboardGetInfo",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeKeyboardGetInfo(CpuContext ctx)
|
||||
{
|
||||
var infoAddress = ctx[CpuRegister.Rsi] != 0 ? ctx[CpuRegister.Rsi] : ctx[CpuRegister.Rdi];
|
||||
if (infoAddress != 0)
|
||||
{
|
||||
Span<byte> info = stackalloc byte[0x40];
|
||||
info.Clear();
|
||||
_ = ctx.Memory.TryWrite(infoAddress, info);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelEventFlagCompatExports
|
||||
{
|
||||
private const int MaxEventFlagNameLength = 31;
|
||||
private const int HostWaitPumpMilliseconds = 1;
|
||||
private const uint AttrThreadFifo = 0x01;
|
||||
private const uint AttrThreadPriority = 0x02;
|
||||
private const uint AttrSingle = 0x10;
|
||||
@@ -25,8 +23,7 @@ public static class KernelEventFlagCompatExports
|
||||
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
|
||||
private static long _nextEventFlagHandle = 1;
|
||||
|
||||
// Cached once: gating every call site avoids building the interpolated
|
||||
// trace string (and FormatFrameChain/FormatGuestWaitObject) when disabled.
|
||||
// Cached once: gating every call site avoids building the interpolated trace string when disabled.
|
||||
private static readonly bool _traceEventFlag = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal);
|
||||
|
||||
@@ -128,11 +125,11 @@ public static class KernelEventFlagCompatExports
|
||||
lock (state.Gate)
|
||||
{
|
||||
state.Bits |= pattern;
|
||||
// Wake threads parked in-place on the gate; each re-checks its pattern.
|
||||
Monitor.PulseAll(state.Gate);
|
||||
if (_traceEventFlag) TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -229,134 +226,67 @@ public static class KernelEventFlagCompatExports
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.Enter(state.Gate);
|
||||
try
|
||||
// A zero-microsecond timeout degrades to an instant poll because the
|
||||
// deadline is already in the past.
|
||||
var hostDeadlineMs = timeoutAddress != 0
|
||||
? Environment.TickCount64 + (timeoutUsec == 0
|
||||
? 0L
|
||||
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
|
||||
: long.MaxValue;
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||
{
|
||||
if (_traceEventFlag) TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, immediateWaitResult);
|
||||
}
|
||||
|
||||
// Timed waits block on a deadline instead of returning TIMED_OUT
|
||||
// immediately; a zero-microsecond timeout still degrades to an
|
||||
// instant poll because the deadline is already in the past.
|
||||
var deadline = timeoutAddress != 0
|
||||
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
|
||||
: 0;
|
||||
var hostDeadlineMs = timeoutAddress != 0
|
||||
? Environment.TickCount64 + (timeoutUsec == 0
|
||||
? 0L
|
||||
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
|
||||
: long.MaxValue;
|
||||
|
||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||
var managedThread = Environment.CurrentManagedThreadId;
|
||||
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var satisfied = false;
|
||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEventFlag",
|
||||
GetEventFlagWakeKey(handle),
|
||||
() =>
|
||||
// In-place block on the flag gate. Monitor.Wait releases the gate
|
||||
// and parks atomically, so a concurrent SetEventFlag's PulseAll
|
||||
// cannot be lost between the satisfy check and the park. On wake
|
||||
// the predicate is re-evaluated (Set semantics on FreeBSD-style
|
||||
// event flags: OR/AND over the bit pattern, optional clear).
|
||||
state.WaitingThreads++;
|
||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{guestThreadHandle:X16} ret=0x{returnRip:X16}");
|
||||
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitEventFlag");
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (satisfied)
|
||||
if (GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
return (int)blockedWaitResult;
|
||||
if (timeoutAddress != 0) _ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
// Deadline expiry: report timeout with the current bits.
|
||||
if (timeoutAddress != 0)
|
||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var wokeResult))
|
||||
{
|
||||
if (timeoutAddress != 0) _ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, wokeResult);
|
||||
}
|
||||
|
||||
var remaining = hostDeadlineMs - Environment.TickCount64;
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
},
|
||||
() =>
|
||||
{
|
||||
if (!TryPrepareBlockedWait(
|
||||
ctx,
|
||||
state,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress,
|
||||
out var preparedResult))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
blockedWaitResult = preparedResult;
|
||||
satisfied = true;
|
||||
return true;
|
||||
},
|
||||
deadline);
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||
if (!requestedBlock)
|
||||
{
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (scheduler is null)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
state.WaitingThreads++;
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
var releaseWaiter = true;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Monitor.Exit(state.Gate);
|
||||
try
|
||||
{
|
||||
scheduler.Pump(ctx, "sceKernelWaitEventFlag");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Enter(state.Gate);
|
||||
}
|
||||
|
||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var pumpedWaitResult))
|
||||
{
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
releaseWaiter = false;
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, pumpedWaitResult);
|
||||
}
|
||||
|
||||
var remaining = hostDeadlineMs - Environment.TickCount64;
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
{
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
releaseWaiter = false;
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
Monitor.Wait(state.Gate, (int)Math.Min(remaining, HostWaitPumpMilliseconds));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (releaseWaiter)
|
||||
{
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
}
|
||||
GuestThreadBlocking.Checkpoint(guestThreadHandle, state.Gate);
|
||||
_ = Monitor.Wait(state.Gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
state.WaitingThreads++;
|
||||
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Exit(state.Gate);
|
||||
finally
|
||||
{
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,41 +385,6 @@ public static class KernelEventFlagCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryPrepareBlockedWait(
|
||||
CpuContext ctx,
|
||||
EventFlagState state,
|
||||
ulong pattern,
|
||||
uint waitMode,
|
||||
ulong resultAddress,
|
||||
out OrbisGen2Result result)
|
||||
{
|
||||
lock (state.Gate)
|
||||
{
|
||||
result = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||
{
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyClearMode(state, pattern, waitMode);
|
||||
}
|
||||
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
if (_traceEventFlag) TraceEventFlag(
|
||||
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||
$"event_flag:0x{handle:X16}";
|
||||
|
||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||
|
||||
@@ -519,19 +414,6 @@ public static class KernelEventFlagCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
@@ -584,95 +466,4 @@ public static class KernelEventFlagCompatExports
|
||||
? frame.ReturnRip
|
||||
: 0UL;
|
||||
|
||||
private static string FormatFrameChain(CpuContext ctx)
|
||||
{
|
||||
Span<ulong> returns = stackalloc ulong[4];
|
||||
var count = 0;
|
||||
var frame = ctx[CpuRegister.Rbp];
|
||||
for (var index = 0; index < returns.Length && frame != 0; index++)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(frame, out var nextFrame) ||
|
||||
!ctx.TryReadUInt64(frame + sizeof(ulong), out var returnAddress))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
returns[count++] = returnAddress;
|
||||
if (nextFrame <= frame)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
frame = nextFrame;
|
||||
}
|
||||
|
||||
return count switch
|
||||
{
|
||||
0 => "none",
|
||||
1 => $"0x{returns[0]:X16}",
|
||||
2 => $"0x{returns[0]:X16},0x{returns[1]:X16}",
|
||||
3 => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16}",
|
||||
_ => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16},0x{returns[3]:X16}",
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatGuestWaitObject(CpuContext ctx)
|
||||
{
|
||||
var r12 = ctx[CpuRegister.R12];
|
||||
var r13 = ctx[CpuRegister.R13];
|
||||
var objectAddress = r12 != 0
|
||||
? r12
|
||||
: r13 >= 0xA8
|
||||
? r13 - 0xA8
|
||||
: 0;
|
||||
|
||||
var builder = new StringBuilder(256);
|
||||
builder.Append($"r12=0x{r12:X16} r13=0x{r13:X16}");
|
||||
if (objectAddress == 0)
|
||||
{
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
builder.Append($" obj=0x{objectAddress:X16}");
|
||||
AppendUInt32(builder, ctx, objectAddress + 0x58, "o58");
|
||||
AppendUInt32(builder, ctx, objectAddress + 0x5C, "o5C");
|
||||
AppendUInt64(builder, ctx, objectAddress + 0x60, "o60");
|
||||
AppendByte(builder, ctx, objectAddress + 0x6C, "state6C");
|
||||
AppendByte(builder, ctx, objectAddress + 0x6D, "o6D");
|
||||
AppendByte(builder, ctx, objectAddress + 0xA0, "waitA0");
|
||||
AppendByte(builder, ctx, objectAddress + 0xA1, "stateA1");
|
||||
AppendByte(builder, ctx, objectAddress + 0xA2, "oA2");
|
||||
AppendUInt64(builder, ctx, objectAddress + 0xA8, "eventA8");
|
||||
if (r13 != 0)
|
||||
{
|
||||
AppendUInt64(builder, ctx, r13, "r13_0");
|
||||
AppendUInt64(builder, ctx, r13 + 8, "r13_8");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (TryReadByte(ctx, address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X2}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (TryReadUInt32(ctx, address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (TryReadUInt64(ctx, address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X16}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,19 +93,6 @@ public static class KernelEventQueueCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EqueueWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required ulong Handle { get; init; }
|
||||
public required ulong EventsAddress { get; init; }
|
||||
public required int EventCapacity { get; init; }
|
||||
public required ulong OutCountAddress { get; init; }
|
||||
|
||||
public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress);
|
||||
|
||||
public bool TryWake() => HasPendingEvents(Handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "D0OdFMjp46I",
|
||||
ExportName = "sceKernelCreateEqueue",
|
||||
@@ -149,10 +136,10 @@ public static class KernelEventQueueCompatExports
|
||||
_eventQueues.Remove(handle);
|
||||
_pendingEvents.Remove(handle);
|
||||
_registeredEvents.Remove(handle);
|
||||
// Wake any thread parked on this queue so it observes the deletion.
|
||||
Monitor.PulseAll(_eventQueueGate);
|
||||
}
|
||||
|
||||
_wakeKeys.TryRemove(handle, out _);
|
||||
|
||||
TraceEventQueue(ctx, "delete", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -412,59 +399,81 @@ public static class KernelEventQueueCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (timeoutAddress == 0 &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
GetEventQueueWakeKey(handle),
|
||||
new EqueueWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
Handle = handle,
|
||||
EventsAddress = eventsAddress,
|
||||
EventCapacity = eventCapacity,
|
||||
OutCountAddress = outCountAddress,
|
||||
}))
|
||||
// No events ready: block this host thread in place on the queue gate.
|
||||
// Monitor.Wait releases the gate and parks atomically, so an
|
||||
// EnqueueEvent/TriggerDisplayEvent PulseAll issued the instant after
|
||||
// the emptiness check cannot be lost. kqueue/kevent semantics: sleep
|
||||
// until an event matching a registration is delivered or the timeout
|
||||
// (usec, infinite when the arg pointer is null) lapses; a zero timeout
|
||||
// degrades to an instant poll.
|
||||
long deadline;
|
||||
if (timeoutAddress == 0)
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-block", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
deadline = long.MaxValue;
|
||||
}
|
||||
else if (timeoutUsec == 0)
|
||||
{
|
||||
deadline = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
deadline = Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw))
|
||||
TraceEventQueue(ctx, "wait-block", handle);
|
||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitEqueue");
|
||||
try
|
||||
{
|
||||
var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL;
|
||||
var deadline = Environment.TickCount64 +
|
||||
Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue));
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
while (!HasPendingEvents(handle))
|
||||
while (true)
|
||||
{
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (remaining <= 0)
|
||||
if ((_pendingEvents.TryGetValue(handle, out var queue) && queue.Count != 0) ||
|
||||
!_eventQueues.Contains(handle) ||
|
||||
GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100));
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var slice = timeoutAddress == 0
|
||||
? GuestThreadBlocking.WaitSliceMilliseconds
|
||||
: (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
GuestThreadBlocking.Checkpoint(guestThreadHandle, _eventQueueGate);
|
||||
_ = Monitor.Wait(_eventQueueGate, slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
|
||||
}
|
||||
|
||||
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (deliveredCount > 0)
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-timed-deliver", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
if (deliveredCount > 0)
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-deliver", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-timeout", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
// Reached only on queue deletion or teardown; the guest sees zero events.
|
||||
TraceEventQueue(ctx, "wait", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -798,32 +807,6 @@ public static class KernelEventQueueCompatExports
|
||||
return triggered;
|
||||
}
|
||||
|
||||
private static int ResumeWaitEqueue(
|
||||
CpuContext ctx,
|
||||
ulong handle,
|
||||
ulong eventsAddress,
|
||||
int eventCapacity,
|
||||
ulong outCountAddress)
|
||||
{
|
||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return deliveredCount > 0
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
private static bool HasPendingEvents(ulong handle)
|
||||
{
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void QueueOrUpdateEvent(
|
||||
KernelEventDeque queue,
|
||||
KernelQueuedEvent queuedEvent)
|
||||
@@ -841,16 +824,16 @@ public static class KernelEventQueueCompatExports
|
||||
};
|
||||
}
|
||||
|
||||
// Wake keys are formatted once per handle: WakeEventQueue runs on every event
|
||||
// enqueue (vblank/flip edges included), so formatting there is steady string churn.
|
||||
private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
|
||||
|
||||
private static string GetEventQueueWakeKey(ulong handle) =>
|
||||
_wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
|
||||
|
||||
// Wake threads parked in-place on the queue gate; each re-checks for a
|
||||
// matching pending event. The handle is unused (all queues share one gate)
|
||||
// but kept in the signature so call sites read intent-fully.
|
||||
private static void WakeEventQueue(ulong handle)
|
||||
{
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle));
|
||||
_ = handle;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
Monitor.PulseAll(_eventQueueGate);
|
||||
}
|
||||
}
|
||||
|
||||
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
|
||||
|
||||
@@ -1819,6 +1819,15 @@ public static partial class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// POSIX alias; same Orbis result convention as the other posix-named
|
||||
// file exports in this module (mkdir/rmdir/open).
|
||||
[SysAbiExport(
|
||||
Nid = "VAzswvTOCzI",
|
||||
ExportName = "unlink",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixUnlink(CpuContext ctx) => KernelUnlink(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AUXVxWeJU-A",
|
||||
ExportName = "sceKernelUnlink",
|
||||
|
||||
@@ -37,55 +37,35 @@ public static class KernelPthreadCompatExports
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
||||
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
||||
private static long _nextSynchronizationWaiterId;
|
||||
|
||||
// Blocking model: waiters block their own host thread in place via
|
||||
// Monitor.Wait on the state object (mutexes) or SyncRoot (condvars).
|
||||
// Block-and-wake is therefore atomic — no waiter queues, wake keys, or
|
||||
// continuation hand-offs, and no lost-wakeup window between a thread
|
||||
// deciding to block and registering as blocked.
|
||||
private sealed class PthreadMutexState
|
||||
{
|
||||
public ulong OwnerThreadId { get; set; }
|
||||
public int RecursionCount { get; set; }
|
||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||
public int Protocol { get; set; }
|
||||
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class PthreadMutexWaiter
|
||||
{
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required string WakeKey { get; init; }
|
||||
public required bool Cooperative { get; init; }
|
||||
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
||||
public int Granted;
|
||||
// Threads currently blocked in PthreadMutexLockCore; destroy reports BUSY while nonzero.
|
||||
public int WaiterCount { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PthreadCondState
|
||||
{
|
||||
public object SyncRoot { get; } = new();
|
||||
public LinkedList<PthreadCondWaiter> WaiterQueue { get; } = new();
|
||||
public ulong SignalEpoch { get; set; }
|
||||
public int Waiters { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PthreadCondWaiter
|
||||
{
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required PthreadMutexState MutexState { get; init; }
|
||||
public required string WakeKey { get; init; }
|
||||
public required bool Cooperative { get; init; }
|
||||
public bool PosixErrors { get; init; }
|
||||
public LinkedListNode<PthreadCondWaiter>? Node { get; set; }
|
||||
public PthreadMutexWaiter? MutexWaiter { get; set; }
|
||||
public Timer? TimeoutTimer { get; set; }
|
||||
// 0 = waiting, 1 = signaled, 2 = timed out.
|
||||
public int CompletionState { get; set; }
|
||||
// Signals produced but not yet consumed by a waiter. A signal only
|
||||
// increments this when an unserved waiter exists (POSIX: signaling an
|
||||
// empty condvar is a no-op), so stale signals cannot accumulate.
|
||||
public int SignalsPending { get; set; }
|
||||
}
|
||||
|
||||
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
||||
|
||||
static KernelPthreadCompatExports()
|
||||
{
|
||||
RunSynchronizationSelfChecks();
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aI+OeCz8xrQ",
|
||||
ExportName = "scePthreadSelf",
|
||||
@@ -139,6 +119,13 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "9vyP6Z7bqzc",
|
||||
ExportName = "pthread_rename_np",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadRenameNp(CpuContext ctx) => PthreadRename(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "GBUY7ywdULE",
|
||||
ExportName = "scePthreadRename",
|
||||
@@ -623,7 +610,7 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
lock (state)
|
||||
{
|
||||
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
||||
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.WaiterCount != 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
@@ -653,10 +640,6 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
var canCooperativelyBlock = !tryOnly &&
|
||||
GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
||||
PthreadMutexWaiter? waiter = null;
|
||||
lock (state)
|
||||
{
|
||||
if (state.OwnerThreadId == currentThreadId)
|
||||
@@ -696,14 +679,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
// pthread_mutex_trylock succeeds whenever the mutex is not currently
|
||||
// held; unlike the blocking lock it does not queue behind waiters
|
||||
// (POSIX gives it no fairness obligation). Gating trylock on an empty
|
||||
// wait queue is wrong and, worse, lets a single stale/undrainable
|
||||
// waiter wedge a spin-on-trylock loop forever even though the mutex
|
||||
// is free (owner==0). The blocking lock still honours FIFO so real
|
||||
// blocked waiters are not starved by a barging locker.
|
||||
if (state.OwnerThreadId == 0 && (tryOnly || state.Waiters.Count == 0))
|
||||
if (state.OwnerThreadId == 0)
|
||||
{
|
||||
state.OwnerThreadId = currentThreadId;
|
||||
state.RecursionCount = 1;
|
||||
@@ -717,24 +693,39 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
||||
}
|
||||
|
||||
if (canCooperativelyBlock && waiter is not null &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
waiter.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryGrantBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
{
|
||||
// Contended: block this host thread in place until the owner
|
||||
// releases. Monitor.Wait atomically releases the state lock and
|
||||
// parks, so an unlock's PulseAll cannot be missed. Waits are
|
||||
// sliced only so teardown can unwind parked threads.
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_mutex_lock");
|
||||
state.WaiterCount++;
|
||||
try
|
||||
{
|
||||
while (state.OwnerThreadId != 0)
|
||||
{
|
||||
if (GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-shutdown", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
}
|
||||
|
||||
GuestThreadBlocking.Checkpoint(currentThreadId, state);
|
||||
_ = Monitor.Wait(state, GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
}
|
||||
|
||||
state.OwnerThreadId = currentThreadId;
|
||||
state.RecursionCount = 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
state.WaiterCount--;
|
||||
GuestThreadBlocking.NoteUnblocked(currentThreadId);
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var hostResult = WaitForHostMutexLock(state, waiter!);
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, hostResult);
|
||||
return hostResult;
|
||||
}
|
||||
|
||||
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
|
||||
@@ -751,7 +742,6 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
string? nextWakeKey = null;
|
||||
lock (state)
|
||||
{
|
||||
if (state.RecursionCount <= 0)
|
||||
@@ -770,18 +760,13 @@ public static class KernelPthreadCompatExports
|
||||
if (state.RecursionCount == 0)
|
||||
{
|
||||
state.OwnerThreadId = 0;
|
||||
nextWakeKey = state.Waiters.First?.Value.Cooperative == true
|
||||
? state.Waiters.First.Value.WakeKey
|
||||
: null;
|
||||
Monitor.PulseAll(state);
|
||||
if (state.WaiterCount != 0)
|
||||
{
|
||||
Monitor.PulseAll(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextWakeKey is not null)
|
||||
{
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1);
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -1198,7 +1183,7 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
if (state.WaiterQueue.Count != 0)
|
||||
if (state.Waiters != 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
@@ -1263,94 +1248,80 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
var cooperative = GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
||||
var waiter = new PthreadCondWaiter
|
||||
{
|
||||
ThreadId = currentThreadId,
|
||||
MutexState = mutexState,
|
||||
Cooperative = cooperative,
|
||||
PosixErrors = posixErrors,
|
||||
WakeKey = cooperative
|
||||
? $"pthread_cond_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
||||
: string.Empty,
|
||||
};
|
||||
|
||||
var signaled = false;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
waiter.Node = state.WaiterQueue.AddLast(waiter);
|
||||
state.Waiters++;
|
||||
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
|
||||
// POSIX atomicity: we are registered as a waiter (Waiters++ under
|
||||
// SyncRoot) before the mutex is released, so a signal issued the
|
||||
// instant the mutex unlocks already counts us and lands in
|
||||
// SignalsPending — checked before the first Monitor.Wait. No
|
||||
// window exists where a wake can be lost.
|
||||
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
RemoveCondWaiterLocked(state, waiter);
|
||||
state.Waiters--;
|
||||
TracePthreadCond("wait-unlock-fail", condAddress, mutexAddress, state, timed, unlockResult);
|
||||
return unlockResult;
|
||||
}
|
||||
|
||||
if (cooperative && timed)
|
||||
{
|
||||
waiter.TimeoutTimer = new Timer(
|
||||
static callbackState =>
|
||||
{
|
||||
var (condState, condWaiter) = ((PthreadCondState, PthreadCondWaiter))callbackState!;
|
||||
CompleteCondWaiter(condState, condWaiter, timedOut: true);
|
||||
},
|
||||
(state, waiter),
|
||||
GetCondWaitTimeout(timeoutUsec),
|
||||
Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
}
|
||||
|
||||
if (cooperative &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||
waiter.WakeKey,
|
||||
() => CompleteBlockedCondWait(ctx, condAddress, mutexAddress, state, waiter),
|
||||
() => TryGrantCondWaiterMutex(waiter)))
|
||||
{
|
||||
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Non-guest callers have no resumable CPU continuation. Park only
|
||||
// those host-side compatibility callers, preserving the same FIFO
|
||||
// mutex reacquisition rules as cooperative guest waiters.
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
var deadline = timed
|
||||
? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec))
|
||||
: long.MaxValue;
|
||||
while (waiter.CompletionState == 0)
|
||||
GuestThreadBlocking.NoteBlocked(currentThreadId, timed ? "pthread_cond_timedwait" : "pthread_cond_wait");
|
||||
try
|
||||
{
|
||||
if (!timed)
|
||||
while (state.SignalsPending == 0 && !GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
Monitor.Wait(state.SyncRoot);
|
||||
continue;
|
||||
}
|
||||
var remaining = timed
|
||||
? GetRemainingTimeout(deadline)
|
||||
: TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
if (timed && remaining <= TimeSpan.Zero)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var remaining = GetRemainingTimeout(deadline);
|
||||
if (remaining <= TimeSpan.Zero || !Monitor.Wait(state.SyncRoot, remaining))
|
||||
{
|
||||
CompleteCondWaiterLocked(state, waiter, timedOut: true);
|
||||
break;
|
||||
if (remaining > TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds))
|
||||
{
|
||||
remaining = TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
}
|
||||
|
||||
GuestThreadBlocking.Checkpoint(currentThreadId, state.SyncRoot);
|
||||
_ = Monitor.Wait(state.SyncRoot, remaining);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadBlocking.NoteUnblocked(currentThreadId);
|
||||
}
|
||||
|
||||
if (state.SignalsPending > 0)
|
||||
{
|
||||
state.SignalsPending--;
|
||||
signaled = true;
|
||||
}
|
||||
|
||||
state.Waiters--;
|
||||
if (state.SignalsPending > state.Waiters)
|
||||
{
|
||||
// A timed-out waiter left a signal unconsumed with nobody
|
||||
// remaining to take it; drop it so a future wait does not
|
||||
// observe a phantom wake (signals on an empty condvar are
|
||||
// no-ops on real hardware).
|
||||
state.SignalsPending = state.Waiters;
|
||||
}
|
||||
}
|
||||
|
||||
if (waiter.MutexWaiter is null)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
// POSIX guarantees the mutex is re-acquired on every return path,
|
||||
// signaled or timed out. Blocks in place like any other locker.
|
||||
_ = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
||||
|
||||
_ = WaitForHostMutexLock(mutexState, waiter.MutexWaiter);
|
||||
var waitResult = waiter.CompletionState == 2
|
||||
? CondTimedOutResult(waiter)
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
TracePthreadCond(waiter.CompletionState == 2 ? "wait-exit-timeout" : "wait-exit", condAddress, mutexAddress, state, timed, waitResult);
|
||||
var waitResult = signaled || !timed
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: CondTimedOutResult(posixErrors);
|
||||
TracePthreadCond(signaled ? "wait-exit" : "wait-exit-timeout", condAddress, mutexAddress, state, timed, waitResult);
|
||||
return waitResult;
|
||||
}
|
||||
|
||||
@@ -1366,305 +1337,36 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
List<PthreadCondWaiter>? completedWaiters = null;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
state.SignalEpoch++;
|
||||
for (var node = state.WaiterQueue.First; node is not null;)
|
||||
if (broadcast)
|
||||
{
|
||||
var next = node.Next;
|
||||
var waiter = node.Value;
|
||||
if (waiter.CompletionState == 0 && CompleteCondWaiterLocked(state, waiter, timedOut: false))
|
||||
{
|
||||
(completedWaiters ??= new List<PthreadCondWaiter>()).Add(waiter);
|
||||
if (!broadcast)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
state.SignalsPending = state.Waiters;
|
||||
}
|
||||
else if (state.SignalsPending < state.Waiters)
|
||||
{
|
||||
// Only count a signal an unserved waiter can consume; signaling
|
||||
// an empty condvar is a no-op per POSIX.
|
||||
state.SignalsPending++;
|
||||
}
|
||||
|
||||
node = next;
|
||||
if (state.Waiters != 0)
|
||||
{
|
||||
Monitor.PulseAll(state.SyncRoot);
|
||||
}
|
||||
|
||||
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
if (completedWaiters is not null)
|
||||
{
|
||||
foreach (var waiter in completedWaiters)
|
||||
{
|
||||
WakeCooperativeWaiter(waiter);
|
||||
}
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static PthreadMutexWaiter EnqueueMutexWaiterLocked(
|
||||
PthreadMutexState state,
|
||||
ulong threadId,
|
||||
bool cooperative,
|
||||
string? wakeKey = null)
|
||||
{
|
||||
// A guest thread can have at most one pending acquisition on a mutex —
|
||||
// it is either running or blocked on exactly one wait. If a waiter for
|
||||
// this thread is still queued when it comes back for a fresh
|
||||
// acquisition, that entry is a stale leftover the thread abandoned
|
||||
// (most often a cond_timedwait timeout whose re-acquire hand-off was
|
||||
// lost). Stale entries clog the FIFO head with waiters no thread is
|
||||
// blocked on, so the unlock hand-off wakes a dead wake-key and the
|
||||
// mutex wedges permanently (observed deadlocking Hades: several
|
||||
// re-acquire waiters from one thread piled ahead of a live locker).
|
||||
// Prune any prior entry for this thread before enqueueing the new one.
|
||||
if (threadId != 0)
|
||||
{
|
||||
for (var node = state.Waiters.First; node is not null;)
|
||||
{
|
||||
var next = node.Next;
|
||||
if (node.Value.ThreadId == threadId)
|
||||
{
|
||||
state.Waiters.Remove(node);
|
||||
node.Value.Node = null;
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
|
||||
var waiter = new PthreadMutexWaiter
|
||||
{
|
||||
ThreadId = threadId,
|
||||
Cooperative = cooperative,
|
||||
WakeKey = cooperative
|
||||
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
||||
: string.Empty,
|
||||
};
|
||||
waiter.Node = state.Waiters.AddLast(waiter);
|
||||
return waiter;
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void RunSynchronizationSelfChecks()
|
||||
{
|
||||
var mutex = new PthreadMutexState();
|
||||
PthreadMutexWaiter first;
|
||||
PthreadMutexWaiter second;
|
||||
lock (mutex)
|
||||
{
|
||||
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
||||
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
||||
Debug.Assert(!TryGrantMutexWaiterLocked(mutex, second), "A mutex waiter bypassed FIFO order.");
|
||||
Debug.Assert(TryGrantMutexWaiterLocked(mutex, first), "The FIFO mutex head was not granted.");
|
||||
Debug.Assert(mutex.OwnerThreadId == first.ThreadId && mutex.RecursionCount == 1, "Mutex ownership was not transferred atomically.");
|
||||
mutex.OwnerThreadId = 0;
|
||||
mutex.RecursionCount = 0;
|
||||
Debug.Assert(TryGrantMutexWaiterLocked(mutex, second), "The second mutex waiter was not granted after release.");
|
||||
}
|
||||
|
||||
var cond = new PthreadCondState();
|
||||
var condMutex = new PthreadMutexState();
|
||||
var condWaiter = new PthreadCondWaiter
|
||||
{
|
||||
ThreadId = 0x303,
|
||||
MutexState = condMutex,
|
||||
WakeKey = string.Empty,
|
||||
Cooperative = false,
|
||||
};
|
||||
lock (cond.SyncRoot)
|
||||
{
|
||||
condWaiter.Node = cond.WaiterQueue.AddLast(condWaiter);
|
||||
cond.Waiters++;
|
||||
Debug.Assert(CompleteCondWaiterLocked(cond, condWaiter, timedOut: false), "A condition waiter was not completed.");
|
||||
Debug.Assert(cond.WaiterQueue.Count == 0 && cond.Waiters == 0 && condWaiter.MutexWaiter is not null, "Condition completion did not atomically queue mutex reacquisition.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGrantMutexWaiterLocked(PthreadMutexState state, PthreadMutexWaiter waiter)
|
||||
{
|
||||
if (Volatile.Read(ref waiter.Granted) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state.OwnerThreadId != 0 ||
|
||||
waiter.Node is null ||
|
||||
!ReferenceEquals(state.Waiters.First, waiter.Node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
state.Waiters.Remove(waiter.Node);
|
||||
waiter.Node = null;
|
||||
state.OwnerThreadId = waiter.ThreadId;
|
||||
state.RecursionCount = 1;
|
||||
Volatile.Write(ref waiter.Granted, 1);
|
||||
Monitor.PulseAll(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
|
||||
{
|
||||
lock (state)
|
||||
{
|
||||
while (!TryGrantMutexWaiterLocked(state, waiter))
|
||||
{
|
||||
Monitor.Wait(state);
|
||||
}
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryGrantBlockedMutexLock(
|
||||
CpuContext ctx,
|
||||
ulong mutexAddress,
|
||||
ulong resolvedAddress,
|
||||
PthreadMutexState state,
|
||||
PthreadMutexWaiter waiter)
|
||||
{
|
||||
var granted = false;
|
||||
lock (state)
|
||||
{
|
||||
granted = TryGrantMutexWaiterLocked(state, waiter);
|
||||
}
|
||||
|
||||
TracePthreadMutex(
|
||||
ctx,
|
||||
granted ? "lock-reserve" : "lock-reserve-busy",
|
||||
mutexAddress,
|
||||
resolvedAddress,
|
||||
state,
|
||||
waiter.ThreadId,
|
||||
granted ? (int)OrbisGen2Result.ORBIS_GEN2_OK : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return granted;
|
||||
}
|
||||
|
||||
private static int CompleteBlockedMutexLock(
|
||||
CpuContext ctx,
|
||||
ulong mutexAddress,
|
||||
ulong resolvedAddress,
|
||||
PthreadMutexState state,
|
||||
PthreadMutexWaiter waiter)
|
||||
{
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
if (Volatile.Read(ref waiter.Granted) == 1)
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "lock-resume-ungranted", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
private static bool CompleteCondWaiterLocked(
|
||||
PthreadCondState state,
|
||||
PthreadCondWaiter waiter,
|
||||
bool timedOut)
|
||||
{
|
||||
if (waiter.CompletionState != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
waiter.CompletionState = timedOut ? 2 : 1;
|
||||
RemoveCondWaiterLocked(state, waiter);
|
||||
waiter.TimeoutTimer?.Dispose();
|
||||
waiter.TimeoutTimer = null;
|
||||
|
||||
lock (waiter.MutexState)
|
||||
{
|
||||
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
||||
waiter.MutexState,
|
||||
waiter.ThreadId,
|
||||
waiter.Cooperative,
|
||||
waiter.WakeKey);
|
||||
}
|
||||
|
||||
Monitor.PulseAll(state.SyncRoot);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CompleteCondWaiter(
|
||||
PthreadCondState state,
|
||||
PthreadCondWaiter waiter,
|
||||
bool timedOut)
|
||||
{
|
||||
var completed = false;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
completed = CompleteCondWaiterLocked(state, waiter, timedOut);
|
||||
}
|
||||
|
||||
if (completed)
|
||||
{
|
||||
WakeCooperativeWaiter(waiter);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveCondWaiterLocked(PthreadCondState state, PthreadCondWaiter waiter)
|
||||
{
|
||||
if (waiter.Node is not null)
|
||||
{
|
||||
state.WaiterQueue.Remove(waiter.Node);
|
||||
waiter.Node = null;
|
||||
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGrantCondWaiterMutex(PthreadCondWaiter waiter)
|
||||
{
|
||||
var mutexWaiter = waiter.MutexWaiter;
|
||||
if (waiter.CompletionState == 0 || mutexWaiter is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (waiter.MutexState)
|
||||
{
|
||||
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
||||
}
|
||||
}
|
||||
|
||||
private static int CompleteBlockedCondWait(
|
||||
CpuContext ctx,
|
||||
ulong condAddress,
|
||||
ulong mutexAddress,
|
||||
PthreadCondState state,
|
||||
PthreadCondWaiter waiter)
|
||||
{
|
||||
waiter.TimeoutTimer?.Dispose();
|
||||
waiter.TimeoutTimer = null;
|
||||
var result = waiter.MutexWaiter is not null &&
|
||||
Volatile.Read(ref waiter.MutexWaiter.Granted) == 1
|
||||
? (waiter.CompletionState == 2
|
||||
? CondTimedOutResult(waiter)
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
TracePthreadCond(
|
||||
waiter.CompletionState == 2 ? "wait-resume-timeout" : "wait-resume",
|
||||
condAddress,
|
||||
mutexAddress,
|
||||
state,
|
||||
waiter.CompletionState == 2,
|
||||
result);
|
||||
_ = ctx;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int CondTimedOutResult(PthreadCondWaiter waiter) =>
|
||||
waiter.PosixErrors
|
||||
private static int CondTimedOutResult(bool posixErrors) =>
|
||||
posixErrors
|
||||
? 60 // ETIMEDOUT on Orbis/FreeBSD; pthread APIs return errno directly.
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
|
||||
private static void WakeCooperativeWaiter(PthreadCondWaiter waiter)
|
||||
{
|
||||
if (waiter.Cooperative)
|
||||
{
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(waiter.WakeKey, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
||||
{
|
||||
if (timeoutUsec == 0)
|
||||
|
||||
@@ -91,10 +91,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
||||
}
|
||||
|
||||
// On the outer class deliberately: a static on the nested state class gives it a type
|
||||
// initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||
private static long _nextRwlockWakeId;
|
||||
|
||||
private sealed class PthreadRwlockState
|
||||
{
|
||||
public object SyncRoot { get; } = new();
|
||||
@@ -105,8 +101,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
public ulong WriterThreadId { get; set; }
|
||||
public int WaitingWriters { get; set; }
|
||||
|
||||
// See PthreadMutexState.WakeKey.
|
||||
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
|
||||
|
||||
public int GetReaderCount(ulong threadId)
|
||||
{
|
||||
@@ -168,17 +162,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required PthreadRwlockState Rwlock { get; init; }
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required bool Write { get; init; }
|
||||
|
||||
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
||||
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
|
||||
}
|
||||
|
||||
private readonly record struct TlsKeyState(ulong Destructor);
|
||||
|
||||
private readonly record struct PthreadAttrState(
|
||||
@@ -1184,7 +1167,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1479,35 +1461,30 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// In-place block: Monitor.Wait releases SyncRoot and parks
|
||||
// atomically, so an unlock's PulseAll cannot be lost. Sliced
|
||||
// only so teardown can unwind parked threads.
|
||||
rwlock.WaitingWriters++;
|
||||
var transferredToScheduler = false;
|
||||
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_wrlock");
|
||||
try
|
||||
{
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_rwlock_wrlock",
|
||||
rwlock.WakeKey,
|
||||
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
|
||||
{
|
||||
transferredToScheduler = true;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||
{
|
||||
Monitor.Wait(rwlock.SyncRoot);
|
||||
if (GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
}
|
||||
|
||||
GuestThreadBlocking.Checkpoint(currentThreadId, rwlock.SyncRoot);
|
||||
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
}
|
||||
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!transferredToScheduler)
|
||||
{
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
}
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
GuestThreadBlocking.NoteUnblocked(currentThreadId);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1517,20 +1494,26 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
if (GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_rwlock_rdlock",
|
||||
rwlock.WakeKey,
|
||||
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
|
||||
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_rdlock");
|
||||
try
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
if (GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
}
|
||||
|
||||
Monitor.Wait(rwlock.SyncRoot);
|
||||
GuestThreadBlocking.Checkpoint(currentThreadId, rwlock.SyncRoot);
|
||||
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadBlocking.NoteUnblocked(currentThreadId);
|
||||
}
|
||||
}
|
||||
|
||||
if (rwlock.WriterThreadId != 0 ||
|
||||
@@ -1547,33 +1530,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
|
||||
{
|
||||
lock (rwlock.SyncRoot)
|
||||
{
|
||||
if (write)
|
||||
{
|
||||
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rwlock.AddReader(currentThreadId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
|
||||
// writer would share the rwlock with another holder — a data race.
|
||||
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
|
||||
@@ -1604,8 +1560,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
rwlock.GetReaderCount(currentThreadId) == 0;
|
||||
}
|
||||
|
||||
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
|
||||
|
||||
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
|
||||
{
|
||||
PthreadRwlockState? rwlock;
|
||||
|
||||
@@ -97,8 +97,6 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelUsleep");
|
||||
|
||||
if (micros < 1000)
|
||||
{
|
||||
// Guest worker pools use usleep(1) as a polling backoff. Do not turn
|
||||
@@ -1304,6 +1302,30 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Same (pc, flags, out-info) contract as sceKernelGetModuleInfoForUnwind,
|
||||
// surfaced through libSceSysmodule on Gen5; the unwinder threads whichever
|
||||
// one the module's libc was linked against.
|
||||
[SysAbiExport(
|
||||
Nid = "4fU5yvOkVG4",
|
||||
ExportName = "sceSysmoduleGetModuleInfoForUnwind",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSysmodule")]
|
||||
public static int SysmoduleGetModuleInfoForUnwind(CpuContext ctx) => KernelGetModuleInfoForUnwind(ctx);
|
||||
|
||||
// libc unwinder predicate: is this PC the kernel signal-return trampoline?
|
||||
// Guest signal returns do not run through a guest-visible trampoline here,
|
||||
// so no PC is ever one — report false and let the frame unwind normally.
|
||||
[SysAbiExport(
|
||||
Nid = "crb5j7mkk1c",
|
||||
ExportName = "_is_signal_return",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int IsSignalReturn(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "nu4a0-arQis",
|
||||
ExportName = "sceKernelAioInitializeParam",
|
||||
@@ -2097,7 +2119,6 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
|
||||
var totalTicks = tvSec * TimeSpan.TicksPerSecond + Math.Max(tvNsec / 100L, 1L);
|
||||
try
|
||||
{
|
||||
|
||||
@@ -17,8 +17,6 @@ public static class KernelSemaphoreCompatExports
|
||||
private sealed class KernelSemaphoreState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
// Formatted once at creation; signal/wait/cancel/delete all wake through this key.
|
||||
public required string WakeKey { get; init; }
|
||||
public required int InitialCount { get; init; }
|
||||
public required int MaxCount { get; init; }
|
||||
public int Count { get; set; }
|
||||
@@ -65,7 +63,6 @@ public static class KernelSemaphoreCompatExports
|
||||
_semaphores[handle] = new KernelSemaphoreState
|
||||
{
|
||||
Name = name,
|
||||
WakeKey = GetSemaphoreWakeKey(handle),
|
||||
InitialCount = initialCount,
|
||||
MaxCount = maxCount,
|
||||
Count = initialCount,
|
||||
@@ -111,148 +108,79 @@ public static class KernelSemaphoreCompatExports
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, timeoutUsec);
|
||||
}
|
||||
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
|
||||
}
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
semaphore.WaitingThreads++;
|
||||
}
|
||||
|
||||
// Block cooperatively: the wake predicate atomically acquires the
|
||||
// tokens (so a wake commits the acquisition), while the resume
|
||||
// handler distinguishes a real acquisition from a deadline expiry.
|
||||
var acquired = false;
|
||||
var deadline = timeoutAddress != 0
|
||||
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
|
||||
: 0;
|
||||
|
||||
bool WakePredicate()
|
||||
{
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
acquired = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int ResumeWait()
|
||||
{
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
}
|
||||
|
||||
if (acquired)
|
||||
{
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
}
|
||||
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
ResumeWait,
|
||||
WakePredicate,
|
||||
deadline))
|
||||
{
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||
}
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Not a guest thread (or no scheduler): fall back to a host-thread
|
||||
// wait so the semantics still hold on non-cooperative callers.
|
||||
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
|
||||
}
|
||||
|
||||
private static int WaitSemaphoreOnHostThread(
|
||||
CpuContext ctx,
|
||||
KernelSemaphoreState semaphore,
|
||||
uint handle,
|
||||
int needCount,
|
||||
ulong timeoutAddress,
|
||||
uint timeoutUsec)
|
||||
{
|
||||
// In-place block on the semaphore gate. Monitor.Wait releases the gate
|
||||
// and parks atomically, so a concurrent SignalSema's PulseAll cannot be
|
||||
// lost between the count check and the park. Semantics mirror FreeBSD
|
||||
// ksem / sem_wait: acquire when count>=need, else sleep until posted or
|
||||
// the deadline lapses. Waits are sliced only so teardown can unwind.
|
||||
var deadlineMs = timeoutAddress != 0
|
||||
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
|
||||
: long.MaxValue;
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (_traceSema)
|
||||
if (semaphore.Count < needCount)
|
||||
{
|
||||
TraceSemaphore(
|
||||
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
|
||||
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
|
||||
}
|
||||
while (semaphore.Count < needCount)
|
||||
{
|
||||
var remaining = deadlineMs - Environment.TickCount64;
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
semaphore.WaitingThreads++;
|
||||
if (_traceSema)
|
||||
{
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||
}
|
||||
|
||||
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
|
||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitSema");
|
||||
try
|
||||
{
|
||||
while (semaphore.Count < needCount)
|
||||
{
|
||||
if (GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
var remaining = deadlineMs - Environment.TickCount64;
|
||||
if (timeoutAddress != 0 && remaining <= 0)
|
||||
{
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
GuestThreadBlocking.Checkpoint(guestThreadHandle, semaphore.Gate);
|
||||
_ = Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
|
||||
}
|
||||
}
|
||||
|
||||
semaphore.Count -= needCount;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore(
|
||||
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
|
||||
}
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
}
|
||||
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSemaphoreWakeKey(uint handle) => $"sceKernelWaitSema:{handle:X8}";
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "12wOHk8ywb0",
|
||||
ExportName = "sceKernelPollSema",
|
||||
@@ -315,7 +243,7 @@ public static class KernelSemaphoreCompatExports
|
||||
}
|
||||
|
||||
semaphore.Count += signalCount;
|
||||
// Wake host-thread waiters parked in the fallback path.
|
||||
// Wake threads parked in-place on the gate; each re-checks the count.
|
||||
Monitor.PulseAll(semaphore.Gate);
|
||||
if (_traceSema)
|
||||
{
|
||||
@@ -323,9 +251,6 @@ public static class KernelSemaphoreCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
// Wake cooperatively-blocked guest threads; their wake predicate
|
||||
// acquires the tokens atomically, so this respects the new count.
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -354,7 +279,6 @@ public static class KernelSemaphoreCompatExports
|
||||
}
|
||||
|
||||
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||
semaphore.WaitingThreads = 0;
|
||||
Monitor.PulseAll(semaphore.Gate);
|
||||
if (_traceSema)
|
||||
{
|
||||
@@ -362,7 +286,6 @@ public static class KernelSemaphoreCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -410,7 +333,6 @@ public static class KernelSemaphoreCompatExports
|
||||
_semaphores[handle] = new KernelSemaphoreState
|
||||
{
|
||||
Name = $"posix@0x{semaphoreAddress:X16}",
|
||||
WakeKey = GetSemaphoreWakeKey(handle),
|
||||
InitialCount = initialCount,
|
||||
MaxCount = int.MaxValue,
|
||||
Count = initialCount,
|
||||
|
||||
@@ -14,44 +14,35 @@ namespace SharpEmu.Libs.Kernel;
|
||||
// loop; left unimplemented, every wait returns immediately and the runtime
|
||||
// busy-spins forever (millions of calls, no forward progress).
|
||||
//
|
||||
// This implements wait/wake over the existing cooperative-block scheduler,
|
||||
// keyed on the address. The real primitive takes a compare value so the wait
|
||||
// only sleeps while the address still holds the expected value; that exact
|
||||
// value is not recovered here, so each wait is given a bounded deadline and
|
||||
// treated as a spurious-wakeup-tolerant park: a genuinely missed wake
|
||||
// self-heals when the deadline expires and the guest re-checks its own
|
||||
// condition, which futex callers already tolerate. A matching wake releases
|
||||
// waiters immediately through the same key.
|
||||
// Waits block in place on a per-address gate (see GuestThreadBlocking):
|
||||
// Monitor.Wait releases the gate and parks atomically, so a wake's generation
|
||||
// bump + PulseAll cannot be lost between the generation check and the park.
|
||||
// The real primitive takes a compare value so the wait only sleeps while the
|
||||
// address still holds the expected value; that exact value is not recovered
|
||||
// here, so each wait is bounded by a self-heal deadline and treated as a
|
||||
// spurious-wakeup-tolerant park: the guest re-checks its own condition after
|
||||
// resuming, which futex callers already tolerate. Wake-one degrades to
|
||||
// wake-all for the same reason (each resumed waiter re-evaluates).
|
||||
public static class KernelSyncOnAddressCompatExports
|
||||
{
|
||||
// Safety-net poll interval. Real releases come from the wake side (generation
|
||||
// bump + WakeBlockedThreads); this only bounds how long a wait that genuinely
|
||||
// raced/missed its wake stays parked before the guest re-evaluates. Kept
|
||||
// large: a short interval turns every parked waiter into a hot re-poll that
|
||||
// steals scheduler bandwidth from the threads that actually make progress
|
||||
// (including the ones that would issue the wake), so it must be a rare last
|
||||
// resort, not a spin substitute.
|
||||
// Safety-net bound. Real releases come from the wake side; this only limits
|
||||
// how long a wait that genuinely raced/missed its wake stays parked before
|
||||
// the guest re-evaluates. Kept large: a short bound turns every parked
|
||||
// waiter into a hot re-poll that steals CPU from the threads that would
|
||||
// issue the wake, so it must be a rare last resort, not a spin substitute.
|
||||
private static readonly TimeSpan WaitSelfHealTimeout = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
// Per-address host gate for the non-cooperative (host main thread) fallback,
|
||||
// which cannot use the guest-thread scheduler's block mechanism.
|
||||
private static readonly ConcurrentDictionary<ulong, object> _hostAddressGates = new();
|
||||
private static readonly ConcurrentDictionary<ulong, object> _addressGates = new();
|
||||
|
||||
// Per-address wake generation. A wait captures the current generation and
|
||||
// its wake predicate stays unsatisfied (keeps the thread parked) until a
|
||||
// wake bumps it. This is what actually holds the thread blocked: a bare
|
||||
// "always satisfied" predicate is treated as an immediate late-arrival by
|
||||
// the dispatcher's race guard and never yields, leaving the guest to
|
||||
// busy-spin. The generation also closes the register-vs-park race for free:
|
||||
// a wake landing in that window bumps the generation, so the predicate is
|
||||
// already satisfied and the guest correctly resumes at once.
|
||||
// stays parked while it is unchanged; a wake bumps it first, then pulses
|
||||
// the gate, so a wait between its generation check and its park still
|
||||
// observes the bump (the check happens under the gate).
|
||||
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
|
||||
|
||||
private static long CurrentGeneration(ulong address) =>
|
||||
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
|
||||
|
||||
private static string WakeKey(ulong address) => $"sceKernelSyncOnAddress:{address:X16}";
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Hc4CaR6JBL0",
|
||||
ExportName = "sceKernelSyncOnAddressWait",
|
||||
@@ -66,32 +57,33 @@ public static class KernelSyncOnAddressCompatExports
|
||||
}
|
||||
|
||||
var observedGeneration = CurrentGeneration(address);
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(WaitSelfHealTimeout);
|
||||
|
||||
// Cooperative path: stay parked until a wake bumps this address's
|
||||
// generation (or the deadline expires as a self-heal). The guest
|
||||
// re-evaluates its own condition after resuming.
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelSyncOnAddressWait",
|
||||
WakeKey(address),
|
||||
resumeHandler: () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
wakeHandler: () => CurrentGeneration(address) != observedGeneration,
|
||||
deadline))
|
||||
var gate = _addressGates.GetOrAdd(address, static _ => new object());
|
||||
var deadlineMs = Environment.TickCount64 + (long)WaitSelfHealTimeout.TotalMilliseconds;
|
||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelSyncOnAddressWait");
|
||||
try
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Non-cooperative caller (host main thread): bounded host wait so a
|
||||
// missed wake self-heals instead of hanging.
|
||||
var gate = _hostAddressGates.GetOrAdd(address, static _ => new object());
|
||||
lock (gate)
|
||||
{
|
||||
if (CurrentGeneration(address) == observedGeneration)
|
||||
lock (gate)
|
||||
{
|
||||
Monitor.Wait(gate, WaitSelfHealTimeout);
|
||||
while (CurrentGeneration(address) == observedGeneration &&
|
||||
!GuestThreadBlocking.ShutdownRequested)
|
||||
{
|
||||
var remaining = deadlineMs - Environment.TickCount64;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
// Self-heal: resume and let the guest re-check its condition.
|
||||
break;
|
||||
}
|
||||
|
||||
GuestThreadBlocking.Checkpoint(guestThreadHandle, gate);
|
||||
_ = Monitor.Wait(gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
@@ -109,18 +101,13 @@ public static class KernelSyncOnAddressCompatExports
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// rsi carries the number of waiters to release (1 = wake-one, a large
|
||||
// value = wake-all); default to all if it looks unset.
|
||||
var requested = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||
var wakeCount = requested is > 0 and < int.MaxValue ? (int)requested : int.MaxValue;
|
||||
|
||||
// Bump the generation first so a wait that has registered but not yet
|
||||
// parked sees the change and resumes instead of missing this wake.
|
||||
// Bump the generation first so a wait that has checked but not yet
|
||||
// parked (it holds the gate for both) observes the change; then pulse
|
||||
// parked waiters. rsi's wake count degrades to wake-all — resumed
|
||||
// waiters re-evaluate their own condition, which futex callers tolerate.
|
||||
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
|
||||
|
||||
GuestThreadExecution.Scheduler?.WakeBlockedThreads(WakeKey(address), wakeCount);
|
||||
|
||||
if (_hostAddressGates.TryGetValue(address, out var gate))
|
||||
if (_addressGates.TryGetValue(address, out var gate))
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Reflection;
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed class GuestThreadBlockWaiterRepresentationTests
|
||||
{
|
||||
[Fact]
|
||||
public void SchedulerStoresOnlyTheWaiterObjectRepresentation()
|
||||
{
|
||||
var stateType = typeof(DirectExecutionBackend).GetNestedType(
|
||||
"GuestThreadState",
|
||||
BindingFlags.NonPublic);
|
||||
Assert.NotNull(stateType);
|
||||
|
||||
var waiterProperty = stateType.GetProperty(
|
||||
"BlockWaiter",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
Assert.NotNull(waiterProperty);
|
||||
Assert.Equal(typeof(IGuestThreadBlockWaiter), waiterProperty.PropertyType);
|
||||
Assert.Null(stateType.GetProperty(
|
||||
"BlockResumeHandler",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
|
||||
Assert.Null(stateType.GetProperty(
|
||||
"BlockWakeHandler",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
|
||||
|
||||
var registerMethods = typeof(DirectExecutionBackend)
|
||||
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.Where(method => method.Name == "RegisterBlockedGuestThreadContinuation")
|
||||
.ToArray();
|
||||
var registerMethod = Assert.Single(registerMethods);
|
||||
Assert.Contains(
|
||||
registerMethod.GetParameters(),
|
||||
parameter => parameter.ParameterType == typeof(IGuestThreadBlockWaiter));
|
||||
Assert.DoesNotContain(
|
||||
registerMethod.GetParameters(),
|
||||
parameter => IsFuncParameter(parameter.ParameterType));
|
||||
|
||||
var consumeMethods = typeof(GuestThreadExecution)
|
||||
.GetMethods(BindingFlags.Static | BindingFlags.Public)
|
||||
.Where(method => method.Name == nameof(GuestThreadExecution.TryConsumeCurrentThreadBlock));
|
||||
Assert.DoesNotContain(
|
||||
consumeMethods.SelectMany(method => method.GetParameters()),
|
||||
parameter => IsFuncParameter(parameter.ParameterType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DelegateCompatibilityBridgeIsConsumedAsOneWaiterObject()
|
||||
{
|
||||
var previousThread = GuestThreadExecution.EnterGuestThread(0x1234);
|
||||
try
|
||||
{
|
||||
var canWake = false;
|
||||
var wakeCalls = 0;
|
||||
var resumeCalls = 0;
|
||||
Assert.True(GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
context: null,
|
||||
reason: "test_wait",
|
||||
wakeKey: "test_waiter:1",
|
||||
resumeHandler: () =>
|
||||
{
|
||||
resumeCalls++;
|
||||
return 42;
|
||||
},
|
||||
wakeHandler: () =>
|
||||
{
|
||||
wakeCalls++;
|
||||
return canWake;
|
||||
}));
|
||||
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||
out var reason,
|
||||
out _,
|
||||
out var hasContinuation,
|
||||
out var wakeKey,
|
||||
out IGuestThreadBlockWaiter? waiter,
|
||||
out var deadline));
|
||||
Assert.Equal("test_wait", reason);
|
||||
Assert.Equal("test_waiter:1", wakeKey);
|
||||
Assert.False(hasContinuation);
|
||||
Assert.Equal(0, deadline);
|
||||
Assert.NotNull(waiter);
|
||||
|
||||
Assert.False(waiter.TryWake());
|
||||
canWake = true;
|
||||
Assert.True(waiter.TryWake());
|
||||
Assert.Equal(42, waiter.Resume());
|
||||
Assert.Equal(2, wakeCalls);
|
||||
Assert.Equal(1, resumeCalls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadExecution.RestoreGuestThread(previousThread);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFuncParameter(Type parameterType)
|
||||
{
|
||||
var type = parameterType.IsByRef
|
||||
? parameterType.GetElementType()
|
||||
: parameterType;
|
||||
return type is not null &&
|
||||
type.IsGenericType &&
|
||||
type.GetGenericTypeDefinition() == typeof(Func<>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Emulation;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed class Sha1InstructionEmulatorTests
|
||||
{
|
||||
private static readonly Sha1Vector Destination = new(
|
||||
0x0123_4567u, 0x89AB_CDEFu, 0x0F1E_2D3Cu, 0x4B5A_6978u);
|
||||
private static readonly Sha1Vector Source = new(
|
||||
0xFEDC_BA98u, 0x7654_3210u, 0xF0E1_D2C3u, 0xB4A5_9687u);
|
||||
|
||||
[Fact]
|
||||
public void MessageSchedule1_MatchesIntelLaneSemantics()
|
||||
{
|
||||
Assert.Equal(
|
||||
new Sha1Vector(0xF1C2_97A4u, 0x3D0E_5B68u, 0x0E3D_685Bu, 0xC2F1_A497u),
|
||||
Sha1InstructionEmulator.MessageSchedule1(Destination, Source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MessageSchedule2_MatchesIntelLaneSemantics()
|
||||
{
|
||||
Assert.Equal(
|
||||
new Sha1Vector(0xECA8_6420u, 0xEEEE_EEEEu, 0xF294_3E58u, 0x7777_7777u),
|
||||
Sha1InstructionEmulator.MessageSchedule2(Destination, Source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextE_MatchesIntelLaneSemantics()
|
||||
{
|
||||
Assert.Equal(
|
||||
new Sha1Vector(0xFEDC_BA98u, 0x7654_3210u, 0xF0E1_D2C3u, 0xC77C_30E5u),
|
||||
Sha1InstructionEmulator.NextE(Destination, Source));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0x20E8_2326u, 0x911F_2CA8u, 0xECE0_593Fu, 0x0C1C_11FBu)]
|
||||
[InlineData(1, 0x4598_D5B9u, 0x7BA0_0411u, 0x464E_3C51u, 0xF514_1B52u)]
|
||||
[InlineData(2, 0xEE0E_73F6u, 0x2509_A67Bu, 0x26C6_85CCu, 0x0097_5845u)]
|
||||
[InlineData(3, 0x9C7B_0B46u, 0xAEC8_EB49u, 0x8FD5_A2B7u, 0xFD49_9AECu)]
|
||||
public void FourRounds_MatchesAllFourSha1Functions(
|
||||
byte function,
|
||||
uint lane0,
|
||||
uint lane1,
|
||||
uint lane2,
|
||||
uint lane3)
|
||||
{
|
||||
Assert.Equal(
|
||||
new Sha1Vector(lane0, lane1, lane2, lane3),
|
||||
Sha1InstructionEmulator.FourRounds(Destination, Source, function));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user