From 5a08a9bb43a9f5704a63a729ab2e503921e43782 Mon Sep 17 00:00:00 2001
From: CasualcoderDev <262955786+CasualcoderDev@users.noreply.github.com>
Date: Thu, 23 Jul 2026 19:41:51 +0700
Subject: [PATCH] fix: VulkanHostBufferPool deadlock, audio overflow crash, and
log grouping (#564)
* fix: VulkanHostBufferPool deadlock, audio overflow crash, and log grouping
* fix: implement thread-safe buffer pool, refactor output handling, and use unchecked cast for audio conversion
---
src/SharpEmu.GUI/EmulatorProcess.cs | 2 +-
src/SharpEmu.Libs/Audio/AudioPcmConversion.cs | 2 +-
.../VideoOut/VulkanHostBufferPool.cs | 106 ++++++++++++------
3 files changed, 71 insertions(+), 39 deletions(-)
diff --git a/src/SharpEmu.GUI/EmulatorProcess.cs b/src/SharpEmu.GUI/EmulatorProcess.cs
index 290d3f9e..822b181c 100644
--- a/src/SharpEmu.GUI/EmulatorProcess.cs
+++ b/src/SharpEmu.GUI/EmulatorProcess.cs
@@ -387,7 +387,7 @@ internal sealed class EmulatorProcess : IDisposable
private void ForwardOutput(string? line, bool isError)
{
- if (!string.IsNullOrEmpty(line))
+ if (line is not null)
{
OutputReceived?.Invoke(line, isError);
}
diff --git a/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs b/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs
index 6294402f..0cc2a697 100644
--- a/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs
+++ b/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs
@@ -68,7 +68,7 @@ internal static class AudioPcmConversion
value = Math.Clamp(value, -1.0f, 1.0f);
var scale = value < 0.0f ? 32768.0f : short.MaxValue;
- return checked((short)MathF.Round(value * scale));
+ return unchecked((short)MathF.Round(value * scale));
}
// is expected pre-clamped to [0, 1] by the caller.
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanHostBufferPool.cs b/src/SharpEmu.Libs/VideoOut/VulkanHostBufferPool.cs
index b6d2404f..d71d10c3 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanHostBufferPool.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanHostBufferPool.cs
@@ -18,6 +18,7 @@ internal readonly record struct VulkanHostBufferAllocation(
internal sealed class VulkanHostBufferPool : IDisposable
{
+ private readonly object _gate = new();
private readonly Dictionary>
_available = [];
private readonly Dictionary _allocations = [];
@@ -40,16 +41,19 @@ internal sealed class VulkanHostBufferPool : IDisposable
VulkanHostBufferPoolKey key,
out VulkanHostBufferAllocation allocation)
{
- if (!_available.TryGetValue(key, out var available) ||
- !available.TryPop(out allocation))
+ lock (_gate)
{
- allocation = default;
- return false;
- }
+ if (!_available.TryGetValue(key, out var available) ||
+ !available.TryPop(out allocation))
+ {
+ allocation = default;
+ return false;
+ }
- _cachedHandles.Remove(allocation.Buffer.Handle);
- CachedBytes -= allocation.Key.Capacity;
- return true;
+ _cachedHandles.Remove(allocation.Buffer.Handle);
+ CachedBytes -= allocation.Key.Capacity;
+ return true;
+ }
}
public void Register(VulkanHostBufferAllocation allocation)
@@ -59,51 +63,79 @@ internal sealed class VulkanHostBufferPool : IDisposable
throw new ArgumentException("A pooled buffer must have a valid handle.", nameof(allocation));
}
- _allocations.Add(allocation.Buffer.Handle, allocation);
+ lock (_gate)
+ {
+ _allocations.Add(allocation.Buffer.Handle, allocation);
+ }
}
public bool Return(VkBuffer buffer, DeviceMemory memory)
{
- if (!_allocations.TryGetValue(buffer.Handle, out var allocation) ||
- allocation.Memory.Handle != memory.Handle)
+ VulkanHostBufferAllocation? toDestroy = null;
+ lock (_gate)
{
- return false;
+ if (!_allocations.TryGetValue(buffer.Handle, out var allocation) ||
+ allocation.Memory.Handle != memory.Handle)
+ {
+ return false;
+ }
+
+ if (!_cachedHandles.Add(buffer.Handle))
+ {
+ return true;
+ }
+
+ if (allocation.Key.Capacity > MaximumCachedBytes - CachedBytes)
+ {
+ _cachedHandles.Remove(buffer.Handle);
+ _allocations.Remove(buffer.Handle);
+ toDestroy = allocation;
+ }
+ else
+ {
+ if (!_available.TryGetValue(allocation.Key, out var available))
+ {
+ available = [];
+ _available.Add(allocation.Key, available);
+ }
+
+ available.Push(allocation);
+ CachedBytes += allocation.Key.Capacity;
+ }
}
- if (!_cachedHandles.Add(buffer.Handle))
- {
- return true;
+ // Destroy outside the lock — _destroy calls into Vulkan which may
+ // grab device-level locks, and holding _gate while doing so risks
+ // a lock-ordering deadlock with a thread that holds the device lock
+ // and is waiting on _gate.
+if (toDestroy is { } td)
+{
+ _destroy(td);
+
}
- if (allocation.Key.Capacity > MaximumCachedBytes - CachedBytes)
- {
- _cachedHandles.Remove(buffer.Handle);
- _allocations.Remove(buffer.Handle);
- _destroy(allocation);
- return true;
- }
-
- if (!_available.TryGetValue(allocation.Key, out var available))
- {
- available = [];
- _available.Add(allocation.Key, available);
- }
-
- available.Push(allocation);
- CachedBytes += allocation.Key.Capacity;
return true;
}
public void Dispose()
{
- foreach (var allocation in _allocations.Values)
+ // Snapshot under the lock, destroy outside — _destroy calls into
+ // Vulkan which may grab device-level locks; holding _gate while
+ // doing so risks a lock-ordering deadlock with any thread that
+ // acquires the device lock first and then waits on _gate.
+ List toDestroy;
+ lock (_gate)
+ {
+ toDestroy = new List(_allocations.Values);
+ _allocations.Clear();
+ _available.Clear();
+ _cachedHandles.Clear();
+ CachedBytes = 0;
+ }
+
+ foreach (var allocation in toDestroy)
{
_destroy(allocation);
}
-
- _allocations.Clear();
- _available.Clear();
- _cachedHandles.Clear();
- CachedBytes = 0;
}
}