-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathScene.cpp
More file actions
5129 lines (4599 loc) · 189 KB
/
Copy pathScene.cpp
File metadata and controls
5129 lines (4599 loc) · 189 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "Scene.h"
#include "AuthoringNodeViewAccess.h" // D3-a-5
#include <chrono>
#include <cstdio> // FireReentrancyStress가 stdout에도 낸다(회귀가 발화를 본다)
#include "LifecycleRegistry.h"
#include "VolumeComponent.h"
#include "LifecycleTrace.h"
#include "Entity.h"
#include "ClrHost.h"
#include "ScriptComponent.h"
#include "ScriptObjectRegistry.h"
#include "LightComponent.h"
#include "MeshRenderer.h"
#include "SpriteRenderer.h"
#include "Terrain.h"
#include "RenderScene.h"
#include "Animator.h"
#include "AnimatorSystem.h"
#include "DecalSystem.h"
#include "FoliageSystem.h"
#include "UITickSystem.h"
#include "SoundSystem.h"
#include "PlayerInputSystem.h"
#include "LightSystem.h"
#include "CameraSystem.h"
#include "CameraComponent.h"
#include "TweenManager.h"
#include "CharacterControllerSystem.h"
#include "BoneRegion.h"
#include "BoneComponent.h"
#include "PhysicsManager.h"
#include "BoxColliderComponent.h"
#include "SphereColliderComponent.h"
#include "CapsuleColliderComponent.h"
#include "MeshCollider.h"
#include "CharacterControllerComponent.h"
#include "FoliageComponent.h"
#include "TerrainCollider.h"
#include "RigidBodyComponent.h"
#include "ImageComponent.h"
#include "TextComponent.h"
#include "TagManager.h"
#include "UIManager.h"
#include "PlayerInput.h"
#include "DecalComponent.h"
#include "RectTransformComponent.h"
#include "Canvas.h"
#include "SpriteSheetComponent.h"
#include "AIManager.h"
#include <execution>
#include <queue>
#include <algorithm>
#include <ranges>
#include <iterator>
#include <atomic> // TryEnterTraversal의 깊이초과 1회 보고 플래그(std::atomic<bool>)에 필요 — 전이 include에 기대지 않음
#include <limits>
#include <mutex>
#include <utility>
struct Scene::TransformUpdateAccumulator
{
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
std::atomic<uint64_t> visitNs{ 0 };
std::atomic<uint64_t> localComposeNs{ 0 };
std::atomic<uint64_t> worldMultiplyNs{ 0 };
std::atomic<uint64_t> decomposeNs{ 0 };
std::atomic<uint64_t> visitCount{ 0 };
std::atomic<uint64_t> localComposeCount{ 0 };
std::atomic<uint64_t> worldMultiplyCount{ 0 };
std::atomic<uint64_t> decomposeCount{ 0 };
static uint64_t ElapsedNs(TimePoint begin, TimePoint end)
{
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count());
}
static void AddElapsed(std::atomic<uint64_t>& destination,
TimePoint begin, TimePoint end)
{
destination.fetch_add(ElapsedNs(begin, end), std::memory_order_relaxed);
}
// 한 노드의 exclusive visit 비용만 모은다. 수학 단계 전에는 Pause/Resume,
// 자식 재귀 전에는 Stop을 호출하므로 worker 합계에 자손 시간이 중복되지 않는다.
class VisitTimer
{
public:
explicit VisitTimer(TransformUpdateAccumulator* owner)
: m_owner(owner)
{
if (m_owner)
{
m_begin = Clock::now();
m_owner->visitCount.fetch_add(1, std::memory_order_relaxed);
}
}
~VisitTimer() { Pause(); }
void Pause()
{
if (!m_owner || !m_running) return;
const TimePoint end = Clock::now();
AddElapsed(m_owner->visitNs, m_begin, end);
m_running = false;
}
void Resume()
{
if (!m_owner || m_running) return;
m_begin = Clock::now();
m_running = true;
}
void Stop() { Pause(); }
private:
TransformUpdateAccumulator* m_owner = nullptr;
TimePoint m_begin{};
bool m_running = true;
};
};
// X4 compiled projection의 전부. ExecIndex는 의도적으로 Scene.h에 선언하지 않는다.
// 저작 정체성(Entity slot/generation)은 안정적으로 남고 이 배열만 canonical
// HierarchyStore의 preorder로 매 topology transaction 뒤 다시 packed 된다.
struct TransformExecutionGraphState
{
using ExecIndex = uint32_t;
static constexpr ExecIndex kInvalidExec = std::numeric_limits<ExecIndex>::max();
static constexpr uint64_t kInvalidVersion = std::numeric_limits<uint64_t>::max();
struct Projection
{
std::vector<ExecIndex> entityToExec;
std::vector<EntityHandle> execToEntity;
std::vector<ExecIndex> parentExec;
std::vector<ExecIndex> subtreeEnd;
};
struct SpatialProjection : Projection
{
std::vector<math::matrix4x4> localMatrix;
std::vector<math::matrix4x4> worldMatrix;
std::vector<uint64_t> localEpoch;
std::vector<uint64_t> resolvedLocalEpoch;
std::vector<uint64_t> worldEpoch;
std::vector<uint64_t> parentWorldEpoch;
std::vector<uint8_t> scaleQuatDirty;
// X7의 bulk pose upload 전까지 Bone만 기존 binding을 읽는다. culling
// publication도 compile 시 포인터를 잡아 일반 노드 inner loop의 component
// lookup을 없앤다.
std::vector<BoneComponent*> boneComponents;
std::vector<MeshRenderer*> meshRenderers;
};
struct AnimatorPoseBinding
{
EntityHandle owner{};
uint64_t skeletonSerial = 0;
uint64_t topologyVersion = kInvalidVersion;
std::vector<ExecIndex> boneExecByIndex;
uint64_t validBones = 0;
uint64_t invalidBones = 0;
};
SpatialProjection spatial;
Projection layout;
ExecutionGraphCompileMetrics metrics{};
SpatialResolveMetrics resolveMetrics{};
SpatialPullMetrics pullMetrics{};
std::unordered_map<size_t, AnimatorPoseBinding> animatorPoseBindings;
uint64_t attemptedVersion = kInvalidVersion;
uint64_t compiledVersion = kInvalidVersion;
bool spatialDataSynchronized = false;
// PublishLocalWrite는 worker에서도 도달할 수 있어 queue와 epoch snapshot을
// 한 잠금으로 묶는다. write-before-snapshot은 이번 resolve, write-after는 다음
// resolve가 소비한다.
std::mutex dirtyMutex;
std::vector<EntityHandle> dirtyRoots;
std::vector<uint64_t> entityQueuedEpoch;
std::vector<uint32_t> entityQueuedGeneration;
uint64_t enqueueEpoch = 1;
bool forceFullResolve = false;
};
using namespace std::literals;
#include "Profiler.h"
// ===== 유틸: 중복 없이 push_back =====
template<class R, class T>
static bool push_unique(R& vec, const T& v)
{
if (std::ranges::find(vec, v) == vec.end())
{
vec.push_back(v);
return true;
}
return false;
}
// Scene의 렌더 도메인 비소유 registry. Entity/component lifetime은 여전히
// m_Entities가 단독 소유한다. X8부터 종류별 벡터는 편집기/기존 시스템 조회만
// 맡고, 렌더 갱신은 generation ticket을 가진 frame-persistent dirty queue가 맡는다.
struct SceneRenderRegistryState
{
enum class Kind : uint8_t
{
Light, Mesh, Terrain, Foliage, Decal, Sprite, Image, Text, SpriteSheet
};
struct Registration
{
Kind kind{};
EntityHandle owner{};
size_t instanceId = 0;
uint64_t generation = 0;
ProxyDirty pending = ProxyDirty::None;
bool queued = false;
};
struct Ticket
{
Component* component = nullptr;
uint64_t generation = 0;
};
struct Dispatch
{
Component* component = nullptr;
Kind kind{};
EntityHandle owner{};
size_t instanceId = 0;
ProxyDirty mask = ProxyDirty::None;
};
std::vector<LightComponent*> lights;
std::vector<MeshRenderer*> meshes;
std::vector<TerrainComponent*> terrains;
std::vector<FoliageComponent*> foliages;
std::vector<DecalComponent*> decals;
std::vector<SpriteRenderer*> sprites;
std::vector<ImageComponent*> images;
std::vector<TextComponent*> texts;
std::vector<SpriteSheetComponent*> spriteSheets;
// 직렬화된 LightComponent::m_lightIndex 복원과 파괴 시 압축용 점유표다.
// 실제 렌더 값은 LightRenderProxy가 소유한다.
std::vector<uint8_t> lightSlots;
std::mutex dirtyMutex;
std::unordered_map<Component*, Registration> registrations;
std::vector<std::vector<Component*>> entityProxies;
std::vector<Ticket> dirtyQueue;
std::vector<Ticket> drainQueue;
std::vector<Dispatch> dispatchQueue;
uint64_t nextRegistrationGeneration = 1;
RenderProxyCommitMetrics metrics{};
void Clear()
{
lights.clear();
meshes.clear();
terrains.clear();
foliages.clear();
decals.clear();
sprites.clear();
images.clear();
texts.clear();
spriteSheets.clear();
lightSlots.clear();
registrations.clear();
entityProxies.clear();
dirtyQueue.clear();
drainQueue.clear();
dispatchQueue.clear();
metrics = {};
}
};
static void RegisterRenderProxy(SceneRenderRegistryState& state,
Component* component, SceneRenderRegistryState::Kind kind, EntityHandle owner)
{
if (nullptr == component || !owner.IsValid()) return;
std::scoped_lock lock(state.dirtyMutex);
if (state.registrations.contains(component)) return;
if (owner.index >= state.entityProxies.size())
state.entityProxies.resize(static_cast<size_t>(owner.index) + 1u);
uint64_t generation = state.nextRegistrationGeneration++;
if (0 == generation) generation = state.nextRegistrationGeneration++;
SceneRenderRegistryState::Registration registration{
kind, owner, component->GetInstanceID(), generation, ProxyDirty::All, true };
state.registrations.emplace(component, registration);
state.entityProxies[owner.index].push_back(component);
state.dirtyQueue.push_back({ component, generation });
++state.metrics.publishCalls;
}
static void UnregisterRenderProxy(SceneRenderRegistryState& state,
Component* component)
{
if (nullptr == component) return;
std::scoped_lock lock(state.dirtyMutex);
const auto found = state.registrations.find(component);
if (found == state.registrations.end()) return;
const EntityHandle owner = found->second.owner;
if (owner.index < state.entityProxies.size())
{
auto& proxies = state.entityProxies[owner.index];
std::erase(proxies, component);
}
// Outstanding tickets intentionally remain. Commit validates their captured
// registration generation before touching the pointer, closing pointer reuse ABA.
state.registrations.erase(found);
}
Scene::Scene()
// 씬 식별자(트랙 W)는 생성자에서 딱 한 번 받는다 — Scene은 복사·이동이
// 불가능한 타입이라(Scene.h의 m_sceneId 주석 참고) 이 값이 인스턴스 생애
// 내내 유일하다는 전제가 깨지지 않는다.
: m_sceneId(NextSceneId()),
m_executionGraphs(std::make_unique<TransformExecutionGraphState>()),
m_renderRegistry(std::make_unique<SceneRenderRegistryState>())
{
resetObjHandle = SceneManagers->resetSelectedObjectEvent.AddRaw(this, &Scene::ResetSelectedEntity);
m_Entities.reserve(3000);
m_generations.reserve(3000);
m_hierarchyStore.Reserve(3000);
m_tweenManager = std::make_unique<TweenManager>();
}
TweenManager& Scene::Tweens() noexcept
{
return *m_tweenManager;
}
const TweenManager& Scene::Tweens() const noexcept
{
return *m_tweenManager;
}
// 씬 생성마다 단조 증가하는 일련번호(Scene.h의 m_sceneId 주석 — Skeleton::NextSerial
// 선례와 같은 패턴). 1부터 시작한다 — 0은 EntityHandle의 "무효/미지정"과 겹치면
// 안 되므로 건너뛴다.
uint32_t Scene::NextSceneId()
{
static std::atomic<uint32_t> counter{ 1 };
return counter.fetch_add(1, std::memory_order_relaxed);
}
Scene::~Scene()
{
DrainAIUpdate();
SceneManagers->resetSelectedObjectEvent -= resetObjHandle;
// 생명주기 델리게이트 15종의 Clear 연쇄가 여기 있었다(PHASE 9-3에서 철거).
//
// 종료 행의 자리이기도 했다: Clear가 콜백을 파괴하는데 그 파괴가 같은 델리게이트의
// Remove를 다시 부르면 재진입 불가 스핀락에서 영원히 돌았다(커밋 c712011f).
// 델리게이트가 없으니 그 연쇄 자체가 성립하지 않는다.
//
// 리스트는 비우기만 하면 된다 — 원소가 raw 포인터라 소멸자 연쇄가 없다.
m_schedule.Clear();
m_entityNameSet.clear();
m_renderRegistry->Clear();
m_Entities.clear();
m_generations.clear();
m_freeSlots.clear();
m_hierarchyStore.Clear();
}
const char* ReparentResultName(ReparentResult result)
{
switch (result)
{
case ReparentResult::Success: return "success";
case ReparentResult::NoChange: return "no-change";
case ReparentResult::InvalidHandle: return "invalid-handle";
case ReparentResult::StaleHandle: return "stale-handle";
case ReparentResult::CrossScene: return "cross-scene";
case ReparentResult::RootRejected: return "root-rejected";
case ReparentResult::SelfRejected: return "self-rejected";
case ReparentResult::CycleRejected: return "cycle-rejected";
case ReparentResult::CorruptHierarchy: return "corrupt-hierarchy";
default: return "unknown";
}
}
Scene::HierarchyBulkBuildScope::HierarchyBulkBuildScope(Scene& scene)
: m_scene(&scene)
{
m_scene->EnterHierarchyBulkBuild();
}
Scene::HierarchyBulkBuildScope::~HierarchyBulkBuildScope()
{
Complete();
}
void Scene::HierarchyBulkBuildScope::Complete() noexcept
{
Scene* scene = std::exchange(m_scene, nullptr);
if (scene) scene->ExitHierarchyBulkBuild();
}
Scene::HierarchyBulkBuildScope::HierarchyBulkBuildScope(
HierarchyBulkBuildScope&& other) noexcept
: m_scene(std::exchange(other.m_scene, nullptr))
{
}
const TransformUpdateMetrics& Scene::GetLastTransformUpdateMetrics(
TransformSyncPoint syncPoint) const
{
size_t index = TransformSyncPointIndex(syncPoint);
if (index >= m_transformUpdateMetrics.size())
{
index = TransformSyncPointIndex(TransformSyncPoint::Unspecified);
}
return m_transformUpdateMetrics[index];
}
TransformTopologyMutationCounters Scene::GetTopologyMutationTotals() const
{
return TransformTopologyMutationCounters{
m_topologyCreated.load(std::memory_order_relaxed),
m_topologyDestroyed.load(std::memory_order_relaxed),
m_topologyReparented.load(std::memory_order_relaxed) };
}
TransformTopologyMutationCounters Scene::GetTransformDiagnosticTopologyMutations() const
{
const TransformTopologyMutationCounters totals = GetTopologyMutationTotals();
return TransformTopologyMutationCounters{
totals.created - m_topologyObservationBaseline.created,
totals.destroyed - m_topologyObservationBaseline.destroyed,
totals.reparented - m_topologyObservationBaseline.reparented };
}
void Scene::ResetTransformDiagnostics()
{
for (TransformUpdateMetrics& metrics : m_transformUpdateMetrics)
{
metrics = TransformUpdateMetrics{};
}
m_topologyFrameBaseline = GetTopologyMutationTotals();
m_topologyObservationBaseline = m_topologyFrameBaseline;
m_lastFrameTopologyMutations = {};
m_transformDiagnosticFrameCount = 0;
}
bool Scene::PublishLocalWrite(EntityHandle handle, TransformWriteReason reason)
{
if (nullptr == Resolve(handle))
{
if (IsTransformWriteDiagnosticsEnabled())
m_transformInvalidPublishCount.fetch_add(1, std::memory_order_relaxed);
return false;
}
const size_t reasonIndex = static_cast<size_t>(reason);
if (reasonIndex >= m_transformWriteReasonCounts.size())
{
if (IsTransformWriteDiagnosticsEnabled())
m_transformInvalidPublishCount.fetch_add(1, std::memory_order_relaxed);
return false;
}
{
std::scoped_lock lock(m_executionGraphs->dirtyMutex);
if (IsSparseSpatialResolverEnabled() && !m_executionGraphs->forceFullResolve)
{
if (m_executionGraphs->entityQueuedEpoch.size() < m_Entities.size())
{
m_executionGraphs->entityQueuedEpoch.resize(m_Entities.size(), 0);
m_executionGraphs->entityQueuedGeneration.resize(m_Entities.size(), 0);
}
const size_t slot = handle.index;
if (m_executionGraphs->entityQueuedEpoch[slot] != m_executionGraphs->enqueueEpoch
|| m_executionGraphs->entityQueuedGeneration[slot] != handle.generation)
{
m_executionGraphs->entityQueuedEpoch[slot] = m_executionGraphs->enqueueEpoch;
m_executionGraphs->entityQueuedGeneration[slot] = handle.generation;
m_executionGraphs->dirtyRoots.push_back(handle);
}
// Dense writes stop growing/sorting Q and become one full-root range. 1%
// stays sparse at every X5 benchmark size; dense/full movement does not pay
// O(N log N) sorting before its unavoidable O(N) resolve.
const size_t spatialCount = m_executionGraphs->spatial.execToEntity.size();
const size_t saturation = (std::max)(size_t{ 256 }, spatialCount / 8);
if (spatialCount > 0 && m_executionGraphs->dirtyRoots.size() >= saturation)
{
m_executionGraphs->forceFullResolve = true;
m_executionGraphs->dirtyRoots.clear();
}
}
else if (!IsSparseSpatialResolverEnabled())
{
m_executionGraphs->forceFullResolve = true;
}
if (m_executionGraphs->compiledVersion == GetTopologyVersion()
&& handle.index < m_executionGraphs->spatial.entityToExec.size())
{
const auto exec = m_executionGraphs->spatial.entityToExec[handle.index];
if (TransformExecutionGraphState::kInvalidExec != exec
&& exec < m_executionGraphs->spatial.localEpoch.size())
{
uint64_t& epoch = m_executionGraphs->spatial.localEpoch[exec];
if (0 == ++epoch) ++epoch;
}
}
m_spatialDirtyEpoch.fetch_add(1, std::memory_order_release);
}
if (IsTransformWriteDiagnosticsEnabled())
{
m_transformPublishEpoch.fetch_add(1, std::memory_order_relaxed);
m_transformWriteReasonCounts[reasonIndex].fetch_add(1, std::memory_order_relaxed);
}
return true;
}
uint64_t Scene::PublishLocalWriteBatch(
std::span<const EntityHandle> handles, TransformWriteReason reason)
{
if (handles.empty()) return 0;
const size_t reasonIndex = static_cast<size_t>(reason);
if (reasonIndex >= m_transformWriteReasonCounts.size())
{
if (IsTransformWriteDiagnosticsEnabled())
m_transformInvalidPublishCount.fetch_add(
handles.size(), std::memory_order_relaxed);
return 0;
}
uint64_t accepted = 0;
uint64_t rejected = 0;
for (const EntityHandle handle : handles)
{
if (nullptr != Resolve(handle)) ++accepted;
else ++rejected;
}
if (0 == accepted)
{
if (rejected && IsTransformWriteDiagnosticsEnabled())
m_transformInvalidPublishCount.fetch_add(rejected, std::memory_order_relaxed);
return 0;
}
{
std::scoped_lock lock(m_executionGraphs->dirtyMutex);
if (m_executionGraphs->entityQueuedEpoch.size() < m_Entities.size())
{
m_executionGraphs->entityQueuedEpoch.resize(m_Entities.size(), 0);
m_executionGraphs->entityQueuedGeneration.resize(m_Entities.size(), 0);
}
for (const EntityHandle handle : handles)
{
if (nullptr == Resolve(handle)) continue;
if (IsSparseSpatialResolverEnabled() && !m_executionGraphs->forceFullResolve)
{
const size_t slot = handle.index;
if (m_executionGraphs->entityQueuedEpoch[slot]
!= m_executionGraphs->enqueueEpoch
|| m_executionGraphs->entityQueuedGeneration[slot]
!= handle.generation)
{
m_executionGraphs->entityQueuedEpoch[slot]
= m_executionGraphs->enqueueEpoch;
m_executionGraphs->entityQueuedGeneration[slot]
= handle.generation;
m_executionGraphs->dirtyRoots.push_back(handle);
}
}
else if (!IsSparseSpatialResolverEnabled())
{
m_executionGraphs->forceFullResolve = true;
}
if (m_executionGraphs->compiledVersion == GetTopologyVersion()
&& handle.index < m_executionGraphs->spatial.entityToExec.size())
{
const auto exec =
m_executionGraphs->spatial.entityToExec[handle.index];
if (TransformExecutionGraphState::kInvalidExec != exec
&& exec < m_executionGraphs->spatial.localEpoch.size())
{
uint64_t& epoch = m_executionGraphs->spatial.localEpoch[exec];
if (0 == ++epoch) ++epoch;
}
}
}
const size_t spatialCount = m_executionGraphs->spatial.execToEntity.size();
const size_t saturation = (std::max)(size_t{ 256 }, spatialCount / 8);
if (spatialCount > 0 && m_executionGraphs->dirtyRoots.size() >= saturation)
{
m_executionGraphs->forceFullResolve = true;
m_executionGraphs->dirtyRoots.clear();
}
m_spatialDirtyEpoch.fetch_add(1, std::memory_order_release);
}
if (IsTransformWriteDiagnosticsEnabled())
{
m_transformPublishEpoch.fetch_add(accepted, std::memory_order_relaxed);
m_transformWriteReasonCounts[reasonIndex].fetch_add(
accepted, std::memory_order_relaxed);
if (rejected)
m_transformInvalidPublishCount.fetch_add(rejected, std::memory_order_relaxed);
}
return accepted;
}
void Scene::MarkUILayoutDirty()
{
m_uiDirtyEpoch.fetch_add(1, std::memory_order_release);
}
void Scene::MarkSpatialTransformsDirty()
{
std::scoped_lock lock(m_executionGraphs->dirtyMutex);
m_executionGraphs->forceFullResolve = true;
m_executionGraphs->dirtyRoots.clear();
m_spatialDirtyEpoch.fetch_add(1, std::memory_order_release);
}
TransformWriteMetrics Scene::GetTransformWriteMetrics() const
{
TransformWriteMetrics metrics{};
metrics.publishEpoch = m_transformPublishEpoch.load(std::memory_order_relaxed);
metrics.windowStartEpoch = m_transformWriteEpochBaseline;
metrics.invalidHandle = m_transformInvalidPublishCount.load(std::memory_order_relaxed)
- m_transformInvalidPublishBaseline;
for (size_t i = 0; i < m_transformWriteReasonCounts.size(); ++i)
{
metrics.byReason[i] = m_transformWriteReasonCounts[i].load(
std::memory_order_relaxed) - m_transformWriteReasonBaselines[i];
metrics.total += metrics.byReason[i];
}
return metrics;
}
void Scene::ResetTransformWriteDiagnostics()
{
m_transformWriteEpochBaseline =
m_transformPublishEpoch.load(std::memory_order_relaxed);
m_transformInvalidPublishBaseline =
m_transformInvalidPublishCount.load(std::memory_order_relaxed);
for (size_t i = 0; i < m_transformWriteReasonCounts.size(); ++i)
{
m_transformWriteReasonBaselines[i] =
m_transformWriteReasonCounts[i].load(std::memory_order_relaxed);
}
}
void Scene::CaptureTransformSceneCensus(TransformUpdateMetrics& metrics) const
{
// 인위적인 Scene root(슬롯 0)는 저작 오브젝트 비율에서 제외한다.
for (size_t slot = 1; slot < m_Entities.size(); ++slot)
{
const auto& object = m_Entities[slot];
if (!object || object->IsDestroyMark()) continue;
++metrics.entityCount;
const bool hasTransform = object->HasTransform();
const RectTransformComponent* rect =
object->GetComponent<RectTransformComponent>();
const bool hasRect = nullptr != rect;
if (hasTransform && hasRect) ++metrics.transformAndRectCount;
else if (hasTransform) ++metrics.transformOnlyCount;
else if (hasRect) ++metrics.rectOnlyCount;
else ++metrics.neitherCount;
if (hasTransform && slot < m_transformStore.Size()
&& 0 != m_transformStore.dirty[slot])
{
++metrics.transformDirtyCount;
}
if (rect && rect->IsDirty()) ++metrics.rectDirtyCount;
}
}
void Scene::RecordTopologyCreated()
{
m_topologyCreated.fetch_add(1, std::memory_order_relaxed);
PublishTopologyMutation();
}
void Scene::RecordTopologyDestroyed()
{
m_topologyDestroyed.fetch_add(1, std::memory_order_relaxed);
PublishTopologyMutation();
}
void Scene::RecordTopologyReparented()
{
m_topologyReparented.fetch_add(1, std::memory_order_relaxed);
PublishTopologyMutation();
}
void Scene::PublishTopologyMutation()
{
if (m_hierarchyBulkBuildDepth > 0)
{
m_hierarchyBulkBuildMutated = true;
return;
}
m_topologyVersion.fetch_add(1, std::memory_order_release);
MarkUILayoutDirty();
MarkSpatialTransformsDirty();
}
void Scene::EnterHierarchyBulkBuild()
{
++m_hierarchyBulkBuildDepth;
}
void Scene::ExitHierarchyBulkBuild()
{
if (0 == m_hierarchyBulkBuildDepth) return;
--m_hierarchyBulkBuildDepth;
if (0 != m_hierarchyBulkBuildDepth || !m_hierarchyBulkBuildMutated) return;
m_hierarchyBulkBuildMutated = false;
m_topologyVersion.fetch_add(1, std::memory_order_release);
MarkUILayoutDirty();
MarkSpatialTransformsDirty();
}
// ─────────────────────────────────────────────────────────────────────────────
// 슬롯맵 (SceneGraphRedesignPlan 트랙 E1)
// ─────────────────────────────────────────────────────────────────────────────
//
// 예전에는 DestroyEntities가 파괴마다 생존자 전원의 인덱스를 재부여했다
// (N-6) — 여기 세 함수가 그것을 대체한다. 생존자의 인덱스는 이제 파괴가
// 일어나도 절대 바뀌지 않는다.
Entity::Index Scene::AllocateSlot()
{
if (!m_freeSlots.empty())
{
Entity::Index index = static_cast<Entity::Index>(m_freeSlots.back());
m_freeSlots.pop_back();
return index;
}
Entity::Index index = static_cast<Entity::Index>(m_Entities.size());
m_Entities.push_back(nullptr);
m_generations.push_back(1);
// 트랜스폼 스토어를 슬롯맵과 평행하게 늘린다(트랙 S, S1) — 프리리스트
// 재사용 슬롯은 이미 ReleaseSlot이 초기값으로 되돌려 놨으므로 여기 오지 않는다.
m_transformStore.GrowOne();
m_hierarchyStore.GrowOne();
return index;
}
std::unique_ptr<Entity> Scene::ReleaseSlot(Entity::Index index)
{
// 루트(0)는 씬 자체가 서 있는 동안 절대 해제하지 않는다.
if (index == 0) return {};
if (index < 0 || static_cast<size_t>(index) >= m_Entities.size()) return {};
// ── 여기서 ScriptObjectRegistry를 건드리면 안 된다(SceneGraphRedesignPlan
// 트랙 E4 검토 결과) ──
//
// 이 함수는 DestroyEntities(진짜 파괴)와 DetachEntityHierarchy(DDOL
// 이송 — 오브젝트는 살아서 다른 씬으로 옮겨갈 뿐)가 공유하는 슬롯 해제
// 단일점이다. "슬롯 해제 지점에서 관리 핸들 무효화가 함께 일어난다"가
// 설계 문서의 원칙이라 여기서 스크립트 핸들도 죽이고 싶어질 수 있지만, 그러면
// DDOL 이송 중에도 핸들이 죽는다 — 그리고 그 이송 창(Detach 직후·재부착
// 이전) 동안 실제로 SceneManager::LoadSceneImmediate가
// ClrHost::NotifySceneUnload를 부르고, 그 안에서
// ScriptRegistry.SweepOrphans가 모든 활성 스크립트의 Entity.IsAlive를
// 확인한다(ScriptCore/ScriptRegistry.cs:324, "살아 있다 — DDOL 포함"). 여기서
// 핸들을 무효화하면 살아있는 DDOL 스크립트가 씬 전환마다 고아로 오판되어
// 뜯겨나간다. 스크립트 핸들 무효화의 정본 지점은 대신 이 함수의 **호출부**인
// DestroyEntities다(2026-09-05 재배치 — 그전에는 Entity::Destroy였는데, 그러면
// 축소 삼단이 발화하기 전에 핸들이 죽어 스크립트가 자기 마지막 훅에서 자기
// 오브젝트에 닿지 못했다). DestroyEntities는 파괴 표시된 엔티티만 훑으므로
// DDOL 이송(DetachEntityHierarchy)은 여전히 그 루프를 지나지 않는다.
const bool removedTopologyNode = nullptr != m_Entities[index]
&& m_hierarchyStore.IsOccupied(static_cast<size_t>(index));
std::unique_ptr<Entity> released = std::move(m_Entities[index]);
// 트랜스폼 스토어 슬롯 리셋(트랙 S, S1) — Transform::ResolveStore의 점유자
// 확인이 이 시점부터 실패하므로(m_Entities[index]가 비었다) 이 리셋을
// 하지 않아도 낡은 데이터를 읽을 위험은 없지만, 다음 입주자가 재사용
// 슬롯을 잡았을 때 곧바로 깨끗한 값을 보게 여기서 미리 되돌려 둔다.
m_transformStore.ResetSlot(static_cast<size_t>(index));
m_hierarchyStore.ResetSlot(static_cast<size_t>(index));
// 세대 0은 EntityHandle의 "무효"와 겹치므로 건너뛴다.
++m_generations[index];
if (0 == m_generations[index])
{
m_generations[index] = 1;
}
m_freeSlots.push_back(static_cast<uint32_t>(index));
if (removedTopologyNode)
{
RecordTopologyDestroyed();
}
return released;
}
void Scene::SerializeEntityHierarchy(const Entity& entity, const Authoring::MutableNodeView& view) const
{
const Authoring::WriteNode node = Authoring::MutableNodeViewAccess::Node(view);
if (!Entity::IsValidIndex(entity.m_index)) return;
const size_t index = static_cast<size_t>(entity.m_index);
if (index >= m_Entities.size() || m_Entities[index].get() != &entity) return;
if (!m_hierarchyStore.IsOccupied(index)) return;
// 디스크 스키마는 H3 이전과 동일하게 유지한다. 달라진 것은 값의 출처다:
// Entity 멤버가 아니라 Scene-owned Store에서 세 키를 보충한다.
node.Child("m_parentIndex").SetScalar(m_hierarchyStore.ParentOf(index));
node.Child("m_rootIndex").SetScalar(m_hierarchyStore.RootOf(index));
const Authoring::WriteNode children = node.Child("m_childrenIndices");
children.SetSequence(true);
for (Entity::Index child : m_hierarchyStore.ChildrenOf(index))
{
children.Append().SetScalar(child);
}
}
size_t Scene::CountHierarchyStoreMismatches() const
{
size_t mismatches = 0;
if (m_hierarchyStore.Size() != m_Entities.size())
++mismatches;
for (size_t index = 0; index < m_Entities.size(); ++index)
{
const bool hasEntity = static_cast<bool>(m_Entities[index]);
if (hasEntity != m_hierarchyStore.IsOccupied(index)) ++mismatches;
}
return mismatches;
}
void Scene::DrainAIUpdate()
{
if (m_AIFuture.valid())
m_AIFuture.get();
}
void Scene::UnlinkFromParentChildren(Entity::Index index)
{
if (!Entity::IsValidIndex(index) || static_cast<size_t>(index) >= m_Entities.size())
return;
auto& node = m_Entities[index];
if (!node) return;
const Entity::Index parentIndex = node->GetParentIndex();
if (Entity::IsValidIndex(parentIndex))
{
if (Entity* parent = TryGetEntity(parentIndex))
{
parent->DetachChildIndex(index);
}
}
// 최상위 오브젝트는 부모 인덱스가 무효인 채로 씬 루트의 children에만 들어
// 있다(N-13 이전부터의 관례) — 그래서 무조건 한 번 더 시도한다.
if (!m_Entities.empty() && m_Entities[0])
{
m_Entities[0]->DetachChildIndex(index);
}
}
Entity* Scene::Resolve(EntityHandle handle) const
{
if (!handle.IsValid()) return nullptr;
// 씬 스코프 검사(트랙 W) — index+generation이 우연히 맞아도 다른 씬 것이면
// 즉시 거른다. m_generations/m_Entities는 씬마다 독립이라 이 검사
// 없이는 "다른 씬의 같은 슬롯"을 구조적으로 막을 수 없다(EntityHandle.h
// 상단 주석 참고).
if (handle.sceneId != m_sceneId) return nullptr;
if (handle.index >= m_generations.size()) return nullptr;
if (m_generations[handle.index] != handle.generation) return nullptr;
if (handle.index >= m_Entities.size()) return nullptr;
return m_Entities[handle.index].get();
}
EntityHandle Scene::HandleOf(Entity::Index index) const
{
if (index < 0 || static_cast<size_t>(index) >= m_generations.size())
return EntityHandle{};
if (static_cast<size_t>(index) >= m_Entities.size() || !m_Entities[index])
return EntityHandle{};
return EntityHandle{ m_sceneId, static_cast<uint32_t>(index), m_generations[index] };
}
ReparentResult Scene::Reparent(EntityHandle childHandle, EntityHandle newParentHandle)
{
if (!childHandle.IsValid() || !newParentHandle.IsValid())
return ReparentResult::InvalidHandle;
if (childHandle.sceneId != m_sceneId || newParentHandle.sceneId != m_sceneId)
return ReparentResult::CrossScene;
Entity* child = Resolve(childHandle);
Entity* newParent = Resolve(newParentHandle);
if (!child || !newParent) return ReparentResult::StaleHandle;
if (child->m_index == Entity::kSceneRootIndex)
return ReparentResult::RootRejected;
if (child == newParent) return ReparentResult::SelfRejected;
const Entity::Index childIndex = child->m_index;
const Entity::Index newParentIndex = newParent->m_index;
if (!m_hierarchyStore.IsOccupied(static_cast<size_t>(childIndex))
|| !m_hierarchyStore.IsOccupied(static_cast<size_t>(newParentIndex)))
{
return ReparentResult::CorruptHierarchy;
}
std::unordered_set<Entity::Index> ancestors;
Entity::Index cursor = newParentIndex;
while (Entity::IsValidIndex(cursor))
{
if (cursor == childIndex) return ReparentResult::CycleRejected;
if (!ancestors.insert(cursor).second) return ReparentResult::CorruptHierarchy;
if (cursor < 0 || static_cast<size_t>(cursor) >= m_Entities.size()
|| !m_Entities[cursor]
|| !m_hierarchyStore.IsOccupied(static_cast<size_t>(cursor)))
{
return ReparentResult::CorruptHierarchy;
}
cursor = m_hierarchyStore.ParentOf(static_cast<size_t>(cursor));
}
const Entity::Index oldParentIndex =
m_hierarchyStore.ParentOf(static_cast<size_t>(childIndex));
const auto& newParentChildren =
m_hierarchyStore.ChildrenOf(static_cast<size_t>(newParentIndex));
const size_t newParentOccurrences = static_cast<size_t>(std::count(
newParentChildren.begin(), newParentChildren.end(), childIndex));
if (oldParentIndex == newParentIndex && 1 == newParentOccurrences)
return ReparentResult::NoChange;
if (Entity::IsValidIndex(oldParentIndex))
{
if (oldParentIndex < 0
|| static_cast<size_t>(oldParentIndex) >= m_Entities.size()
|| !m_Entities[oldParentIndex]
|| !m_hierarchyStore.IsOccupied(static_cast<size_t>(oldParentIndex)))
{
return ReparentResult::CorruptHierarchy;
}
m_hierarchyStore.DetachChild(static_cast<size_t>(oldParentIndex), childIndex);
}
// Commit order is deliberately detach -> parent -> attach. All validation has
// completed above, so no failure path can expose a half-written relationship.
child->SetParentIndex(newParentIndex);
m_hierarchyStore.AttachChild(static_cast<size_t>(newParentIndex), childIndex);
if (Entity::IsValidIndex(oldParentIndex) && oldParentIndex != newParentIndex)
RecordTopologyReparented();
else
PublishTopologyMutation();
return ReparentResult::Success;
}
HierarchyIntegrityMetrics Scene::GetHierarchyIntegrityMetrics() const
{
HierarchyIntegrityMetrics metrics{};
std::vector<uint32_t> listedCount(m_Entities.size(), 0);
for (size_t parentIndex = 0; parentIndex < m_Entities.size(); ++parentIndex)
{
if (!m_Entities[parentIndex]) continue;
if (!m_hierarchyStore.IsOccupied(parentIndex))
{
++metrics.invalidReference;
continue;
}
std::unordered_set<Entity::Index> localChildren;
for (Entity::Index childIndex : m_hierarchyStore.ChildrenOf(parentIndex))
{
if (!localChildren.insert(childIndex).second)
++metrics.duplicateChild;
if (childIndex < 0 || static_cast<size_t>(childIndex) >= m_Entities.size()
|| !m_Entities[childIndex]
|| !m_hierarchyStore.IsOccupied(static_cast<size_t>(childIndex)))
{
++metrics.invalidReference;
continue;