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
This commit is contained in:
CasualcoderDev
2026-07-23 19:41:51 +07:00
committed by GitHub
parent f9d92135a0
commit 5a08a9bb43
3 changed files with 71 additions and 39 deletions
+1 -1
View File
@@ -387,7 +387,7 @@ internal sealed class EmulatorProcess : IDisposable
private void ForwardOutput(string? line, bool isError) private void ForwardOutput(string? line, bool isError)
{ {
if (!string.IsNullOrEmpty(line)) if (line is not null)
{ {
OutputReceived?.Invoke(line, isError); OutputReceived?.Invoke(line, isError);
} }
@@ -68,7 +68,7 @@ internal static class AudioPcmConversion
value = Math.Clamp(value, -1.0f, 1.0f); value = Math.Clamp(value, -1.0f, 1.0f);
var scale = value < 0.0f ? 32768.0f : short.MaxValue; var scale = value < 0.0f ? 32768.0f : short.MaxValue;
return checked((short)MathF.Round(value * scale)); return unchecked((short)MathF.Round(value * scale));
} }
// <paramref name="volume"/> is expected pre-clamped to [0, 1] by the caller. // <paramref name="volume"/> is expected pre-clamped to [0, 1] by the caller.
@@ -18,6 +18,7 @@ internal readonly record struct VulkanHostBufferAllocation(
internal sealed class VulkanHostBufferPool : IDisposable internal sealed class VulkanHostBufferPool : IDisposable
{ {
private readonly object _gate = new();
private readonly Dictionary<VulkanHostBufferPoolKey, Stack<VulkanHostBufferAllocation>> private readonly Dictionary<VulkanHostBufferPoolKey, Stack<VulkanHostBufferAllocation>>
_available = []; _available = [];
private readonly Dictionary<ulong, VulkanHostBufferAllocation> _allocations = []; private readonly Dictionary<ulong, VulkanHostBufferAllocation> _allocations = [];
@@ -40,16 +41,19 @@ internal sealed class VulkanHostBufferPool : IDisposable
VulkanHostBufferPoolKey key, VulkanHostBufferPoolKey key,
out VulkanHostBufferAllocation allocation) out VulkanHostBufferAllocation allocation)
{ {
if (!_available.TryGetValue(key, out var available) || lock (_gate)
!available.TryPop(out allocation))
{ {
allocation = default; if (!_available.TryGetValue(key, out var available) ||
return false; !available.TryPop(out allocation))
} {
allocation = default;
return false;
}
_cachedHandles.Remove(allocation.Buffer.Handle); _cachedHandles.Remove(allocation.Buffer.Handle);
CachedBytes -= allocation.Key.Capacity; CachedBytes -= allocation.Key.Capacity;
return true; return true;
}
} }
public void Register(VulkanHostBufferAllocation allocation) 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)); 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) public bool Return(VkBuffer buffer, DeviceMemory memory)
{ {
if (!_allocations.TryGetValue(buffer.Handle, out var allocation) || VulkanHostBufferAllocation? toDestroy = null;
allocation.Memory.Handle != memory.Handle) 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)) // Destroy outside the lock — _destroy calls into Vulkan which may
{ // grab device-level locks, and holding _gate while doing so risks
return true; // 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; return true;
} }
public void Dispose() 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<VulkanHostBufferAllocation> toDestroy;
lock (_gate)
{
toDestroy = new List<VulkanHostBufferAllocation>(_allocations.Values);
_allocations.Clear();
_available.Clear();
_cachedHandles.Clear();
CachedBytes = 0;
}
foreach (var allocation in toDestroy)
{ {
_destroy(allocation); _destroy(allocation);
} }
_allocations.Clear();
_available.Clear();
_cachedHandles.Clear();
CachedBytes = 0;
} }
} }