mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-18 17:06:12 +08:00
cdee77521e
* [agc] WAIT_REG_MEM suspend/resume, draw packet fixes, new HLE exports, debug cleanup
Rebased onto upstream 79a7437 (par274/sharpemu, rewritten history).
- GpuWaitRegistry: DCBs suspended on unsatisfied WAIT_REG_MEM are re-polled
against guest memory on every submit; fixed 64-bit and standard packet parse
offsets, apply the mask, treat PM4 compare function 0 as "always".
- TryReadSubmittedDrawCount: accept the 5-dword ItDrawIndex2 form emitted by
DcbDrawIndex (count at +4); menu draws were silently discarded before.
- sceAgcDriverSubmitMultiDcbs: reversed ABI (rdi=address array, rsi=dword
sizes, rdx=count).
- VideoOut: vblank events, sceVideoOutGetFlipStatus, buffers registered via
sceVideoOutRegisterBuffers are valid flip targets.
- New HLE: libc stdio (fopen/fread/fseek/ftell/fclose/fgets), Dinkumware
_Getpctype ctype table, NpTrophy2 stubs, AMPR PAK sequential-read tracker,
MsgDialog lifecycle, NGS2 alt NIDs + dummy vtable for handle objects,
guarded memset intrinsic, abort()/strcasecmp null-arg recovery.
- Removed investigation-only code (INT3 breakpoints, qfont/mcpp dumps,
error-candidate printf traces, unconditional debug logs).
First rendered frame: Quake (PPSA01880) presents a 1920x1080 guest frame.
* Implemented a guarded native intrinsic (rep movsb) in DirectExecutionBackend to bypass HLE dispatch overhead, while preserving memory safety checks.
* [hle] clock_gettime clock ids, AudioOut2 canary fix, NID rebinds, new offline stubs
- clock_gettime (lLMT9vJAck0): support CLOCK_SECOND and the *_PRECISE/*_FAST
variants instead of returning EINVAL, which games treated as fatal and
retried in a tight loop.
- AudioOut2: context param writes shrunk to the guest-observed layout (the
old 0x80-byte reset smashed the stack canary at +0x60 and killed audio
init); ContextQueryMemory writes the single u64 the caller expects.
- NGS2: dropped wrong alt-NID aliases (they hash to sceImeUpdate,
sceMouseRead, sceSystemGestureUpdateAllTouchRecognizer - now bound in
their real libraries); added sceNgs2PanInit; fixed VoiceGetState NIDs.
- New verified stubs: sceUltInitialize, sceNpUniversalDataSystemDestroyHandle,
sceNpGetOnlineId, sceNpGetNpReachabilityState, sceImeKeyboardOpen,
sceImeKeyboardGetResourceId, sceMouseOpen, sceKernelAprGetFileSize.
- Import gateway: unwind guest workers at dispatch during backend teardown;
env-gated SHARPEMU_LOG_THREAD_MODE tracing.
* [cpu] Isolate guest execution on native worker threads
Guest entry stubs no longer run above CLR-managed frames: each run is handed
to a pooled raw OS thread whose loop is emitted native code. While guest code
executes there is not a single managed frame on the thread and it stays in
preemptive GC mode, so the GC never walks a frame chain interleaved with
guest stubs that carry no CLR unwind info (the ReversePInvokeBadTransition /
UnmanagedCallersOnly FailFast class of crashes on pumped guest threads).
- NativeGuestExecutor: CreateThread + emitted run loop (WaitForSingleObject,
UnmanagedCallersOnly prologue/epilogue, entry stub call, SetEvent). The
prologue rebinds guest TLS, the host-RSP slot, thread affinity and the
Active* ambient per run, so workers carry no guest identity and pool
freely; the orchestrating managed thread parks in a preemptive wait.
- All three entry sites route through RunGuestEntryStub: guest thread
entries, blocked-continuation resumes, and the main ExecuteEntry.
- Teardown stops workers before any executable stub or TLS index they
reference is freed; a worker that will not stop leaks its loop instead of
freeing running code.
- Kill switch: SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS=1 restores the inline
calli path.
350 lines
12 KiB
C#
350 lines
12 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Kernel;
|
|
using System.Buffers.Binary;
|
|
|
|
namespace SharpEmu.Libs.Ngs2;
|
|
|
|
public static class Ngs2Exports
|
|
{
|
|
private const int OrbisNgs2ErrorInvalidOutAddress = unchecked((int)0x804A0053);
|
|
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
|
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
|
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
|
private const ulong HandleStorageSize = 0x1000;
|
|
private const int RenderBufferInfoSize = 0x18;
|
|
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
|
|
|
private static readonly object StateGate = new();
|
|
private static readonly Dictionary<ulong, SystemState> Systems = new();
|
|
private static readonly Dictionary<ulong, RackState> Racks = new();
|
|
private static readonly Dictionary<ulong, VoiceState> Voices = new();
|
|
private static long _nextUid;
|
|
private static long _renderCount;
|
|
|
|
private sealed record SystemState(uint Uid);
|
|
private sealed record RackState(ulong SystemHandle, uint RackId);
|
|
private sealed record VoiceState(ulong RackHandle, uint VoiceIndex);
|
|
|
|
[SysAbiExport(
|
|
Nid = "mPYgU4oYpuY",
|
|
ExportName = "sceNgs2SystemCreateWithAllocator",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2SystemCreateWithAllocator(CpuContext ctx)
|
|
{
|
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
|
if (outHandleAddress == 0)
|
|
{
|
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
|
}
|
|
|
|
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
|
{
|
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
lock (StateGate)
|
|
{
|
|
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
|
}
|
|
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "u-WrYDaJA3k",
|
|
ExportName = "sceNgs2SystemDestroy",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2SystemDestroy(CpuContext ctx)
|
|
{
|
|
var handle = ctx[CpuRegister.Rdi];
|
|
lock (StateGate)
|
|
{
|
|
if (!Systems.Remove(handle))
|
|
{
|
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
|
}
|
|
|
|
var rackHandles = Racks
|
|
.Where(pair => pair.Value.SystemHandle == handle)
|
|
.Select(pair => pair.Key)
|
|
.ToArray();
|
|
foreach (var rackHandle in rackHandles)
|
|
{
|
|
RemoveRackLocked(rackHandle);
|
|
}
|
|
}
|
|
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "U546k6orxQo",
|
|
ExportName = "sceNgs2RackCreateWithAllocator",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2RackCreateWithAllocator(CpuContext ctx)
|
|
{
|
|
var systemHandle = ctx[CpuRegister.Rdi];
|
|
var rackId = unchecked((uint)ctx[CpuRegister.Rsi]);
|
|
var outHandleAddress = ctx[CpuRegister.R8];
|
|
lock (StateGate)
|
|
{
|
|
if (!Systems.ContainsKey(systemHandle))
|
|
{
|
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
|
}
|
|
}
|
|
|
|
if (outHandleAddress == 0)
|
|
{
|
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
|
}
|
|
|
|
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
|
{
|
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
lock (StateGate)
|
|
{
|
|
Racks[handle] = new RackState(systemHandle, rackId);
|
|
}
|
|
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "lCqD7oycmIM",
|
|
ExportName = "sceNgs2RackDestroy",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2RackDestroy(CpuContext ctx)
|
|
{
|
|
var handle = ctx[CpuRegister.Rdi];
|
|
lock (StateGate)
|
|
{
|
|
if (!Racks.ContainsKey(handle))
|
|
{
|
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
|
}
|
|
|
|
RemoveRackLocked(handle);
|
|
}
|
|
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "MwmHz8pAdAo",
|
|
ExportName = "sceNgs2RackGetVoiceHandle",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2RackGetVoiceHandle(CpuContext ctx)
|
|
{
|
|
var rackHandle = ctx[CpuRegister.Rdi];
|
|
var voiceIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
|
lock (StateGate)
|
|
{
|
|
if (!Racks.ContainsKey(rackHandle))
|
|
{
|
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
|
}
|
|
|
|
var existing = Voices.FirstOrDefault(
|
|
pair => pair.Value.RackHandle == rackHandle && pair.Value.VoiceIndex == voiceIndex);
|
|
if (existing.Key != 0)
|
|
{
|
|
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
|
? ctx.SetReturn(0)
|
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
}
|
|
|
|
if (outHandleAddress == 0)
|
|
{
|
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
|
}
|
|
|
|
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
|
{
|
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
lock (StateGate)
|
|
{
|
|
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
|
}
|
|
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "uu94irFOGpA",
|
|
ExportName = "sceNgs2VoiceControl",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2VoiceControl(CpuContext ctx)
|
|
{
|
|
lock (StateGate)
|
|
{
|
|
return ctx.SetReturn(
|
|
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
|
? 0
|
|
: OrbisNgs2ErrorInvalidVoiceHandle);
|
|
}
|
|
}
|
|
|
|
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
|
|
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
|
|
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
|
|
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
|
|
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
|
|
// their real libraries; the NIDs below are verified against the hash.
|
|
[SysAbiExport(
|
|
Nid = "AbYvTOZ8Pts",
|
|
ExportName = "sceNgs2VoiceRunCommands",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
|
|
|
[SysAbiExport(
|
|
Nid = "-TOuuAQ-buE",
|
|
ExportName = "sceNgs2VoiceGetState",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
|
|
|
|
[SysAbiExport(
|
|
Nid = "rEh728kXk3w",
|
|
ExportName = "sceNgs2VoiceGetStateFlags",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
|
|
|
|
[SysAbiExport(
|
|
Nid = "xa8oL9dmXkM",
|
|
ExportName = "sceNgs2PanInit",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
|
|
|
[SysAbiExport(
|
|
Nid = "i0VnXM-C9fc",
|
|
ExportName = "sceNgs2SystemRender",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNgs2")]
|
|
public static int Ngs2SystemRender(CpuContext ctx)
|
|
{
|
|
var systemHandle = ctx[CpuRegister.Rdi];
|
|
var bufferInfoAddress = ctx[CpuRegister.Rsi];
|
|
var bufferInfoCount = unchecked((uint)ctx[CpuRegister.Rdx]);
|
|
lock (StateGate)
|
|
{
|
|
if (!Systems.ContainsKey(systemHandle))
|
|
{
|
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
|
}
|
|
}
|
|
|
|
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
|
{
|
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
for (uint i = 0; i < bufferInfoCount; i++)
|
|
{
|
|
var entryAddress = bufferInfoAddress + (i * RenderBufferInfoSize);
|
|
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
|
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
|
|
{
|
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
if (bufferAddress != 0 && bufferSize != 0)
|
|
{
|
|
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
|
{
|
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
}
|
|
}
|
|
|
|
var count = Interlocked.Increment(ref _renderCount);
|
|
if (ShouldTrace() && (count <= 4 || count % 10_000 == 0))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
|
}
|
|
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
|
{
|
|
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
|
data.Clear();
|
|
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
|
if (!ctx.Memory.TryWrite(handle, data))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Offset 0 doubles as a vtable slot for guest code that treats this handle as a C++
|
|
// object; stamp it with the shared dummy vtable instead of a self-referential value.
|
|
KernelMemoryCompatExports.TryWriteDummyVtable(ctx, handle);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
|
{
|
|
Span<byte> zeroes = stackalloc byte[4096];
|
|
zeroes.Clear();
|
|
for (ulong offset = 0; offset < length;)
|
|
{
|
|
var chunkSize = (int)Math.Min((ulong)zeroes.Length, length - offset);
|
|
if (!ctx.Memory.TryWrite(address + offset, zeroes[..chunkSize]))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
offset += unchecked((uint)chunkSize);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void RemoveRackLocked(ulong rackHandle)
|
|
{
|
|
Racks.Remove(rackHandle);
|
|
foreach (var voiceHandle in Voices
|
|
.Where(pair => pair.Value.RackHandle == rackHandle)
|
|
.Select(pair => pair.Key)
|
|
.ToArray())
|
|
{
|
|
Voices.Remove(voiceHandle);
|
|
}
|
|
}
|
|
|
|
private static bool ShouldTrace() =>
|
|
string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
}
|