[cpu] hooked windows write faults into guest image tracking

This commit is contained in:
ParantezTech
2026-07-28 01:05:55 +03:00
parent da5a4b14df
commit c630441152
4 changed files with 230 additions and 8 deletions
@@ -40,6 +40,15 @@ public sealed partial class DirectExecutionBackend
} }
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub); _rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}"); Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
// The raw handler carries the guest-image write-fault bridge, so the
// path must be compiled before the first protected-page store can
// reach it. Guest code has not started yet, so warming here cannot
// race a real fault.
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
Console.Error.WriteLine(
"[LOADER][INFO] Guest image CPU write tracking: " +
$"{(SharpEmu.HLE.GuestImageWriteTracker.Enabled ? "enabled" : "disabled")}");
} }
else else
{ {
@@ -54,10 +54,13 @@ public sealed partial class DirectExecutionBackend
var startTicks = System.Diagnostics.Stopwatch.GetTimestamp(); var startTicks = System.Diagnostics.Stopwatch.GetTimestamp();
var r = directExecutionBackend.DispatchImport(importIndex, argPackPtr); var r = directExecutionBackend.DispatchImport(importIndex, argPackPtr);
RecordPerfHleDispatchTime(System.Diagnostics.Stopwatch.GetTimestamp() - startTicks); RecordPerfHleDispatchTime(System.Diagnostics.Stopwatch.GetTimestamp() - startTicks);
directExecutionBackend.ClearActiveImportIndex();
return r; return r;
} }
return directExecutionBackend.DispatchImport(importIndex, argPackPtr); var result = directExecutionBackend.DispatchImport(importIndex, argPackPtr);
directExecutionBackend.ClearActiveImportIndex();
return result;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -69,11 +72,7 @@ public sealed partial class DirectExecutionBackend
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo) private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
{ {
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord; if (TryHandleGuestImageWriteFault(exceptionInfo))
if (exceptionRecord->ExceptionCode == 3221225477u &&
exceptionRecord->NumberParameters >= 2 &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
exceptionRecord->ExceptionInformation[1]))
{ {
return -1; return -1;
} }
@@ -81,6 +80,37 @@ public sealed partial class DirectExecutionBackend
return TryRecoverUnresolvedSentinel(exceptionInfo); return TryRecoverUnresolvedSentinel(exceptionInfo);
} }
/// <summary>
/// Windows counterpart of the POSIX SIGSEGV bridge into
/// <see cref="SharpEmu.HLE.GuestImageWriteTracker"/>. Guest code runs natively,
/// so a store into a surface the GPU backend has cached is an ordinary CPU
/// write with nothing to intercept — the page is write-protected instead and
/// the resulting fault is what tells the backend to re-upload. Without this
/// the cache serves the first upload forever, and anything the guest CPU
/// draws (a software-decoded movie frame, a memset fog layer) never reaches
/// the screen.
/// </summary>
private unsafe static bool TryHandleGuestImageWriteFault(void* exceptionInfo)
{
if (!SharpEmu.HLE.GuestImageWriteTracker.Enabled)
{
return false;
}
var exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
// STATUS_ACCESS_VIOLATION, and only the write flavour: ExceptionInformation
// is [accessKind, address] with 0=read, 1=write, 8=DEP execute.
if (exceptionRecord->ExceptionCode != 3221225477u ||
exceptionRecord->NumberParameters < 2 ||
exceptionRecord->ExceptionInformation[0] != 1uL)
{
return false;
}
return SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
exceptionRecord->ExceptionInformation[1]);
}
private unsafe static int RawUnhandledFilterManaged(void* exceptionInfo) private unsafe static int RawUnhandledFilterManaged(void* exceptionInfo)
{ {
return TryRecoverUnresolvedSentinel(exceptionInfo); return TryRecoverUnresolvedSentinel(exceptionInfo);
@@ -174,6 +204,10 @@ public sealed partial class DirectExecutionBackend
{ {
RecordPerfHleCall(importStubEntry.Export?.Name ?? importStubEntry.Nid); RecordPerfHleCall(importStubEntry.Export?.Name ?? importStubEntry.Nid);
} }
if (_profileGuestRip)
{
EnsureGuestRipSampler();
}
int num2 = Volatile.Read(in _rawSentinelRecoveries); int num2 = Volatile.Read(in _rawSentinelRecoveries);
if (num2 != _lastReportedRawSentinelRecoveries) if (num2 != _lastReportedRawSentinelRecoveries)
{ {
@@ -187,6 +221,10 @@ public sealed partial class DirectExecutionBackend
} }
cpuContext.Rip = importStubEntry.Address; cpuContext.Rip = importStubEntry.Address;
if (_profileGuestRip)
{
cpuContext.ActiveImportIndex = importIndex;
}
LoadImportVolatileArguments(cpuContext, argPackPtr); LoadImportVolatileArguments(cpuContext, argPackPtr);
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr; cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8); cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
@@ -1453,7 +1491,7 @@ public sealed partial class DirectExecutionBackend
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) && string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedMutexTrylockBusy = var expectedMutexTrylockBusy =
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) && (nid is "K-jXhbt2gn4" or "upoVrzMHFeE") &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
var expectedSemaphoreTrywaitAgain = var expectedSemaphoreTrywaitAgain =
string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) && string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) &&
@@ -1470,6 +1508,9 @@ public sealed partial class DirectExecutionBackend
var expectedPrivacyInvalidParameter = var expectedPrivacyInvalidParameter =
string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) && string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) &&
resultValue == unchecked((int)0x80960009); resultValue == unchecked((int)0x80960009);
var expectedPlayGoChunkEnumerationEnd =
string.Equals(nid, "uWIYLFkkwqk", StringComparison.Ordinal) &&
resultValue == unchecked((int)0x80B2000C);
if (!expectedFileProbeMiss && if (!expectedFileProbeMiss &&
!expectedTimedWaitTimeout && !expectedTimedWaitTimeout &&
!expectedEqueueTimeout && !expectedEqueueTimeout &&
@@ -1478,7 +1519,8 @@ public sealed partial class DirectExecutionBackend
!expectedPollSemaBusy && !expectedPollSemaBusy &&
!expectedNetAcceptWouldBlock && !expectedNetAcceptWouldBlock &&
!expectedUserServiceNoEvent && !expectedUserServiceNoEvent &&
!expectedPrivacyInvalidParameter) !expectedPrivacyInvalidParameter &&
!expectedPlayGoChunkEnumerationEnd)
{ {
return true; return true;
} }
+11
View File
@@ -4383,6 +4383,14 @@ public static partial class AgcExports
_tracedProducerlessWaits.Clear(); _tracedProducerlessWaits.Clear();
} }
if (!stale)
{
// Count before the deduplication below: the warning fires once
// per label, so on its own it cannot say how often a queue
// actually suspends.
GpuWaitProfile.RecordSuspend(producer is not null);
}
if (!stale && producer is null && if (!stale && producer is null &&
!_tracedProducerlessWaits.Add( !_tracedProducerlessWaits.Add(
(memory, waiter.WaitAddress))) (memory, waiter.WaitAddress)))
@@ -5648,6 +5656,8 @@ public static partial class AgcExports
SharpEmu.Libs.Diagnostics.LoadProgressDiagnostics.TraceGpuWaitSnapshot( SharpEmu.Libs.Diagnostics.LoadProgressDiagnostics.TraceGpuWaitSnapshot(
ctx.Memory); ctx.Memory);
GpuWaitProfile.RecordMonitorPoll(resumed != 0);
GpuWaitProfile.ReportIfDue(remaining);
if (remaining == 0) if (remaining == 0)
{ {
gpuState.WaitMonitorRunning = false; gpuState.WaitMonitorRunning = false;
@@ -5841,6 +5851,7 @@ public static partial class AgcExports
$"submission={waiter.SubmissionId} label=0x{waiter.WaitAddress:X16} " + $"submission={waiter.SubmissionId} label=0x{waiter.WaitAddress:X16} " +
$"resume=0x{waiter.ResumeAddress:X16} remaining_dwords={remainingDwords} " + $"resume=0x{waiter.ResumeAddress:X16} remaining_dwords={remainingDwords} " +
$"waited_ms={waitedMilliseconds:F3}"); $"waited_ms={waitedMilliseconds:F3}");
GpuWaitProfile.RecordResume(waiter.WaitAddress, waitedMilliseconds);
if (remainingDwords == 0) if (remainingDwords == 0)
{ {
state.IsSuspended = false; state.IsSuspended = false;
+160
View File
@@ -0,0 +1,160 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.Libs.Agc;
/// <summary>
/// Aggregate accounting for suspended WAIT_REG_MEM packets, enabled with
/// SHARPEMU_PROFILE_GPU_WAIT=1.
///
/// The existing <c>agc.wait_suspended</c> warning is deduplicated per label, so
/// a label that suspends every frame is reported once and then goes silent —
/// which makes the log useless for judging whether GPU waits cost frame time.
/// This counts every suspension and every resume, and reports how long the
/// queues actually sat blocked.
/// </summary>
internal static class GpuWaitProfile
{
public static readonly bool Enabled = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GPU_WAIT"),
"1",
StringComparison.Ordinal);
private static readonly double _reportSeconds =
double.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GPU_WAIT_REPORT_S"),
System.Globalization.CultureInfo.InvariantCulture,
out var seconds) && seconds > 0
? seconds
: 5.0;
private static readonly object _gate = new();
private static readonly Dictionary<ulong, (long Count, double Milliseconds)> _byLabel = new();
private static long _suspensions;
private static long _resumes;
private static long _producerless;
private static long _monitorPolls;
private static long _monitorEmptyPolls;
private static double _totalWaitMilliseconds;
private static double _maxWaitMilliseconds;
private static long _windowStart = Stopwatch.GetTimestamp();
public static void RecordSuspend(bool hasProducer)
{
if (!Enabled)
{
return;
}
lock (_gate)
{
_suspensions++;
if (!hasProducer)
{
_producerless++;
}
}
}
public static void RecordResume(ulong label, double waitedMilliseconds)
{
if (!Enabled)
{
return;
}
lock (_gate)
{
_resumes++;
_totalWaitMilliseconds += waitedMilliseconds;
if (waitedMilliseconds > _maxWaitMilliseconds)
{
_maxWaitMilliseconds = waitedMilliseconds;
}
if (_byLabel.Count < 4096)
{
var existing = _byLabel.TryGetValue(label, out var entry) ? entry : default;
_byLabel[label] = (existing.Count + 1, existing.Milliseconds + waitedMilliseconds);
}
}
}
/// <summary>
/// Called once per wake of the wait monitor. An empty poll means the monitor
/// burned a wakeup without resuming anything, which is the cost of the
/// backoff loop rather than of the wait itself.
/// </summary>
public static void RecordMonitorPoll(bool resumedAny)
{
if (!Enabled)
{
return;
}
lock (_gate)
{
_monitorPolls++;
if (!resumedAny)
{
_monitorEmptyPolls++;
}
}
}
public static void ReportIfDue(int remainingWaiters)
{
if (!Enabled)
{
return;
}
string line;
lock (_gate)
{
var now = Stopwatch.GetTimestamp();
var elapsedTicks = now - _windowStart;
if (elapsedTicks < _reportSeconds * Stopwatch.Frequency)
{
return;
}
_windowStart = now;
var seconds = elapsedTicks / (double)Stopwatch.Frequency;
// Total blocked time across all queues. Above 1000ms/s the queues are
// overlapping their stalls, so compare it against the frame budget,
// not against wall time.
var top = _byLabel
.OrderByDescending(entry => entry.Value.Milliseconds)
.Take(5)
.Select(entry =>
$"0x{entry.Key:X}={entry.Value.Milliseconds / seconds:F0}ms/s" +
$"/n{entry.Value.Count}")
.ToArray();
line =
$"[PERF][GPUWAIT] {seconds:F1}s suspend/s={_suspensions / seconds:F0} " +
$"resume/s={_resumes / seconds:F0} producerless/s={_producerless / seconds:F0} " +
$"blocked_ms/s={_totalWaitMilliseconds / seconds:F0} " +
$"avg_ms={(_resumes > 0 ? _totalWaitMilliseconds / _resumes : 0):F2} " +
$"max_ms={_maxWaitMilliseconds:F1} " +
$"monitor_polls/s={_monitorPolls / seconds:F0} " +
$"empty={(_monitorPolls > 0 ? _monitorEmptyPolls * 100.0 / _monitorPolls : 0):F0}% " +
$"outstanding={remainingWaiters} top: {string.Join(" | ", top)}";
_suspensions = 0;
_resumes = 0;
_producerless = 0;
_monitorPolls = 0;
_monitorEmptyPolls = 0;
_totalWaitMilliseconds = 0;
_maxWaitMilliseconds = 0;
_byLabel.Clear();
}
Console.Error.WriteLine(line);
}
}