mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 22:49:53 +08:00
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:
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user