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

  1. Ensure the IOSurface is not GPU-resident at dump time — insert a wait/fence or flush the GrContext before locking.
  2. Always pair IOSurfaceLock with IOSurfaceUnlock (the sample does) and avoid nesting locks.
  3. Decode the non-zero return (kIOReturnBusy etc.) instead of throwing blindly to pinpoint the cause.
  4. 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

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


AI-assisted analysis of AvaloniaUI/Avalonia@11c5427268 (2026-08-13). Data as JSON: /api/errors/4c8f2b4dbc02b3cc. Report an issue: GitHub.