From d8397b022e675c38d749c441450889c9dd16680c Mon Sep 17 00:00:00 2001 From: Spooks <62370103+Spooks4576@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:22:52 -0600 Subject: [PATCH] Performance Improvements and Optimization Tweaks (#156) * Improve Gen5 rendering performance and compatibility * Pin .NET SDK for locked restore --------- Co-authored-by: Spooks4576 --- .../Native/DirectExecutionBackend.Imports.cs | 6 +- .../Memory/PhysicalVirtualMemory.cs | 41 ++ src/SharpEmu.HLE/ICpuMemory.cs | 2 + src/SharpEmu.Libs/Agc/AgcExports.cs | 179 +++++++-- .../Agc/Gen5ShaderScalarEvaluator.cs | 133 ++++++- src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs | 44 ++- .../Kernel/KernelEventFlagCompatExports.cs | 132 ++++++- src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs | 21 ++ src/SharpEmu.Libs/Pad/PadExports.cs | 15 +- .../VideoOut/VulkanVideoPresenter.cs | 351 +++++++++++++++--- 10 files changed, 801 insertions(+), 123 deletions(-) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index f4016fdb..fcf1c436 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -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 diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index 3170d192..b6da6eba 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -550,6 +550,47 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA } } + public bool TryCompare(ulong virtualAddress, ReadOnlySpan 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(srcPtr, expected.Length).SequenceEqual(expected); + } + finally + { + _gate.ExitReadLock(); + } + } + public bool TryWrite(ulong virtualAddress, ReadOnlySpan source) { var requiresExclusiveAccess = false; diff --git a/src/SharpEmu.HLE/ICpuMemory.cs b/src/SharpEmu.HLE/ICpuMemory.cs index c7a9cf8d..6cc001b0 100644 --- a/src/SharpEmu.HLE/ICpuMemory.cs +++ b/src/SharpEmu.HLE/ICpuMemory.cs @@ -8,4 +8,6 @@ public interface ICpuMemory bool TryRead(ulong virtualAddress, Span destination); bool TryWrite(ulong virtualAddress, ReadOnlySpan source); + + bool TryCompare(ulong virtualAddress, ReadOnlySpan expected) => false; } diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index e5f80b1d..57d17bd4 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -157,7 +157,7 @@ public static class AgcExports private static readonly HashSet _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 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; diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs index adc5701f..62db7a1f 100644 --- a/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs @@ -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(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(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; } diff --git a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs index 8cdb3760..ea257d5d 100644 --- a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs @@ -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 _interfaces = []; private readonly Dictionary _pixelInputs = []; private readonly Dictionary _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)); + } } } diff --git a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs index 6d0f9786..c692f725 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs @@ -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}"; diff --git a/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs b/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs index 38f92bd2..dc3af01b 100644 --- a/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs +++ b/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs @@ -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", diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs index 16a19eaf..8b747e42 100644 --- a/src/SharpEmu.Libs/Pad/PadExports.cs +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -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); } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 9994aed1..54126750 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -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 _performanceHudThreadCpu = []; + private readonly Dictionary _performanceHudThreadNames = []; private bool _vulkanReady; private bool _firstFramePresented; private bool _firstGuestDrawPresented; @@ -1121,9 +1146,11 @@ internal static unsafe class VulkanVideoPresenter options.Size = new Vector2D((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(); + var currentThreadIds = new HashSet(); + 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); }