Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Packages/Tracking/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

[docs-website]: https://docs.ultraleap.com/unity-api/ "Ultraleap Docs"

## [Next]

### Fixed
- Fixed a number of File Descriptor and native handle leaks in `Connection` that could leads to resource exhaustion on repeated editor play/stop.
- Fixed errorneous "LeapProvider not assigned" log messages during teardown.

### Changed
- Deprecated `Dispose()` on `Connection` and made this a thin shim to `Stop()` to enable better cleanup of resources.
- Added timeout to `Connection.Stop()` to reduce hang time on teardown.

## [7.3.0] - 25/02/2026

### Added
Expand Down
202 changes: 108 additions & 94 deletions Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ namespace LeapInternal

public class Connection
{
// Timeout used both for the PollConnection wait inside the worker thread and
// for the Join wait when stopping it. They match because Stop is essentially
// waiting for one final Poll cycle to complete after CloseConnection signals it.
private const uint DEFAULT_TIMEOUT_MILLISECONDS = 150;

public struct Key
{
public readonly int connectionId;
Expand Down Expand Up @@ -59,6 +64,28 @@ static Connection()
long palmOffset = Marshal.OffsetOf(typeof(LEAP_HAND), "palm").ToInt64();
_handPositionOffset = Marshal.OffsetOf(typeof(LEAP_PALM), "position").ToInt64() + palmOffset;
_handOrientationOffset = Marshal.OffsetOf(typeof(LEAP_PALM), "orientation").ToInt64() + palmOffset;

// Stop every pooled connection on domain unload / process exit so native
// LeapC handles and worker threads are released deterministically.
// Registered once here rather than per-Start() to avoid handler accumulation.
AppDomain.CurrentDomain.DomainUnload += (s, e) => StopAll();
AppDomain.CurrentDomain.ProcessExit += (s, e) => StopAll();
}

private static void StopAll()
{
foreach (var conn in connectionDictionary.Values)
{
try
{
conn.Stop();
}
catch
{
// Ignore all errors during shutdown.
}
}
connectionDictionary.Clear();
}

public Key ConnectionKey { get; private set; }
Expand Down Expand Up @@ -143,34 +170,14 @@ public event EventHandler<ConnectionEventArgs> LeapConnection
public Action<BeginProfilingBlockArgs> LeapBeginProfilingBlock;
public Action<EndProfilingBlockArgs> LeapEndProfilingBlock;

private bool _disposed = false;

private bool _loggedNullDeviceWarningForGetInterpolatedFrame = false;
private bool _loggedNullDeviceWarningForGetInterpolatedFrameSize = false;

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
[Obsolete("Dispose is deprecated; Call Stop() instead.")]
public void Dispose() => Stop();

// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;

Stop();
LeapC.DestroyConnection(_leapConnection);
_leapConnection = IntPtr.Zero;

_disposed = true;
}

~Connection()
{
Dispose(false);
}
[Obsolete("Dispose is deprecated; Call Stop() instead.")]
protected virtual void Dispose(bool disposing) => Stop();

