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 OptitrackSkeletonDefinition m_skeletonDef;
private Dictionary<int, OptitrackPose> m_lastBonePoses = new Dictionary<int, OptitrackPose>(); 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() void Start()
{ {
if (StreamingClient == null) if (StreamingClient == null)
@ -37,16 +41,24 @@ public class OptitrackRawDataReceiver : MonoBehaviour
if (m_skeletonDef == null) return; if (m_skeletonDef == null) return;
} }
// 최신 스켈레톤 상태 가져오기 // FillBoneSnapshot으로 락 보호 하에 스냅샷 복사 (torn read 방지)
OptitrackSkeletonState skelState = StreamingClient.GetLatestSkeletonState(m_skeletonDef.Id); OptitrackHiResTimer.Timestamp ts;
if (skelState == null) return; if (!StreamingClient.FillBoneSnapshot(m_skeletonDef.Id, m_snapshotPositions, m_snapshotOrientations, out ts))
return;
// 각 본의 원본 데이터 저장 // 스냅샷에서 OptitrackPose로 변환하여 저장
foreach (var bone in m_skeletonDef.Bones) 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_skeletonDefinitions.Clear();
m_tmarkersetDefinitions.Clear(); m_tmarkersetDefinitions.Clear();
m_mirrorBoneIdMaps.Clear(); // 스켈레톤 정의 변경 시 mirror map 캐시 무효화 m_mirrorBoneIdMaps.Clear(); // 스켈레톤 정의 변경 시 mirror map 캐시 무효화
m_assetIdToNameCache.Clear(); // assetID→이름 캐시 무효화
// NatNet 스레드가 접근하는 캐시는 락으로 보호하여 레이스 방지
lock (m_frameDataUpdateLock)
{
m_assetIdToNameCache.Clear();
}
// ---------------------------------- // ----------------------------------
// - Translate Rigid Body Definitions // - Translate Rigid Body Definitions
@ -1880,19 +1885,19 @@ public class OptitrackStreamingClient : MonoBehaviour
// ----------------------------------------------------- // -----------------------------------------------------
// - Update trained markerset // trained markerset added // - Update trained markerset // trained markerset added
// ---------------------------------------------------- // ----------------------------------------------------
//Int32 frameTMarkersetCount = m_dataDescs.AssetDescriptions.Count; // m_dataDescs를 로컬 변수로 캡처: UpdateDefinitions()(메인 스레드)가 참조를 교체해도 안전
/*result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetTMarkersetCount(pFrame, out frameTMarkersetCount); // null 체크: SkipDataDescriptions=true 또는 UpdateDefinitions() 미완료/실패 시 크래시 방지
NatNetException.ThrowIfNotOK(result, "NatNet_Frame_GetTMarkersetCount failed.");*/ var dataDescsSnapshot = m_dataDescs;
if (dataDescsSnapshot != null && dataDescsSnapshot.AssetDescriptions != null)
for (int tmarkIdx = 0; tmarkIdx < m_dataDescs.AssetDescriptions.Count; ++tmarkIdx) 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. // Ensure we have a state corresponding to this tmarkerset ID.
OptitrackTMarkersetState tmarkState = GetOrCreateTMarkersetState(tmarkersetId); OptitrackTMarkersetState tmarkState = GetOrCreateTMarkersetState(tmarkersetId);
// TMarkerset 정의 검색을 본 루프 밖에서 1회만 수행 (기존: 매 본마다 선형 탐색) // TMarkerset 정의 검색을 본 루프 밖에서 1회만 수행 (기존: 매 본마다 선형 탐색)
Int32 tmarkRbCount = m_dataDescs.AssetDescriptions[tmarkIdx].RigidBodyCount; Int32 tmarkRbCount = dataDescsSnapshot.AssetDescriptions[tmarkIdx].RigidBodyCount;
OptitrackTMarkersetDefinition tmarkDef = GetTMarkersetDefinitionById(tmarkersetId); OptitrackTMarkersetDefinition tmarkDef = GetTMarkersetDefinitionById(tmarkersetId);
if (tmarkDef == null) if (tmarkDef == null)
{ {
@ -1940,7 +1945,7 @@ public class OptitrackStreamingClient : MonoBehaviour
// -------------------------------------------- // --------------------------------------------
// - Update trained markerset markers // - 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); /*result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_TMarkerset_GetMarkerCount(pFrame, tmarkIdx, out tmarkMarkerCount);
NatNetException.ThrowIfNotOK(result, "NatNet_Frame_TMarkerset_GetMarkerCount failed.");*/ NatNetException.ThrowIfNotOK(result, "NatNet_Frame_TMarkerset_GetMarkerCount failed.");*/
//Debug.Log("tmark marker count: " + tmarkMarkerCount); // working finally //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 ); Monitor.Enter( m_frameDataUpdateLock );
} }
/// <summary>
public void _ExitFrameDataUpdateLock() /// 내부 프레임 데이터 락 해제. 반드시 _EnterFrameDataUpdateLock()과 쌍으로 사용하세요.
/// </summary>
[System.Obsolete("직접 락 조작 대신 FillBoneSnapshot() 등 스레드 안전 API를 사용하세요.")]
internal void _ExitFrameDataUpdateLock()
{ {
Monitor.Exit( m_frameDataUpdateLock ); Monitor.Exit( m_frameDataUpdateLock );
} }