Fix: OptiTrack 플러그인 런타임 안전성 강화

- m_dataDescs null 체크 추가: SkipDataDescriptions=true 시 NatNet 스레드 크래시 방지
- m_dataDescs를 NatNet 콜백에서 로컬 변수로 캡처: UpdateDefinitions() 중 참조 교체 레이스 방지
- m_assetIdToNameCache 클리어를 락으로 보호: NatNet 스레드와의 동시 접근 방지
- _EnterFrameDataUpdateLock/_ExitFrameDataUpdateLock을 internal+Obsolete로 변경: 데드락 위험 차단
- OptitrackRawDataReceiver를 FillBoneSnapshot 패턴으로 변경: torn read 방지

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
qsxft258@gmail.com 2026-04-19 19:11:14 +09:00
parent 5aa805e16a
commit f6a6034387
2 changed files with 43 additions and 18 deletions

View File

@ -10,6 +10,10 @@ public class OptitrackRawDataReceiver : MonoBehaviour
private OptitrackSkeletonDefinition m_skeletonDef;
private Dictionary<int, OptitrackPose> m_lastBonePoses = new Dictionary<int, OptitrackPose>();
// FillBoneSnapshot 패턴: 락 안에서 데이터를 복사하여 torn read 방지
private Dictionary<int, Vector3> m_snapshotPositions = new Dictionary<int, Vector3>();
private Dictionary<int, Quaternion> m_snapshotOrientations = new Dictionary<int, Quaternion>();
void Start()
{
if (StreamingClient == null)
@ -37,16 +41,24 @@ public class OptitrackRawDataReceiver : MonoBehaviour
if (m_skeletonDef == null) return;
}
// 최신 스켈레톤 상태 가져오기
OptitrackSkeletonState skelState = StreamingClient.GetLatestSkeletonState(m_skeletonDef.Id);
if (skelState == null) return;
// FillBoneSnapshot으로 락 보호 하에 스냅샷 복사 (torn read 방지)
OptitrackHiResTimer.Timestamp ts;
if (!StreamingClient.FillBoneSnapshot(m_skeletonDef.Id, m_snapshotPositions, m_snapshotOrientations, out ts))
return;
// 각 본의 원본 데이터 저장
// 스냅샷에서 OptitrackPose로 변환하여 저장
foreach (var bone in m_skeletonDef.Bones)
{
if (skelState.LocalBonePoses.TryGetValue(bone.Id, out OptitrackPose bonePose))
if (m_snapshotPositions.TryGetValue(bone.Id, out Vector3 pos) &&
m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion ori))
{
m_lastBonePoses[bone.Id] = bonePose;
if (!m_lastBonePoses.TryGetValue(bone.Id, out OptitrackPose existingPose))
{
existingPose = new OptitrackPose();
m_lastBonePoses[bone.Id] = existingPose;
}
existingPose.Position = pos;
existingPose.Orientation = ori;
}
}
}

View File

@ -1269,7 +1269,12 @@ public class OptitrackStreamingClient : MonoBehaviour
m_skeletonDefinitions.Clear();
m_tmarkersetDefinitions.Clear();
m_mirrorBoneIdMaps.Clear(); // 스켈레톤 정의 변경 시 mirror map 캐시 무효화
m_assetIdToNameCache.Clear(); // assetID→이름 캐시 무효화
// NatNet 스레드가 접근하는 캐시는 락으로 보호하여 레이스 방지
lock (m_frameDataUpdateLock)
{
m_assetIdToNameCache.Clear();
}
// ----------------------------------
// - Translate Rigid Body Definitions
@ -1880,19 +1885,19 @@ public class OptitrackStreamingClient : MonoBehaviour
// -----------------------------------------------------
// - Update trained markerset // trained markerset added
// ----------------------------------------------------
//Int32 frameTMarkersetCount = m_dataDescs.AssetDescriptions.Count;
/*result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetTMarkersetCount(pFrame, out frameTMarkersetCount);
NatNetException.ThrowIfNotOK(result, "NatNet_Frame_GetTMarkersetCount failed.");*/
for (int tmarkIdx = 0; tmarkIdx < m_dataDescs.AssetDescriptions.Count; ++tmarkIdx)
// m_dataDescs를 로컬 변수로 캡처: UpdateDefinitions()(메인 스레드)가 참조를 교체해도 안전
// null 체크: SkipDataDescriptions=true 또는 UpdateDefinitions() 미완료/실패 시 크래시 방지
var dataDescsSnapshot = m_dataDescs;
if (dataDescsSnapshot != null && dataDescsSnapshot.AssetDescriptions != null)
for (int tmarkIdx = 0; tmarkIdx < dataDescsSnapshot.AssetDescriptions.Count; ++tmarkIdx)
{
Int32 tmarkersetId = m_dataDescs.AssetDescriptions[tmarkIdx].AssetID;
Int32 tmarkersetId = dataDescsSnapshot.AssetDescriptions[tmarkIdx].AssetID;
// Ensure we have a state corresponding to this tmarkerset ID.
OptitrackTMarkersetState tmarkState = GetOrCreateTMarkersetState(tmarkersetId);
// TMarkerset 정의 검색을 본 루프 밖에서 1회만 수행 (기존: 매 본마다 선형 탐색)
Int32 tmarkRbCount = m_dataDescs.AssetDescriptions[tmarkIdx].RigidBodyCount;
Int32 tmarkRbCount = dataDescsSnapshot.AssetDescriptions[tmarkIdx].RigidBodyCount;
OptitrackTMarkersetDefinition tmarkDef = GetTMarkersetDefinitionById(tmarkersetId);
if (tmarkDef == null)
{
@ -1940,7 +1945,7 @@ public class OptitrackStreamingClient : MonoBehaviour
// --------------------------------------------
// - Update trained markerset markers
// --------------------------------------------
Int32 tmarkMarkerCount = m_dataDescs.AssetDescriptions[tmarkIdx].MarkerCount;
Int32 tmarkMarkerCount = dataDescsSnapshot.AssetDescriptions[tmarkIdx].MarkerCount;
/*result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_TMarkerset_GetMarkerCount(pFrame, tmarkIdx, out tmarkMarkerCount);
NatNetException.ThrowIfNotOK(result, "NatNet_Frame_TMarkerset_GetMarkerCount failed.");*/
//Debug.Log("tmark marker count: " + tmarkMarkerCount); // working finally
@ -2398,13 +2403,21 @@ public class OptitrackStreamingClient : MonoBehaviour
}
public void _EnterFrameDataUpdateLock()
/// <summary>
/// 내부 프레임 데이터 락 진입. 반드시 try-finally 패턴으로 _ExitFrameDataUpdateLock()과 쌍으로 사용하세요.
/// Exit 없이 호출하면 NatNet 스레드가 영구 데드락됩니다.
/// </summary>
[System.Obsolete("직접 락 조작 대신 FillBoneSnapshot() 등 스레드 안전 API를 사용하세요.")]
internal void _EnterFrameDataUpdateLock()
{
Monitor.Enter( m_frameDataUpdateLock );
}
public void _ExitFrameDataUpdateLock()
/// <summary>
/// 내부 프레임 데이터 락 해제. 반드시 _EnterFrameDataUpdateLock()과 쌍으로 사용하세요.
/// </summary>
[System.Obsolete("직접 락 조작 대신 FillBoneSnapshot() 등 스레드 안전 API를 사용하세요.")]
internal void _ExitFrameDataUpdateLock()
{
Monitor.Exit( m_frameDataUpdateLock );
}