mirror of
https://github.com/par274/sharpemu.git
synced 2026-09-01 22:31:17 +08:00
[cpu] hooked windows write faults into guest image tracking
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user