mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
Performance Improvements and Optimization Tweaks (#156)
* Improve Gen5 rendering performance and compatibility * Pin .NET SDK for locked restore --------- Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
This commit is contained in:
@@ -814,10 +814,10 @@ public sealed partial class DirectExecutionBackend
|
||||
return !_logUsleep;
|
||||
}
|
||||
|
||||
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
|
||||
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
|
||||
// slows guest spinlocks enough to livelock (Demon's Souls).
|
||||
// Mutex lock uses this block-capable leaf path. Keep it out of the no-block subset.
|
||||
return nid is
|
||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||
|
||||
@@ -550,6 +550,47 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected)
|
||||
{
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var region = FindRegion(virtualAddress, (ulong)expected.Length);
|
||||
if (region is null ||
|
||||
!TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)expected.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expected.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (region.IsReservedOnly &&
|
||||
!EnsureRangeCommitted((ulong)srcPtr, (ulong)expected.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)expected.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return new ReadOnlySpan<byte>(srcPtr, expected.Length).SequenceEqual(expected);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
var requiresExclusiveAccess = false;
|
||||
|
||||
@@ -8,4 +8,6 @@ public interface ICpuMemory
|
||||
bool TryRead(ulong virtualAddress, Span<byte> destination);
|
||||
|
||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||
|
||||
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ public static class AgcExports
|
||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output),
|
||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output, uint Attributes),
|
||||
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||
@@ -2019,8 +2019,16 @@ public static class AgcExports
|
||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||
try
|
||||
{
|
||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2050,27 +2058,35 @@ public static class AgcExports
|
||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
for (uint i = 0; i < bufferCount; i++)
|
||||
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||
try
|
||||
{
|
||||
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
||||
commandAddress == 0 ||
|
||||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
||||
dwordCount == 0)
|
||||
for (uint i = 0; i < bufferCount; i++)
|
||||
{
|
||||
continue;
|
||||
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
||||
commandAddress == 0 ||
|
||||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
||||
dwordCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tracePackets)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
||||
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
||||
}
|
||||
|
||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||
}
|
||||
|
||||
if (tracePackets)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
||||
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
||||
}
|
||||
|
||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||
}
|
||||
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2118,8 +2134,16 @@ public static class AgcExports
|
||||
gpuState.ComputeQueues.Add(ownerHandle, queueState);
|
||||
}
|
||||
|
||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||
try
|
||||
{
|
||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -3418,7 +3442,8 @@ public static class AgcExports
|
||||
exportState,
|
||||
out var exportEvaluation,
|
||||
out error,
|
||||
resolveVertexInputs: true) ||
|
||||
resolveVertexInputs: true,
|
||||
vertexRecordLimit: indexed ? null : vertexCount) ||
|
||||
!Gen5ShaderTranslator.TryCreateState(
|
||||
ctx,
|
||||
pixelShaderAddress,
|
||||
@@ -3442,14 +3467,19 @@ public static class AgcExports
|
||||
HasPixelColorExport(pixelState, target.Slot))
|
||||
.ToArray();
|
||||
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
||||
var exportStateFingerprint = ComputeShaderStateFingerprint(exportEvaluation);
|
||||
var pixelStateFingerprint = ComputeShaderStateFingerprint(pixelEvaluation);
|
||||
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
||||
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
||||
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
||||
var shaderKey = (
|
||||
exportShaderAddress,
|
||||
exportStateFingerprint,
|
||||
pixelShaderAddress,
|
||||
pixelStateFingerprint,
|
||||
outputKind);
|
||||
outputKind,
|
||||
attributeCount);
|
||||
var totalGlobalBuffers =
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count;
|
||||
(byte[] Vertex, byte[] Pixel) compiled;
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
@@ -3458,9 +3488,6 @@ public static class AgcExports
|
||||
|
||||
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||
{
|
||||
var totalGlobalBuffers =
|
||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||
exportEvaluation.GlobalMemoryBindings.Count;
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||
pixelState,
|
||||
pixelEvaluation,
|
||||
@@ -3468,16 +3495,18 @@ public static class AgcExports
|
||||
out var pixelShader,
|
||||
out error,
|
||||
globalBufferBase: 0,
|
||||
totalGlobalBufferCount: totalGlobalBuffers,
|
||||
imageBindingBase: 0) ||
|
||||
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||
imageBindingBase: 0,
|
||||
scalarRegisterBufferIndex: totalGlobalBuffers) ||
|
||||
!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||
exportState,
|
||||
exportEvaluation,
|
||||
out var vertexShader,
|
||||
out error,
|
||||
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
||||
totalGlobalBufferCount: totalGlobalBuffers,
|
||||
imageBindingBase: pixelEvaluation.ImageBindings.Count))
|
||||
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
||||
scalarRegisterBufferIndex: totalGlobalBuffers + 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -3529,6 +3558,8 @@ public static class AgcExports
|
||||
|
||||
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
||||
.Concat(exportEvaluation.GlobalMemoryBindings)
|
||||
.Append(CreateScalarRegisterBinding(pixelEvaluation))
|
||||
.Append(CreateScalarRegisterBinding(exportEvaluation))
|
||||
.ToArray();
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||
exportEvaluation.VertexInputs ?? [];
|
||||
@@ -3539,7 +3570,7 @@ public static class AgcExports
|
||||
primitiveType,
|
||||
compiled.Vertex,
|
||||
compiled.Pixel,
|
||||
GetInterpolatedAttributeCount(pixelState),
|
||||
attributeCount,
|
||||
vertexCount,
|
||||
state.InstanceCount,
|
||||
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
||||
@@ -3651,6 +3682,88 @@ public static class AgcExports
|
||||
return (uint)(maxAttribute + 1);
|
||||
}
|
||||
|
||||
private const int ShaderScalarRegisterCount = 256;
|
||||
|
||||
private static Gen5GlobalMemoryBinding CreateScalarRegisterBinding(
|
||||
Gen5ShaderEvaluation evaluation)
|
||||
{
|
||||
var data = new byte[ShaderScalarRegisterCount * sizeof(uint)];
|
||||
var registers = evaluation.InitialScalarRegisters;
|
||||
var count = Math.Min(registers.Count, ShaderScalarRegisterCount);
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
var value = registers[index];
|
||||
var offset = index * sizeof(uint);
|
||||
data[offset] = (byte)value;
|
||||
data[offset + 1] = (byte)(value >> 8);
|
||||
data[offset + 2] = (byte)(value >> 16);
|
||||
data[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
|
||||
return new Gen5GlobalMemoryBinding(0, 0, [], data);
|
||||
}
|
||||
|
||||
private static ulong ComputeShaderStructureFingerprint(Gen5ShaderEvaluation evaluation)
|
||||
{
|
||||
const ulong offsetBasis = 14695981039346656037UL;
|
||||
const ulong prime = 1099511628211UL;
|
||||
var hash = offsetBasis;
|
||||
void Mix(ulong value) => hash = (hash ^ value) * prime;
|
||||
|
||||
Mix((ulong)evaluation.GlobalMemoryBindings.Count);
|
||||
foreach (var binding in evaluation.GlobalMemoryBindings)
|
||||
{
|
||||
Mix(binding.ScalarAddress);
|
||||
Mix((ulong)binding.InstructionPcs.Count);
|
||||
foreach (var pc in binding.InstructionPcs)
|
||||
{
|
||||
Mix(pc);
|
||||
}
|
||||
}
|
||||
|
||||
Mix((ulong)evaluation.ImageBindings.Count);
|
||||
foreach (var image in evaluation.ImageBindings)
|
||||
{
|
||||
Mix(image.Pc);
|
||||
Mix((ulong)(uint)image.Opcode.GetHashCode());
|
||||
foreach (var word in image.ResourceDescriptor)
|
||||
{
|
||||
Mix(word);
|
||||
}
|
||||
|
||||
foreach (var word in image.SamplerDescriptor)
|
||||
{
|
||||
Mix(word);
|
||||
}
|
||||
|
||||
Mix(image.MipLevel ?? uint.MaxValue);
|
||||
}
|
||||
|
||||
if (evaluation.VertexInputs is { } vertexInputs)
|
||||
{
|
||||
Mix((ulong)vertexInputs.Count);
|
||||
foreach (var input in vertexInputs)
|
||||
{
|
||||
Mix(input.Pc);
|
||||
Mix(input.Location);
|
||||
Mix(input.ComponentCount);
|
||||
Mix(input.DataFormat);
|
||||
Mix(input.NumberFormat);
|
||||
Mix(input.Stride);
|
||||
}
|
||||
}
|
||||
|
||||
if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters)
|
||||
{
|
||||
Mix(computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue);
|
||||
Mix(computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue);
|
||||
Mix(computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue);
|
||||
Mix(computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
|
||||
{
|
||||
const ulong offsetBasis = 14695981039346656037UL;
|
||||
|
||||
@@ -13,6 +13,24 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
private const int ImageDescriptorDwords = 8;
|
||||
private const int SamplerDescriptorDwords = 4;
|
||||
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
||||
private static readonly int DefaultGlobalMemoryBindingBytes =
|
||||
int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GLOBAL_BINDING_BYTES"),
|
||||
out var configured) && configured >= sizeof(uint)
|
||||
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
|
||||
: 1 * 1024 * 1024;
|
||||
|
||||
internal static long GlobalMemoryReadCount;
|
||||
internal static long GlobalMemoryReadBytes;
|
||||
internal static long GlobalMemoryReadCacheHits;
|
||||
internal static long GlobalMemoryReadPvmBytes;
|
||||
internal static long GlobalMemoryReadLibcBytes;
|
||||
internal static long GlobalMemoryReadReuses;
|
||||
|
||||
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
|
||||
private static readonly object _crossFrameReadGate = new();
|
||||
private static readonly Dictionary<(ulong BaseAddress, int SizeBytes), byte[]> _crossFrameReadCache = new();
|
||||
private static long _crossFrameReadCacheBytes;
|
||||
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
||||
|
||||
private readonly record struct BufferDescriptor(
|
||||
@@ -44,7 +62,8 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
Gen5ShaderState state,
|
||||
out Gen5ShaderEvaluation evaluation,
|
||||
out string error,
|
||||
bool resolveVertexInputs = false)
|
||||
bool resolveVertexInputs = false,
|
||||
uint? vertexRecordLimit = null)
|
||||
{
|
||||
evaluation = default!;
|
||||
error = string.Empty;
|
||||
@@ -255,10 +274,28 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
if (resolveVertexInputs &&
|
||||
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
||||
{
|
||||
var vertexReadSize = bufferDescriptor.SizeBytes;
|
||||
if (vertexRecordLimit is { } recordLimit &&
|
||||
instruction.Sources.Count > 2 &&
|
||||
TryEvaluateScalarOperand(
|
||||
instruction.Sources[2],
|
||||
scalarRegisters,
|
||||
out var scalarOffset))
|
||||
{
|
||||
var bindingOffset = unchecked((uint)bufferMemory.OffsetBytes + scalarOffset);
|
||||
var requiredBytes =
|
||||
(ulong)bindingOffset +
|
||||
(ulong)(Math.Max(recordLimit, 1u) - 1u) * bufferDescriptor.Stride +
|
||||
(ulong)bufferMemory.DwordCount * sizeof(uint);
|
||||
vertexReadSize = Math.Min(
|
||||
bufferDescriptor.SizeBytes,
|
||||
Math.Max(requiredBytes, sizeof(uint)));
|
||||
}
|
||||
|
||||
if (!TryReadGlobalMemory(
|
||||
ctx,
|
||||
bufferDescriptor.BaseAddress,
|
||||
bufferDescriptor.SizeBytes,
|
||||
vertexReadSize,
|
||||
out var vertexData))
|
||||
{
|
||||
error =
|
||||
@@ -533,22 +570,29 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
return false;
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
|
||||
|
||||
internal static void BeginGlobalMemoryReadScope()
|
||||
{
|
||||
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
|
||||
}
|
||||
|
||||
internal static void EndGlobalMemoryReadScope()
|
||||
{
|
||||
_globalMemoryReadCache = null;
|
||||
}
|
||||
|
||||
private static bool TryReadGlobalMemory(
|
||||
CpuContext ctx,
|
||||
ulong baseAddress,
|
||||
out byte[] data)
|
||||
{
|
||||
for (var size = MaxGlobalMemoryBindingBytes; size >= 4096; size >>= 1)
|
||||
{
|
||||
data = GC.AllocateUninitializedArray<byte>(size);
|
||||
if (ctx.Memory.TryRead(baseAddress, data))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
data = [];
|
||||
return false;
|
||||
return TryReadGlobalMemory(
|
||||
ctx,
|
||||
baseAddress,
|
||||
(ulong)DefaultGlobalMemoryBindingBytes,
|
||||
out data);
|
||||
}
|
||||
|
||||
private static bool TryReadGlobalMemory(
|
||||
@@ -570,13 +614,74 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
return false;
|
||||
}
|
||||
|
||||
var cache = _globalMemoryReadCache;
|
||||
var cacheKey = (baseAddress, (int)cappedSize);
|
||||
if (cache is not null && cache.TryGetValue(cacheKey, out var cached))
|
||||
{
|
||||
Interlocked.Increment(ref GlobalMemoryReadCacheHits);
|
||||
data = cached;
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[]? previous;
|
||||
lock (_crossFrameReadGate)
|
||||
{
|
||||
_crossFrameReadCache.TryGetValue(cacheKey, out previous);
|
||||
}
|
||||
|
||||
if (previous is not null && ctx.Memory.TryCompare(baseAddress, previous))
|
||||
{
|
||||
Interlocked.Increment(ref GlobalMemoryReadReuses);
|
||||
if (cache is not null)
|
||||
{
|
||||
cache[cacheKey] = previous;
|
||||
}
|
||||
|
||||
data = previous;
|
||||
return true;
|
||||
}
|
||||
|
||||
var candidateSize = (int)cappedSize;
|
||||
while (candidateSize >= sizeof(uint))
|
||||
{
|
||||
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
||||
if (ctx.Memory.TryRead(baseAddress, data) ||
|
||||
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
|
||||
if (readFromPvm ||
|
||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
|
||||
{
|
||||
Interlocked.Increment(ref GlobalMemoryReadCount);
|
||||
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
|
||||
if (readFromPvm)
|
||||
{
|
||||
Interlocked.Add(ref GlobalMemoryReadPvmBytes, data.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Add(ref GlobalMemoryReadLibcBytes, data.Length);
|
||||
}
|
||||
|
||||
if (cache is not null)
|
||||
{
|
||||
cache[cacheKey] = data;
|
||||
}
|
||||
|
||||
lock (_crossFrameReadGate)
|
||||
{
|
||||
if (_crossFrameReadCache.TryGetValue(cacheKey, out var replaced))
|
||||
{
|
||||
_crossFrameReadCacheBytes -= replaced.Length;
|
||||
}
|
||||
|
||||
if (_crossFrameReadCacheBytes + data.Length > CrossFrameReadCacheMaxBytes)
|
||||
{
|
||||
_crossFrameReadCache.Clear();
|
||||
_crossFrameReadCacheBytes = 0;
|
||||
}
|
||||
|
||||
_crossFrameReadCache[cacheKey] = data;
|
||||
_crossFrameReadCacheBytes += data.Length;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0)
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1)
|
||||
{
|
||||
var context = new CompilationContext(
|
||||
Gen5SpirvStage.Pixel,
|
||||
@@ -30,7 +31,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
1,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase);
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex);
|
||||
return context.TryCompile(out shader, out error);
|
||||
}
|
||||
|
||||
@@ -41,7 +43,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
out string error,
|
||||
int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1,
|
||||
int imageBindingBase = 0)
|
||||
int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1)
|
||||
{
|
||||
var context = new CompilationContext(
|
||||
Gen5SpirvStage.Vertex,
|
||||
@@ -53,7 +56,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
1,
|
||||
globalBufferBase,
|
||||
totalGlobalBufferCount,
|
||||
imageBindingBase);
|
||||
imageBindingBase,
|
||||
scalarRegisterBufferIndex);
|
||||
return context.TryCompile(out shader, out error);
|
||||
}
|
||||
|
||||
@@ -76,7 +80,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
Math.Max(localSizeZ, 1),
|
||||
0,
|
||||
-1,
|
||||
0);
|
||||
0,
|
||||
-1);
|
||||
return context.TryCompile(out shader, out error);
|
||||
}
|
||||
|
||||
@@ -93,6 +98,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
private readonly int _globalBufferBase;
|
||||
private readonly int _totalGlobalBufferCount;
|
||||
private readonly int _imageBindingBase;
|
||||
private readonly int _scalarRegisterBufferIndex;
|
||||
private readonly List<uint> _interfaces = [];
|
||||
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
||||
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
||||
@@ -167,7 +173,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
uint localSizeZ,
|
||||
int globalBufferBase,
|
||||
int totalGlobalBufferCount,
|
||||
int imageBindingBase)
|
||||
int imageBindingBase,
|
||||
int scalarRegisterBufferIndex)
|
||||
{
|
||||
_stage = stage;
|
||||
_state = state;
|
||||
@@ -181,6 +188,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
? evaluation.GlobalMemoryBindings.Count
|
||||
: totalGlobalBufferCount;
|
||||
_imageBindingBase = imageBindingBase;
|
||||
_scalarRegisterBufferIndex = scalarRegisterBufferIndex;
|
||||
}
|
||||
|
||||
public bool TryCompile(out Gen5SpirvShader shader, out string error)
|
||||
@@ -767,15 +775,25 @@ internal static partial class Gen5SpirvTranslator
|
||||
|
||||
private void EmitInitialState()
|
||||
{
|
||||
for (uint index = 0;
|
||||
index < _evaluation.InitialScalarRegisters.Count &&
|
||||
index < ScalarRegisterCount;
|
||||
index++)
|
||||
if (_scalarRegisterBufferIndex >= 0)
|
||||
{
|
||||
var value = _evaluation.InitialScalarRegisters[(int)index];
|
||||
if (value != 0)
|
||||
for (uint index = 0; index < ScalarRegisterCount; index++)
|
||||
{
|
||||
StoreS(index, UInt(value));
|
||||
StoreS(index, LoadBufferWord(_scalarRegisterBufferIndex, UInt(index)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint index = 0;
|
||||
index < _evaluation.InitialScalarRegisters.Count &&
|
||||
index < ScalarRegisterCount;
|
||||
index++)
|
||||
{
|
||||
var value = _evaluation.InitialScalarRegisters[(int)index];
|
||||
if (value != 0)
|
||||
{
|
||||
StoreS(index, UInt(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
@@ -33,6 +34,11 @@ public static class KernelEventFlagCompatExports
|
||||
public object Gate { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class EventFlagWaiter
|
||||
{
|
||||
public OrbisGen2Result? Result { get; set; }
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "BpFoboUJoZU",
|
||||
ExportName = "sceKernelCreateEventFlag",
|
||||
@@ -233,9 +239,47 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (timeoutUsec == 0)
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
||||
var timedWaiter = new EventFlagWaiter();
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEventFlag",
|
||||
GetEventFlagWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedTimedWait(
|
||||
ctx,
|
||||
state,
|
||||
timedWaiter,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress,
|
||||
timeoutAddress,
|
||||
deadline),
|
||||
wakeHandler: () => TryCompleteBlockedTimedWait(
|
||||
ctx,
|
||||
state,
|
||||
timedWaiter,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress),
|
||||
blockDeadlineTimestamp: deadline))
|
||||
{
|
||||
state.WaitingThreads++;
|
||||
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
@@ -446,6 +490,92 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryCompleteBlockedTimedWait(
|
||||
CpuContext ctx,
|
||||
EventFlagState state,
|
||||
EventFlagWaiter waiter,
|
||||
ulong pattern,
|
||||
uint waitMode,
|
||||
ulong resultAddress)
|
||||
{
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (waiter.Result is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
ApplyClearMode(state, pattern, waitMode);
|
||||
}
|
||||
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static int CompleteBlockedTimedWait(
|
||||
CpuContext ctx,
|
||||
EventFlagState state,
|
||||
EventFlagWaiter waiter,
|
||||
ulong pattern,
|
||||
uint waitMode,
|
||||
ulong resultAddress,
|
||||
ulong timeoutAddress,
|
||||
long deadlineTimestamp)
|
||||
{
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (waiter.Result is null)
|
||||
{
|
||||
if (IsSatisfied(state.Bits, pattern, waitMode))
|
||||
{
|
||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
ApplyClearMode(state, pattern, waitMode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
|
||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
||||
var remainingMicros = remainingTicks <= 0
|
||||
? 0u
|
||||
: (uint)Math.Min(
|
||||
uint.MaxValue,
|
||||
remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
}
|
||||
|
||||
return (int)waiter.Result.Value;
|
||||
}
|
||||
|
||||
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||
$"event_flag:0x{handle:X16}";
|
||||
|
||||
|
||||
@@ -237,6 +237,27 @@ public static class Ngs2Exports
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1WsleK-MTkE",
|
||||
ExportName = "sceNgs2GeomCalcListener",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0lbbayqDNoE",
|
||||
ExportName = "sceNgs2GeomResetSourceParam",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "7Lcfo8SmpsU",
|
||||
ExportName = "sceNgs2GeomResetListenerParam",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i0VnXM-C9fc",
|
||||
ExportName = "sceNgs2SystemRender",
|
||||
|
||||
@@ -28,6 +28,7 @@ public static class PadExports
|
||||
private static PadState _cachedInputState;
|
||||
|
||||
private static bool _initialized;
|
||||
private static int _controlsAnnouncementLogged;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hv1luiJrqQM",
|
||||
@@ -70,11 +71,15 @@ public static class PadExports
|
||||
|
||||
DualSenseReader.EnsureStarted();
|
||||
XInputReader.EnsureStarted();
|
||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||
: XInputReader.TryGetState(out _)
|
||||
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||
: XInputReader.TryGetState(out _)
|
||||
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using Silk.NET.Vulkan;
|
||||
using Silk.NET.Vulkan.Extensions.KHR;
|
||||
using Silk.NET.Vulkan.Extensions.EXT;
|
||||
using Silk.NET.Windowing;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -277,6 +278,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
TranslatedDraw: null,
|
||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||
IsSplash: false);
|
||||
System.Threading.Monitor.PulseAll(_gate);
|
||||
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
|
||||
}
|
||||
}
|
||||
@@ -310,6 +312,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
TranslatedDraw: null,
|
||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||
IsSplash: false);
|
||||
System.Threading.Monitor.PulseAll(_gate);
|
||||
if (_thread is not null)
|
||||
{
|
||||
return;
|
||||
@@ -359,6 +362,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
TranslatedDraw: null,
|
||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||
IsSplash: false);
|
||||
System.Threading.Monitor.PulseAll(_gate);
|
||||
if (_thread is not null)
|
||||
{
|
||||
return;
|
||||
@@ -429,6 +433,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
renderState ?? VulkanGuestRenderState.Default),
|
||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||
IsSplash: false);
|
||||
System.Threading.Monitor.PulseAll(_gate);
|
||||
if (_thread is not null)
|
||||
{
|
||||
return;
|
||||
@@ -643,6 +648,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
RequiredGuestWorkSequence: 0,
|
||||
IsSplash: false,
|
||||
GuestImageAddress: address);
|
||||
System.Threading.Monitor.PulseAll(_gate);
|
||||
if (_thread is not null)
|
||||
{
|
||||
return true;
|
||||
@@ -883,6 +889,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
_pendingGuestWork.Enqueue(work);
|
||||
_enqueuedGuestWorkSequence++;
|
||||
System.Threading.Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
private static bool TryTakeGuestWork(out object work)
|
||||
@@ -923,6 +930,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
private readonly IWindow _window;
|
||||
private const int MaxInFlightGuestSubmissions = 8;
|
||||
private const double PerformanceHudSampleSeconds = 0.5;
|
||||
private const uint ThreadQueryLimitedInformation = 0x0800;
|
||||
private static readonly bool _performanceHudEnabled =
|
||||
!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PERF_HUD"),
|
||||
"0",
|
||||
StringComparison.Ordinal);
|
||||
private Vk _vk = null!;
|
||||
private KhrSurface _surfaceApi = null!;
|
||||
private KhrSwapchain _swapchainApi = null!;
|
||||
@@ -961,6 +975,17 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private DeviceMemory _stagingMemory;
|
||||
private ulong _stagingSize;
|
||||
private long _presentedSequence;
|
||||
private long _performanceHudLastTimestamp;
|
||||
private TimeSpan _performanceHudLastProcessCpu;
|
||||
private long _performanceHudPresentedFrames;
|
||||
private long _performanceHudLastPresentedFrames;
|
||||
private long _performanceHudLastReadCount;
|
||||
private long _performanceHudLastReadBytes;
|
||||
private long _performanceHudLastReadHits;
|
||||
private long _performanceHudLastReadPvmBytes;
|
||||
private long _performanceHudLastReadLibcBytes;
|
||||
private readonly Dictionary<int, TimeSpan> _performanceHudThreadCpu = [];
|
||||
private readonly Dictionary<int, string> _performanceHudThreadNames = [];
|
||||
private bool _vulkanReady;
|
||||
private bool _firstFramePresented;
|
||||
private bool _firstGuestDrawPresented;
|
||||
@@ -1121,9 +1146,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight);
|
||||
options.Title = VideoOutExports.GetWindowTitle();
|
||||
options.WindowBorder = WindowBorder.Fixed;
|
||||
options.VSync = true;
|
||||
options.FramesPerSecond = 60;
|
||||
options.UpdatesPerSecond = 60;
|
||||
// FIFO already provides the presentation clock. Throttling Silk's render loop
|
||||
// as well can miss alternating vblanks and collapse delivery to 30 FPS or less.
|
||||
options.VSync = false;
|
||||
options.FramesPerSecond = 0;
|
||||
options.UpdatesPerSecond = 0;
|
||||
_window = Window.Create(options);
|
||||
_window.Load += Initialize;
|
||||
_window.Render += Render;
|
||||
@@ -1883,6 +1910,30 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var surfaceFormat = ChooseSurfaceFormat(formats);
|
||||
_swapchainFormat = surfaceFormat.Format;
|
||||
_extent = ChooseExtent(capabilities);
|
||||
uint presentModeCount = 0;
|
||||
Check(
|
||||
_surfaceApi.GetPhysicalDeviceSurfacePresentModes(
|
||||
_physicalDevice,
|
||||
_surface,
|
||||
&presentModeCount,
|
||||
null),
|
||||
"vkGetPhysicalDeviceSurfacePresentModesKHR");
|
||||
var presentModes = new PresentModeKHR[presentModeCount];
|
||||
fixed (PresentModeKHR* presentModePointer = presentModes)
|
||||
{
|
||||
Check(
|
||||
_surfaceApi.GetPhysicalDeviceSurfacePresentModes(
|
||||
_physicalDevice,
|
||||
_surface,
|
||||
&presentModeCount,
|
||||
presentModePointer),
|
||||
"vkGetPhysicalDeviceSurfacePresentModesKHR");
|
||||
}
|
||||
|
||||
var presentMode = presentModes.Contains(PresentModeKHR.MailboxKhr)
|
||||
? PresentModeKHR.MailboxKhr
|
||||
: PresentModeKHR.FifoKhr;
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Vulkan present mode: {presentMode}");
|
||||
var imageCount = capabilities.MinImageCount + 1;
|
||||
if (capabilities.MaxImageCount != 0)
|
||||
{
|
||||
@@ -1906,7 +1957,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
ImageSharingMode = SharingMode.Exclusive,
|
||||
PreTransform = capabilities.CurrentTransform,
|
||||
CompositeAlpha = compositeAlpha,
|
||||
PresentMode = PresentModeKHR.FifoKhr,
|
||||
PresentMode = presentMode,
|
||||
Clipped = true,
|
||||
};
|
||||
|
||||
@@ -5260,6 +5311,219 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private void UpdatePerformanceHud()
|
||||
{
|
||||
if (!_performanceHudEnabled || !OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
if (_performanceHudLastTimestamp != 0 &&
|
||||
Stopwatch.GetElapsedTime(_performanceHudLastTimestamp, now).TotalSeconds <
|
||||
PerformanceHudSampleSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var process = Process.GetCurrentProcess();
|
||||
var processCpu = process.TotalProcessorTime;
|
||||
var currentThreadCpu = new Dictionary<int, TimeSpan>();
|
||||
var currentThreadIds = new HashSet<int>();
|
||||
var hottestThreadId = 0;
|
||||
var hottestThreadCpuSeconds = 0.0;
|
||||
|
||||
foreach (ProcessThread thread in process.Threads)
|
||||
{
|
||||
using (thread)
|
||||
{
|
||||
try
|
||||
{
|
||||
var threadId = thread.Id;
|
||||
var cpu = thread.TotalProcessorTime;
|
||||
currentThreadIds.Add(threadId);
|
||||
currentThreadCpu[threadId] = cpu;
|
||||
if (_performanceHudThreadCpu.TryGetValue(threadId, out var previousCpu))
|
||||
{
|
||||
var deltaSeconds = Math.Max(0.0, (cpu - previousCpu).TotalSeconds);
|
||||
if (deltaSeconds > hottestThreadCpuSeconds)
|
||||
{
|
||||
hottestThreadCpuSeconds = deltaSeconds;
|
||||
hottestThreadId = threadId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_performanceHudLastTimestamp != 0)
|
||||
{
|
||||
var elapsedSeconds = Math.Max(
|
||||
Stopwatch.GetElapsedTime(_performanceHudLastTimestamp, now).TotalSeconds,
|
||||
0.001);
|
||||
var processCpuPercent = Math.Max(
|
||||
0.0,
|
||||
(processCpu - _performanceHudLastProcessCpu).TotalSeconds /
|
||||
elapsedSeconds /
|
||||
Math.Max(Environment.ProcessorCount, 1) *
|
||||
100.0);
|
||||
var hottestThreadPercent = hottestThreadCpuSeconds / elapsedSeconds * 100.0;
|
||||
var presentedFrames = _performanceHudPresentedFrames;
|
||||
var fps = (presentedFrames - _performanceHudLastPresentedFrames) / elapsedSeconds;
|
||||
var hotName = hottestThreadId == 0
|
||||
? "idle"
|
||||
: GetPerformanceThreadName(hottestThreadId);
|
||||
long guestBacklog;
|
||||
int queuedGuestWork;
|
||||
lock (_gate)
|
||||
{
|
||||
guestBacklog = Math.Max(
|
||||
0,
|
||||
_enqueuedGuestWorkSequence - _completedGuestWorkSequence);
|
||||
queuedGuestWork = _pendingGuestWork.Count;
|
||||
}
|
||||
|
||||
var gpuInFlight = _pendingGuestSubmissions.Count +
|
||||
(_presentationInFlight ? 1 : 0);
|
||||
var readCount = Interlocked.Read(
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
|
||||
var readBytes = Interlocked.Read(
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
|
||||
var readHits = Interlocked.Read(
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
|
||||
var readPvmBytes = Interlocked.Read(
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
|
||||
var readLibcBytes = Interlocked.Read(
|
||||
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
|
||||
var readsPerSecond =
|
||||
(readCount - _performanceHudLastReadCount) / elapsedSeconds;
|
||||
var readMbPerSecond =
|
||||
(readBytes - _performanceHudLastReadBytes) /
|
||||
elapsedSeconds /
|
||||
(1024.0 * 1024.0);
|
||||
var readHitsPerSecond =
|
||||
(readHits - _performanceHudLastReadHits) / elapsedSeconds;
|
||||
var readPvmMbPerSecond =
|
||||
(readPvmBytes - _performanceHudLastReadPvmBytes) /
|
||||
elapsedSeconds /
|
||||
(1024.0 * 1024.0);
|
||||
var readLibcMbPerSecond =
|
||||
(readLibcBytes - _performanceHudLastReadLibcBytes) /
|
||||
elapsedSeconds /
|
||||
(1024.0 * 1024.0);
|
||||
_performanceHudLastReadCount = readCount;
|
||||
_performanceHudLastReadBytes = readBytes;
|
||||
_performanceHudLastReadHits = readHits;
|
||||
_performanceHudLastReadPvmBytes = readPvmBytes;
|
||||
_performanceHudLastReadLibcBytes = readLibcBytes;
|
||||
_window.Title =
|
||||
$"FPS {fps:0.0} CPU {processCpuPercent:0}% | " +
|
||||
$"HOT {hotName}#{hottestThreadId} {hottestThreadPercent:0}% | " +
|
||||
$"WORK {guestBacklog} (q{queuedGuestWork}/gpu{gpuInFlight}) | " +
|
||||
$"RD {readsPerSecond:0}/s {readMbPerSecond:0}MB/s h{readHitsPerSecond:0}/s " +
|
||||
$"P{readPvmMbPerSecond:0} L{readLibcMbPerSecond:0} | " +
|
||||
VideoOutExports.GetWindowTitle();
|
||||
_performanceHudLastPresentedFrames = presentedFrames;
|
||||
}
|
||||
|
||||
_performanceHudThreadCpu.Clear();
|
||||
foreach (var (threadId, cpu) in currentThreadCpu)
|
||||
{
|
||||
_performanceHudThreadCpu[threadId] = cpu;
|
||||
}
|
||||
|
||||
foreach (var staleThreadId in _performanceHudThreadNames.Keys
|
||||
.Where(threadId => !currentThreadIds.Contains(threadId))
|
||||
.ToArray())
|
||||
{
|
||||
_performanceHudThreadNames.Remove(staleThreadId);
|
||||
}
|
||||
|
||||
_performanceHudLastProcessCpu = processCpu;
|
||||
_performanceHudLastTimestamp = now;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is InvalidOperationException or System.ComponentModel.Win32Exception)
|
||||
{
|
||||
_performanceHudLastTimestamp = now;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetPerformanceThreadName(int threadId)
|
||||
{
|
||||
if (_performanceHudThreadNames.TryGetValue(threadId, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var name = "tid";
|
||||
var handle = OpenThread(ThreadQueryLimitedInformation, false, (uint)threadId);
|
||||
if (handle != 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (GetThreadDescription(handle, out var description) >= 0 && description != 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var described = Marshal.PtrToStringUni(description);
|
||||
if (!string.IsNullOrWhiteSpace(described))
|
||||
{
|
||||
name = described.Length <= 28 ? described : described[..28];
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
LocalFree(description);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
_performanceHudThreadNames[threadId] = name;
|
||||
return name;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint OpenThread(uint desiredAccess, bool inheritHandle, uint threadId);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern int GetThreadDescription(nint thread, out nint description);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nint LocalFree(nint memory);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
|
||||
private void WaitForRenderWork()
|
||||
{
|
||||
var gpuWorkInFlight = _pendingGuestSubmissions.Count > 0 || _presentationInFlight;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_closed ||
|
||||
_pendingGuestWork.Count > 0 ||
|
||||
(_latestPresentation is { } latest &&
|
||||
latest.Sequence != _presentedSequence &&
|
||||
latest.RequiredGuestWorkSequence <= _completedGuestWorkSequence))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Threading.Monitor.Wait(_gate, gpuWorkInFlight ? 1 : 8);
|
||||
}
|
||||
}
|
||||
|
||||
private void Render(double _)
|
||||
{
|
||||
if (!_vulkanReady)
|
||||
@@ -5267,6 +5531,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
WaitForRenderWork();
|
||||
UpdatePerformanceHud();
|
||||
|
||||
_commandBuffer = _presentationCommandBuffer;
|
||||
if (!_deviceLost)
|
||||
{
|
||||
@@ -5530,6 +5797,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
|
||||
recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
|
||||
VideoOutExports.ReportPresentedFrame();
|
||||
_performanceHudPresentedFrames++;
|
||||
if (_swapchainReadbackPending)
|
||||
{
|
||||
CompletePendingPresentation(wait: true);
|
||||
@@ -6219,59 +6487,34 @@ internal static unsafe class VulkanVideoPresenter
|
||||
offsets);
|
||||
}
|
||||
|
||||
const uint maxPixelsPerDraw = 512 * 512;
|
||||
var rowsPerDraw = Math.Max(
|
||||
1u,
|
||||
Math.Min(drawScissor.Height, maxPixelsPerDraw / Math.Max(drawScissor.Width, 1u)));
|
||||
var drawCount = 0u;
|
||||
for (var y = 0u; y < drawScissor.Height; y += rowsPerDraw)
|
||||
var scissor = new Rect2D(
|
||||
new Offset2D(drawScissor.X, drawScissor.Y),
|
||||
new Extent2D(drawScissor.Width, drawScissor.Height));
|
||||
_vk.CmdSetScissor(_commandBuffer, 0, 1, &scissor);
|
||||
|
||||
if (resources.IndexBuffer.Handle != 0)
|
||||
{
|
||||
var scissor = new Rect2D(
|
||||
new Offset2D(
|
||||
drawScissor.X,
|
||||
checked(drawScissor.Y + (int)y)),
|
||||
new Extent2D(
|
||||
drawScissor.Width,
|
||||
Math.Min(rowsPerDraw, drawScissor.Height - y)));
|
||||
_vk.CmdSetScissor(_commandBuffer, 0, 1, &scissor);
|
||||
|
||||
if (resources.IndexBuffer.Handle != 0)
|
||||
{
|
||||
_vk.CmdBindIndexBuffer(
|
||||
_commandBuffer,
|
||||
resources.IndexBuffer,
|
||||
0,
|
||||
resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16);
|
||||
_vk.CmdDrawIndexed(
|
||||
_commandBuffer,
|
||||
resources.VertexCount,
|
||||
resources.InstanceCount,
|
||||
0,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
else
|
||||
{
|
||||
_vk.CmdDraw(
|
||||
_commandBuffer,
|
||||
resources.VertexCount,
|
||||
resources.InstanceCount,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
|
||||
drawCount++;
|
||||
_vk.CmdBindIndexBuffer(
|
||||
_commandBuffer,
|
||||
resources.IndexBuffer,
|
||||
0,
|
||||
resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16);
|
||||
_vk.CmdDrawIndexed(
|
||||
_commandBuffer,
|
||||
resources.VertexCount,
|
||||
resources.InstanceCount,
|
||||
0,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
|
||||
if (drawCount > 1)
|
||||
else
|
||||
{
|
||||
TraceVulkanShader(
|
||||
$"vk.graphics_chunked target={extent.Width}x{extent.Height} " +
|
||||
$"draws={drawCount} rows={rowsPerDraw} " +
|
||||
$"scissor={drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height} " +
|
||||
$"viewport={drawViewport.X:0.###},{drawViewport.Y:0.###}," +
|
||||
$"{drawViewport.Width:0.###}x{drawViewport.Height:0.###} " +
|
||||
$"name={resources.DebugName}");
|
||||
_vk.CmdDraw(
|
||||
_commandBuffer,
|
||||
resources.VertexCount,
|
||||
resources.InstanceCount,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
_vk.CmdEndRenderPass(_commandBuffer);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user