diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index ae1c4b5d..1faad7c4 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -2079,6 +2079,42 @@ public static partial class AgcExports 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 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. #pragma warning disable SHEM004 [SysAbiExport( diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index 5fb6971c..eaf33a86 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -1070,15 +1070,29 @@ public static class KernelEventQueueCompatExports _pendingEvents[handle] = queue; } - QueueOrUpdateEvent( - queue, - new KernelQueuedEvent( - registration.Ident, - registration.Filter, - registration.Flags, - 1, - data, - registration.UserData)); + // GPU interrupt events must not coalesce: the AGC driver's + // interrupt thread accounts exactly one completion per + // delivered kevent (it never reads the kevent payload), so + // merging N triggers into one pending entry silently drops + // N-1 completions and wedges its dependency counters. Queue + // a distinct entry per trigger, with a defensive cap so an + // undrained queue cannot grow without bound. + var queuedEvent = new KernelQueuedEvent( + 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); 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( KernelEventDeque queue, KernelQueuedEvent queuedEvent) diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index ca2689c9..2027cc45 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -729,6 +729,14 @@ public static class VideoOutExports KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0); KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, 0); 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; } @@ -1114,7 +1122,14 @@ public static class VideoOutExports 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))