AvaloniaUI/Avalonia · error · Exception
IOSurfaceLock failed
Error message
IOSurfaceLock failed
What it means
On macOS SaveTexture locks the exported IOSurface via IOSurfaceLock to read its pixels; if the return is non-zero it throws a generic Exception. A non-zero kIOReturn code means the kernel rejected the lock (busy, invalid, or already locked).
Source
Thrown at samples/GpuInterop/VulkanDemo/VulkanImage.cs:350
{
Api.DestroyImageView(_device, _imageView, null);
Api.DestroyImage(_device, InternalHandle, null);
Api.FreeMemory(_device, _imageMemory, null);
_imageView = default;
InternalHandle = default;
_imageMemory = default;
}
public void SaveTexture(string path)
{
if (_vk.GrContext == null)
{
if (_hasIOSurface)
{
var surf = ExportIOSurface();
if (NativeMethods.IOSurfaceLock(surf, 0, IntPtr.Zero) != 0)
throw new Exception("IOSurfaceLock failed");
var w = (int)NativeMethods.IOSurfaceGetWidth(surf);
var h = (int)NativeMethods.IOSurfaceGetHeight(surf);
var sstride = NativeMethods.IOSurfaceGetBytesPerRow(surf);
var pSurface = NativeMethods.IOSurfaceGetBaseAddress(surf);
using var b = new Avalonia.Media.Imaging.Bitmap(PixelFormat.Bgra8888,
AlphaFormat.Premul, pSurface, new PixelSize(w, h),
new Vector(96, 96), (int)sstride);
b.Save(path, PngBitmapEncoderOptions.Default);
NativeMethods.IOSurfaceUnlock(surf, 0, IntPtr.Zero);
return;
}
else
throw new NotSupportedException("Need skia to dump textures, sorry");
}
_vk.GrContext.ResetContext();View on GitHub (pinned to 11c5427268)
Solutions
- Ensure the IOSurface is not GPU-resident at dump time — insert a wait/fence or flush the GrContext before locking.
- Always pair IOSurfaceLock with IOSurfaceUnlock (the sample does) and avoid nesting locks.
- Decode the non-zero return (kIOReturnBusy etc.) instead of throwing blindly to pinpoint the cause.
- Confirm ExportIOSurface() returned a valid (non-zero) handle before locking.
Example fix
// before
if (NativeMethods.IOSurfaceLock(surf, 0, IntPtr.Zero) != 0)
throw new Exception("IOSurfaceLock failed");
// after (capture the error code and surface state)
var kr = NativeMethods.IOSurfaceLock(surf, 0, IntPtr.Zero);
if (kr != 0)
throw new InvalidOperationException($"IOSurfaceLock failed (kern_return_t=0x{kr:X}: {new System.ComponentModel.Win32Exception(kr).Message})."); Defensive patterns
Strategy: retry
Validate before calling
// ensure GPU work is flushed before locking _vk.GrContext?.Flush(); _vk.GrContext?.Submit();
Type guard
static bool IsLockable(IntPtr surf) => surf != IntPtr.Zero;
Try / catch
for (int i = 0; i < 3; i++)
{
if (NativeMethods.IOSurfaceLock(surf, 0, IntPtr.Zero) == 0) { /* read */ break; }
Thread.Sleep(5); // surface may be GPU-busy
} Prevention
- Pair every IOSurfaceLock with IOSurfaceUnlock.
- Flush the GPU context before locking to avoid kIOReturnBusy.
- Decode the return code; do not treat all non-zero as identical.
When it happens
Trigger: IOSurfaceLock(surf, 0, NULL) returning non-zero — surface already locked, in use by the GPU (not yet flushed), or surf is an invalid/zero handle from a failed export. Common right after presenting or while Metal still holds it.
Common situations: Dumping a texture that is still GPU-owned without flushing/waiting; double-locking; exporting on a thread without IOSurface allocation entitlement; calling SaveTexture in a tight loop without unlocking.
Related errors
- Need skia to dump textures, sorry
- Unable to export IOSurfaceRef
- Unable to export IOSurfaceRef
- Value must be less than 10.
- Transition elements have different parents.
AI-assisted analysis of AvaloniaUI/Avalonia@11c5427268 (2026-08-13).
Data as JSON: /api/errors/4c8f2b4dbc02b3cc.
Report an issue: GitHub.