private Connection(Key connectionKey)
{
Expand All @@ -184,15 +191,21 @@ private Connection(Key connectionKey)

public void Start(string serverNamespace = "Leap Service", bool multiDeviceAware = true, bool enableFiducialMarkers = false)
{
LEAP_CONNECTION_CONFIG config = new LEAP_CONNECTION_CONFIG();
config.server_namespace = Marshal.StringToHGlobalAnsi(serverNamespace);
config.flags = 0;
if (multiDeviceAware)
config.flags |= (uint)eLeapConnectionFlag.eLeapConnectionFlag_MultipleDevicesAware;
if (enableFiducialMarkers)
config.flags |= (uint)eLeapConnectionFlag.eLeapConnectionFlag_FiducialTracking;
config.size = (uint)Marshal.SizeOf(config);
Start(config);
LEAP_CONNECTION_CONFIG config = new LEAP_CONNECTION_CONFIG
{
server_namespace = Marshal.StringToHGlobalAnsi(serverNamespace),
flags = (multiDeviceAware ? (uint)eLeapConnectionFlag.eLeapConnectionFlag_MultipleDevicesAware : 0u)
| (enableFiducialMarkers ? (uint)eLeapConnectionFlag.eLeapConnectionFlag_FiducialTracking : 0u),
size = (uint)Marshal.SizeOf<LEAP_CONNECTION_CONFIG>()
};
try
{
Start(config);
}
finally
{
Marshal.FreeHGlobal(config.server_namespace);
}
}

public void Start(LEAP_CONNECTION_CONFIG config)
Expand Down Expand Up @@ -247,7 +260,6 @@ public void Start(LEAP_CONNECTION_CONFIG config)
LeapC.SetAllocator(_leapConnection, ref _pLeapAllocator);

_isRunning = true;
AppDomain.CurrentDomain.DomainUnload += (arg1, arg2) => Dispose(true);

_polster = new Thread(new ThreadStart(this.processMessages));
_polster.Name = "LeapC Worker";
Expand All @@ -257,26 +269,25 @@ public void Start(LEAP_CONNECTION_CONFIG config)

public void Stop()
{
if (!_isRunning)
return;

_isRunning = false;

//Very important to close the connection before we try to join the
//worker thread! The call to PollConnection can sometimes block,
//despite the timeout, causing an attempt to join the thread waiting
//forever and preventing the connection from stopping.
//
//It seems that closing the connection causes PollConnection to
//unblock in these cases, so just make sure to close the connection
//before trying to join the worker thread.
LeapC.CloseConnection(_leapConnection);

_polster.Join();
if (_leapConnection != IntPtr.Zero)
{
// Close the connection before joining the worker thread. PollConnection
// can block past its timeout; closing the connection unblocks it so the
// join doesn't hang.
LeapC.CloseConnection(_leapConnection);
_polster?.Join((int)DEFAULT_TIMEOUT_MILLISECONDS);
_polster = null;

// Always destroy the connection to avoid leaking native handle.
LeapC.DestroyConnection(_leapConnection);
_leapConnection = IntPtr.Zero;
}
}

/// <summary>
/// Returns the version of the currently installed Tracking Service.
/// Returns the version of the currently installed Tracking Service.
/// Might return 0.0.0 if no device is connected or it cannot get the current version.
/// </summary>
/// <returns>the current tracking service version</returns>
Expand Down Expand Up @@ -308,9 +319,7 @@ private void processMessages()
}

LEAP_CONNECTION_MESSAGE _msg = new LEAP_CONNECTION_MESSAGE();
uint timeout = 150;

result = LeapC.PollConnection(_leapConnection, timeout, ref _msg);
result = LeapC.PollConnection(_leapConnection, DEFAULT_TIMEOUT_MILLISECONDS, ref _msg);

if (result != eLeapRS.eLeapRS_Success)
{
Expand Down Expand Up @@ -407,6 +416,11 @@ private void processMessages()
}
} //while running
}
catch (ThreadAbortException)
{
// Expected to occur during shutdown.
_isRunning = false;
}
catch (Exception e)
{
Logger.Log("Exception: " + e);
Expand Down Expand Up @@ -1005,48 +1019,48 @@ static public eLeapPolicyFlag FlagForPolicy(Controller.PolicyFlag singlePolicy)
/// </summary>
public static bool IsConnectionAvailable(string serverNamespace = "Leap Service")
{
LEAP_CONNECTION_CONFIG config = new LEAP_CONNECTION_CONFIG();
config.server_namespace = Marshal.StringToHGlobalAnsi(serverNamespace);
config.flags = 0;
config.size = (uint)Marshal.SizeOf(config);

IntPtr tempConnection;

eLeapRS result;

result = LeapC.CreateConnection(ref config, out tempConnection);

if (result != eLeapRS.eLeapRS_Success || tempConnection == IntPtr.Zero)
LEAP_CONNECTION_CONFIG config = new LEAP_CONNECTION_CONFIG
{
LeapC.CloseConnection(tempConnection);
return false;
}
server_namespace = Marshal.StringToHGlobalAnsi(serverNamespace),
flags = 0,
size = (uint)Marshal.SizeOf<LEAP_CONNECTION_CONFIG>()
};

result = LeapC.OpenConnection(tempConnection);

if (result != eLeapRS.eLeapRS_Success)
IntPtr tempConnection = IntPtr.Zero;
try
{
LeapC.CloseConnection(tempConnection);
return false;
}
if (LeapC.CreateConnection(ref config, out tempConnection) != eLeapRS.eLeapRS_Success
|| tempConnection == IntPtr.Zero)
{
return false;
}

if (LeapC.OpenConnection(tempConnection) != eLeapRS.eLeapRS_Success)
{
return false;
}

LEAP_CONNECTION_MESSAGE _msg = new LEAP_CONNECTION_MESSAGE();
uint timeout = 150;
result = LeapC.PollConnection(tempConnection, timeout, ref _msg);
LEAP_CONNECTION_MESSAGE _msg = new LEAP_CONNECTION_MESSAGE();
LeapC.PollConnection(tempConnection, DEFAULT_TIMEOUT_MILLISECONDS, ref _msg);

LEAP_CONNECTION_INFO pInfo = new LEAP_CONNECTION_INFO();
pInfo.size = (uint)Marshal.SizeOf(pInfo);
result = LeapC.GetConnectionInfo(tempConnection, ref pInfo);
LEAP_CONNECTION_INFO pInfo = new LEAP_CONNECTION_INFO
{
size = (uint)Marshal.SizeOf<LEAP_CONNECTION_INFO>()
};
LeapC.GetConnectionInfo(tempConnection, ref pInfo);

if (pInfo.status == eLeapConnectionStatus.eLeapConnectionStatus_Connected)
{
LeapC.CloseConnection(tempConnection);
return true;
return pInfo.status == eLeapConnectionStatus.eLeapConnectionStatus_Connected;
}
finally
{
if (tempConnection != IntPtr.Zero)
{
LeapC.CloseConnection(tempConnection);
LeapC.DestroyConnection(tempConnection);
}

LeapC.CloseConnection(tempConnection);

return false;
Marshal.FreeHGlobal(config.server_namespace);
}
}

