mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-10 19:48:41 +08:00
[HLE] Fix AGC OOB, kevent coalescing, and VideoOut flip/buffer-registration bugs (#808)
sceAgcAddPrimStateRegisters (NID unconfirmed, resolved from the decrypted eboot's call site) reads 32 register pairs from a buffer that sceAgcCreatePrimState only partially fills, treating the unfilled part as guest-stack garbage -- an out-of-bounds probe index sourced from that garbage was the AV. Zero the unfilled part instead. GPU interrupt kevents were being coalesced when several arrived close together, but the AGC driver's interrupt thread expects exactly one completion per delivered kevent -- coalescing silently dropped completions and wedged its dependency counters. Deliver one kevent per trigger instead, with a cap so an undrained queue can't grow unbounded. sceVideoOutGetFlipStatus left an extended region of its output struct un-zeroed; some titles poll a flag past the classic struct layout and spin forever on garbage that never clears. Zero the whole region -- flips already complete synchronously in this emulator, so it should read as not-pending anyway. sceVideoOutRegisterBuffers2 rejected any nonzero category/option, which some titles use, making them unable to ever register display buffers or flip. Treat unknown categories as the standard layout instead of failing the registration. Tested on Ghost of Yotei (PPSA26344): combined with the separate ajm-acm-audio change, gets past its audio-init stall and renders/presents a real GPU frame; neither change alone reaches that point. Tested on Cult of the Lamb: calls sceAgcAddPrimStateRegisters 39 times, 0 crashes (previously an unresolved import on this exact NID). Regression-checked on Demon's Souls (both with and without the separate native-tls-fix change, to reach further into boot), Astro Bot, Outer Wilds, Ghost of Tsushima, GTA, Minecraft, Quake: no change in behavior on any of them.
This commit is contained in:
@@ -2079,6 +2079,42 @@ public static partial class AgcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Symbol name unconfirmed (not in ps5_names.txt); resolved from the
|
||||||
|
// decrypted eboot's call site only. On Ghost of Yotei, the caller scans
|
||||||
|
// this same buffer right after sceAgcCreatePrimState for 32 (offset,value)
|
||||||
|
// pairs (a hardcoded size, not read from any header) and open-address-
|
||||||
|
// probes them as a register hash table -- an out-of-bounds probe index
|
||||||
|
// sourced from an unwritten pair was the AV. CreatePrimState only
|
||||||
|
// populates the first 3 pairs; zero the rest of the scanned window so
|
||||||
|
// every unpopulated slot is a harmless failed probe instead of
|
||||||
|
// guest-stack garbage.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "dbOlWdppb4o",
|
||||||
|
ExportName = "sceAgcAddPrimStateRegisters",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int AddPrimStateRegisters(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var ucRegistersAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (ucRegistersAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int prefilledPairBytes = 3 * 8; // sceAgcCreatePrimState's 3 (offset,value) pairs
|
||||||
|
const int scannedTableBytes = 0x20 * 8; // caller's hardcoded probe-window size
|
||||||
|
Span<byte> zero = stackalloc byte[scannedTableBytes - prefilledPairBytes];
|
||||||
|
zero.Clear();
|
||||||
|
if (!ctx.Memory.TryWrite(ucRegistersAddress + prefilledPairBytes, zero))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.add_prim_state_registers uc=0x{ucRegistersAddress:X16}");
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
// NID captured from shipped titles; the friendly name collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
// NID captured from shipped titles; the friendly name collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
||||||
#pragma warning disable SHEM004
|
#pragma warning disable SHEM004
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -1070,15 +1070,29 @@ public static class KernelEventQueueCompatExports
|
|||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueOrUpdateEvent(
|
// GPU interrupt events must not coalesce: the AGC driver's
|
||||||
queue,
|
// interrupt thread accounts exactly one completion per
|
||||||
new KernelQueuedEvent(
|
// delivered kevent (it never reads the kevent payload), so
|
||||||
registration.Ident,
|
// merging N triggers into one pending entry silently drops
|
||||||
registration.Filter,
|
// N-1 completions and wedges its dependency counters. Queue
|
||||||
registration.Flags,
|
// a distinct entry per trigger, with a defensive cap so an
|
||||||
1,
|
// undrained queue cannot grow without bound.
|
||||||
data,
|
var queuedEvent = new KernelQueuedEvent(
|
||||||
registration.UserData));
|
registration.Ident,
|
||||||
|
registration.Filter,
|
||||||
|
registration.Flags,
|
||||||
|
1,
|
||||||
|
data,
|
||||||
|
registration.UserData);
|
||||||
|
if (CountPendingEvents(queue, registration.Ident, registration.Filter) < 256)
|
||||||
|
{
|
||||||
|
queue.AddLast(queuedEvent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QueueOrUpdateEvent(queue, queuedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
(wakeQueues ??= []).Add(state);
|
(wakeQueues ??= []).Add(state);
|
||||||
triggeredCount++;
|
triggeredCount++;
|
||||||
|
|
||||||
@@ -1304,6 +1318,24 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int CountPendingEvents(
|
||||||
|
KernelEventDeque queue,
|
||||||
|
ulong ident,
|
||||||
|
short filter)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
for (var i = 0; i < queue.Count; i++)
|
||||||
|
{
|
||||||
|
var pending = queue[i];
|
||||||
|
if (pending.Ident == ident && pending.Filter == filter)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
private static void QueueOrUpdateEvent(
|
private static void QueueOrUpdateEvent(
|
||||||
KernelEventDeque queue,
|
KernelEventDeque queue,
|
||||||
KernelQueuedEvent queuedEvent)
|
KernelQueuedEvent queuedEvent)
|
||||||
|
|||||||
@@ -729,6 +729,14 @@ public static class VideoOutExports
|
|||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0);
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0);
|
||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, 0);
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, 0);
|
||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
||||||
|
// Ghost of Yotei polls a flag past the classic 0x28-byte struct and
|
||||||
|
// spins on sceKernelUsleep(1) while it's nonzero; the caller never
|
||||||
|
// pre-zeroes that stack buffer, so an untouched field reads back as
|
||||||
|
// garbage. Flips complete synchronously in this emulator (see
|
||||||
|
// SubmitFlip/sceVideoOutIsFlipPending, always not-pending), so the
|
||||||
|
// extended region must read zero here too.
|
||||||
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x28, 0);
|
||||||
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x30, 0);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,7 +1122,14 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
if (category > 1 || option != 0)
|
if (category > 1 || option != 0)
|
||||||
{
|
{
|
||||||
return OrbisVideoOutErrorInvalidValue;
|
// Ghost of Yotei registers its display buffers with a nonzero
|
||||||
|
// category/option pair; rejecting the registration guarantees the
|
||||||
|
// title can never flip. Treat unknown categories as the standard
|
||||||
|
// uncompressed layout instead of failing the whole registration.
|
||||||
|
TraceVideoOut(
|
||||||
|
$"register_buffers2 nonstandard category=0x{categoryRaw:X} " +
|
||||||
|
$"option=0x{option:X} handle={handle} set={setIndex} " +
|
||||||
|
$"start={bufferIndexStart} count={bufferNum}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute))
|
if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute))
|
||||||
|
|||||||
Reference in New Issue
Block a user