/// <summary>
Expand Down Expand Up @@ -1203,11 +1217,11 @@ public FailedDeviceList FailedDevices

/// <summary>
/// Subscribes to the events coming from an individual device
///
///
/// If this is not called, only the primary device will be subscribed.
/// Will automatically unsubscribe the primary device if this is called
/// on a secondary device, but not a primary one.
///
/// Will automatically unsubscribe the primary device if this is called
/// on a secondary device, but not a primary one.
///
/// @since 4.1
/// </summary>
public void SubscribeToDeviceEvents(Device device)
Expand All @@ -1218,9 +1232,9 @@ public void SubscribeToDeviceEvents(Device device)

/// <summary>
/// Unsubscribes from the events coming from an individual device
///
///
/// This can be called safely, even if the device has not been subscribed.
///
///
/// @since 4.1
/// </summary>
public void UnsubscribeFromDeviceEvents(Device device)
Expand Down Expand Up @@ -1262,7 +1276,7 @@ public UnityEngine.Vector3 PixelToRectilinearEx(IntPtr deviceHandle,

/// <summary>
/// Converts from image-space pixel coordinates to camera-space rectilinear coordinates
///
///
/// Also allows specifying a specific device handle and calibration type.
/// </summary>
public UnityEngine.Vector3 PixelToRectilinearEx(IntPtr deviceHandle,
Expand Down Expand Up @@ -1294,7 +1308,7 @@ public UnityEngine.Vector3 RectilinearToPixel(Image.CameraType camera, UnityEngi

/// <summary>
/// Converts from camera-space rectilinear coordinates to image-space pixel coordinates
///
///
/// Also allows specifying a specific device handle and calibration type.
/// </summary>
public UnityEngine.Vector3 RectilinearToPixelEx(IntPtr deviceHandle,
Expand Down
11 changes: 4 additions & 7 deletions Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Controller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ public class Controller :
IController
{
Connection _connection;
bool _disposed = false;
string _serverNamespace = "Leap Service";
bool _supportsMultipleDevices = true;
bool _supportsFiducialMarkers = false;
Expand Down Expand Up @@ -430,12 +429,10 @@ public void Dispose()
// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_connection.Dispose();
_disposed = true;
_connection.LeapInit -= OnInit;
_connection.LeapConnection -= OnConnect;
_connection.LeapConnectionLost -= OnDisconnect;
_connection.Stop();
}

/// <summary>
Expand Down
8 changes: 7 additions & 1 deletion Packages/Tracking/Core/Runtime/Scripts/Utils/HandUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public static class Hands
/// <param name="preferLiveLeapProviderOverHandPoseViewer">If the search gets to LeapProviders, return providers that are not sources of static data - e.g. hand pose viewers</param>
private static void AssignBestLeapProvider(bool preferLiveLeapProviderOverStaticHandPoseProviders = false)
{
// True only if never assigned; bypasses Unity's destroyed-equals-null check.
bool wasNeverAssigned = ReferenceEquals(s_provider, null);

// Fall through to the best available Leap Provider if none is assigned
if (s_provider == null)
{
Expand Down Expand Up @@ -80,7 +83,10 @@ private static void AssignBestLeapProvider(bool preferLiveLeapProviderOverStatic
}
}

Debug.Log("LeapProvider was not assigned. Auto assigning: " + s_provider);
if (wasNeverAssigned && s_provider != null)
{
Debug.Log("LeapProvider was not assigned. Auto assigning: " + s_provider);
}
}

/// <summary>
Expand Down