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
//! ECSワールド管理
//!
//! - schedule_labels: フレームカウンタとスケジュールラベル定義
//! - vsync: VSYNC優先レンダリングと再入防止ガード
mod vsync;
pub mod schedule_labels;
pub mod thread_registry;
pub mod tick_diag;
pub mod tick_gate;
pub mod tick_wake;
pub use schedule_labels::*;
pub use vsync::*;
use bevy_ecs::message::Messages;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::*;
use bevy_ecs::system::*;
use std::cell::RefCell;
use std::rc::Weak;
use std::time::Instant;
use tracing::{error, info, trace};
use windows::Win32::Foundation::HWND;
/// 外側 `Rc<RefCell<EcsWorld>>` への弱参照を保持する NonSend リソース(task 4.3)。
///
/// `create_windows`(排他システム)は bevy の `&mut World` しか持たないが、ウィンドウ生成
/// ファクトリ(runtime 層 `EcsWindowFactory`)は wndproc クロージャの `WndState` 用に
/// **外側**の `Weak<RefCell<EcsWorld>>`(共有 World ハンドルの弱参照)を必要とする。
///
/// 本リソースを **ECS 層**で定義することで `create_windows` が読むのは ECS 型のまま
/// (ecs→runtime の上向き依存を作らない)。`Weak` を保持する `!Send` 型ゆえ bevy が
/// 自動的に NonSend として扱う(`Send` 偽装はしない)。
///
/// 注入は `WinApp`(runtime)が `new()`/`run()` 開始時に `Rc::downgrade(&self.world)` を
/// insert する(facade から下向き)。`create_windows` はこのリソースの有無で新経路
/// (ファクトリ生成)/ 旧 `WinThreadMgr` 経路(no-op)を分岐する(task 4.3 解釈 (1)/(2))。
pub struct EcsWorldSelfRef(pub Weak<RefCell<EcsWorld>>);
/// ECSワールドのラッパー
/// 初期化ロジックや拡張機能をここに集約
pub struct EcsWorld {
world: World,
has_systems: bool,
message_window: Option<HWND>,
// デバッグ用: フレームレート計測
frame_count: u64,
last_log_time: Option<Instant>,
/// フレーム駆動の相別観測(既定 OFF・`RUST_LOG` に `wintf::tick=debug` で点く)。
tick_diag: tick_diag::TickDiag,
/// tick の門が有効か(既定 **無効**=門を入れる前と同じ挙動)。
///
/// 既定値は周 1 の A/B(既定 ON 対 既定 OFF)で決める(設計 C16)。areka は起動時に
/// `AREKA_TICK_GATE=1|0` を読んで [`set_tick_gate`](Self::set_tick_gate) で上書きできる。
tick_gate_enabled: bool,
/// 起動からの画面更新の回数(**省略した回も含む**・起動直後かどうかの物差し)。
frames_since_boot: u32,
/// 前回**回した** tick からの画面更新の回数(心拍の物差し)。
frames_since_run: u32,
/// 直前の判断が心拍だったか(観測行の `heartbeat=` に載せるための印)。
///
/// 判断が置き、`try_tick_world` が読んで倒す。門を通さない呼び出し(旧経路・テスト)が
/// 前回の印を引き継がないよう、読んだ側で必ず倒す。
last_run_was_heartbeat: bool,
}
impl EcsWorld {
/// 新しいEcsWorldを作成
pub fn new() -> Self {
let mut world = World::new();
// リソースの初期化
world.insert_resource(crate::ecs::app::App::new());
world.insert_resource(FrameCount::default());
world.insert_resource(crate::ecs::layout::taffy::TaffyLayoutResource::default());
// Surface生成統計リソース(Req 5.3)
world.insert_resource(crate::ecs::graphics::SurfaceCreationStats::default());
// WicCore初期化(Device Lostの影響を受けない独立リソース)
// 生成失敗は無言スキップにせず error! で記録する(Req 9.4・log-first)。
// factory 無しでも縮退継続する(画像デコード不可のまま return しない)。
match crate::ecs::widget::bitmap_source::WicCore::new() {
Ok(wic_core) => {
world.insert_resource(wic_core);
}
Err(e) => {
error!(
error = %e,
"WicCore の生成に失敗(WIC factory 未取得)。画像デコード機能を欠いたまま継続する"
);
}
}
// FrameTime初期化(dola::runtime::clock::now() の値を保持)
world.insert_resource(crate::ecs::graphics::FrameTime(dola::runtime::clock::now()));
// TickStart初期化(遷移観測の刻印の時刻基準・D1)
// 最初の tick で FrameCount と同じ点から更新されるが、tick 前に World を借りる
// 観測点が資源を引けるよう構築時に置いておく。
world.insert_resource(crate::ecs::window::transition_diag::TickStart(
Instant::now(),
));
// WintfTaskPool初期化(非同期タスク実行基盤)
world.insert_resource(crate::ecs::widget::bitmap_source::WintfTaskPool::new());
// LayoutRootとMonitor階層を初期化(Window spawnより前に必要)
crate::ecs::layout::initialize_layout_root(&mut world);
// ドラッグ累積器の登録
crate::ecs::drag::install_drag_accumulator(&mut world);
// ドラッグコンテキスト(ECS→wndprocスレッド間転送用)の登録
world.insert_resource(crate::ecs::drag::WindowDragContextResource::new());
// イベントの登録
world.init_resource::<Messages<crate::ecs::drag::DragStartEvent>>();
world.init_resource::<Messages<crate::ecs::drag::DragEvent>>();
world.init_resource::<Messages<crate::ecs::drag::DragEndEvent>>();
// スケジュールの登録
{
world.init_resource::<Schedules>();
let mut schedules = world.resource_mut::<Schedules>();
schedules.insert(Schedule::new(Input));
schedules.insert(Schedule::new(Update));
schedules.insert(Schedule::new(PreLayout));
schedules.insert(Schedule::new(Layout));
schedules.insert(Schedule::new(PostLayout));
// UISetupだけメインスレッド固定
{
let mut sc = Schedule::new(UISetup);
sc.set_executor(SingleThreadedExecutor::new());
schedules.insert(sc);
}
// WUC(Windows.UI.Composition)はスレッド親和 API であり、かつ DispatcherQueue は
// 「メッセージポンプが回るスレッド」に居ないと合成 commit が永遠に flush されない。
// try_tick_world は UI スレッド(AsyncTickTask の spawn_local)から呼ばれるため、
// WUC オブジェクトに触る schedule を SingleThreaded executor に固定して
// 「全 WUC 呼び出し=UI スレッド」を構造的に保証する。
//
// 背景(2026-07-02 実測): DComp は free-threaded で MultiThreaded executor
// (worker スレッド実行)でも動作していたが、WUC 移行でこの前提が崩れた。
// worker スレッドで CreateDispatcherQueueController(DQTYPE_THREAD_CURRENT) が走ると
// DispatcherQueue がポンプの無い worker に紐づき、エラーを一切出さないまま
// 画面へ何も合成されない(dcomp_demo: 窓は visible・SetBrush/SetRoot 全成功・
// 内容だけ出ない/ECS 非依存でメインスレッド生成の wuc_spike: 表示される、で切り分け確定)。
{
let mut sc = Schedule::new(GraphicsSetup);
sc.set_executor(SingleThreadedExecutor::new());
schedules.insert(sc);
}
schedules.insert(Schedule::new(Draw));
{
let mut sc = Schedule::new(PreRenderSurface);
sc.set_executor(SingleThreadedExecutor::new());
schedules.insert(sc);
}
{
let mut sc = Schedule::new(RenderSurface);
sc.set_executor(SingleThreadedExecutor::new());
schedules.insert(sc);
}
{
let mut sc = Schedule::new(Composition);
sc.set_executor(SingleThreadedExecutor::new());
schedules.insert(sc);
}
{
let mut sc = Schedule::new(CommitComposition);
sc.set_executor(SingleThreadedExecutor::new());
schedules.insert(sc);
}
schedules.insert(Schedule::new(FrameFinalize));
}
// デフォルトシステムの登録
{
let mut schedules = world.resource_mut::<Schedules>();
// Inputスケジュール: 非同期タスク完了コマンドのドレイン
schedules.add_systems(
Input,
crate::ecs::widget::bitmap_source::systems::drain_task_pool_commands,
);
// Inputスケジュール: ポインターイベントディスパッチ
// transfer_buffers_to_world()でWorldに直接データ投入済み
schedules.add_systems(
Input,
crate::ecs::pointer::dispatch_pointer_events
.after(crate::ecs::widget::bitmap_source::systems::drain_task_pool_commands),
);
// 注: process_pointer_buffersは廃止
// WndProcスレッドのthread_localバッファは、try_tick_world()内の
// transfer_buffers_to_world()で直接Worldに転送される
// Inputスケジュール: ドラッグイベントディスパッチ
schedules.add_systems(
Input,
crate::ecs::drag::dispatch_drag_events
.after(crate::ecs::pointer::dispatch_pointer_events),
);
// Inputスケジュール: ドラッグ状態クリーンアップ(dispatch_drag_eventsの後)
schedules.add_systems(
Input,
crate::ecs::drag::cleanup_drag_state.after(crate::ecs::drag::dispatch_drag_events),
);
// Inputスケジュール: ポインターデバッグ監視(デバッグビルドのみ)
#[cfg(debug_assertions)]
schedules.add_systems(
Input,
(
crate::ecs::pointer::debug_pointer_state_changes,
crate::ecs::pointer::debug_pointer_leave,
)
.after(crate::ecs::pointer::dispatch_pointer_events),
);
// UISetupスケジュール:ウィンドウ作成とWindowPos反映
schedules.add_systems(UISetup, crate::ecs::window::window_system::create_windows);
// on_window_handle_addedとon_window_handle_removedはフックで代替
// WindowPos変更をSetWindowPosに反映(メインスレッド固定が必要)
schedules.add_systems(UISetup, crate::ecs::graphics::apply_window_pos_changes);
// Updateスケジュールにディスプレイ構成変更検知とモニター階層管理を登録
schedules.add_systems(
Update,
(
// ディスプレイ構成変更検知
crate::ecs::layout::detect_display_change_system,
// Monitorレイアウト更新(detect_display_change_systemの後)
crate::ecs::layout::update_monitor_layout_system
.after(crate::ecs::layout::detect_display_change_system),
// 依存コンポーネント無効化
crate::ecs::graphics::invalidate_dependent_components
.after(crate::ecs::layout::update_monitor_layout_system),
// Typewriter更新(アニメーション進行)
crate::ecs::widget::text::update_typewriters
.after(crate::ecs::graphics::invalidate_dependent_components),
)
.chain(),
);
// PreLayoutスケジュール: GraphicsCore初期化
// Phase 2: DCompシステム(visual_resource_management_system, visual_hierarchy_sync_system)を除去
// init_graphics_coreのみ残存(単独登録、chain不要)
schedules.add_systems(PreLayout, crate::ecs::graphics::init_graphics_core);
// Layoutスケジュールにtaffyレイアウトシステムを登録
schedules.add_systems(
Layout,
(
crate::ecs::layout::build_taffy_styles_system,
crate::ecs::layout::sync_taffy_tree_system
.after(crate::ecs::layout::build_taffy_styles_system),
crate::ecs::layout::compute_taffy_layout_system
.after(crate::ecs::layout::sync_taffy_tree_system),
crate::ecs::layout::update_arrangements_system
.after(crate::ecs::layout::compute_taffy_layout_system),
crate::ecs::layout::cleanup_removed_entities_system,
)
.chain(),
);
// PostLayoutスケジュール: 論理計算系(Arrangement伝播まで)
// Note: Arrangementは各コンポーネントのon_addフックで自動挿入されるため、
// init_window_arrangementシステムは廃止されました。
schedules.add_systems(
PostLayout,
(
crate::ecs::layout::sync_window_arrangement_from_window_pos,
crate::ecs::layout::sync_simple_arrangements,
crate::ecs::layout::mark_dirty_arrangement_trees
.after(crate::ecs::layout::sync_simple_arrangements),
crate::ecs::layout::propagate_global_arrangements
.after(crate::ecs::layout::mark_dirty_arrangement_trees),
crate::ecs::layout::window_pos_sync_system
.after(crate::ecs::layout::propagate_global_arrangements),
)
.chain(),
);
// GraphicsSetupスケジュール: WUC(Windows.UI.Composition)合成スタック初期化
// init_window_graphics: Window に WindowGraphics を作成(単独登録、chain不要)
schedules.add_systems(GraphicsSetup, crate::ecs::graphics::init_window_graphics);
// Drawスケジュール: ウィジェット描画システム
// Phase 2: DCompシステム(deferred_surface_creation_system, cleanup_surface_on_commandlist_removed)を除去
// chainはgenerate_alpha_mask_systemまで維持
schedules.add_systems(
Draw,
(
// Brush継承解決(描画システムより前に実行)
crate::ecs::graphics::resolve_inherited_brushes,
crate::ecs::widget::shapes::rectangle::draw_rectangles
.after(crate::ecs::graphics::resolve_inherited_brushes),
crate::ecs::widget::text::draw_labels
.after(crate::ecs::graphics::resolve_inherited_brushes),
// Typewriter: Arrangement変更で無効化 → LayoutCache初期化 → 描画の順
crate::ecs::widget::text::invalidate_typewriter_layout_on_arrangement_change
.after(crate::ecs::graphics::resolve_inherited_brushes),
crate::ecs::widget::text::init_typewriter_layout
.after(crate::ecs::widget::text::invalidate_typewriter_layout_on_arrangement_change),
crate::ecs::widget::text::draw_typewriters
.after(crate::ecs::widget::text::init_typewriter_layout),
// 空トーク時の背景描画(draw_typewritersの後)
crate::ecs::widget::text::draw_typewriter_backgrounds
.after(crate::ecs::widget::text::draw_typewriters),
crate::ecs::widget::bitmap_source::draw_bitmap_sources
.after(crate::ecs::graphics::resolve_inherited_brushes),
// αマスク生成(draw_bitmap_sourcesの後、BitmapSourceResource追加検出時に実行)
crate::ecs::widget::bitmap_source::generate_alpha_mask_system
.after(crate::ecs::widget::bitmap_source::draw_bitmap_sources),
)
.chain(),
);
// PreRenderSurface: DComp パイプライン用 Visual/Surface リソース管理
schedules.add_systems(
PreRenderSurface,
(
crate::ecs::graphics::visual_manager::visual_resource_management_system,
crate::ecs::graphics::deferred_surface_creation_system.after(
crate::ecs::graphics::visual_manager::visual_resource_management_system,
),
crate::ecs::graphics::mark_dirty_surfaces
.after(crate::ecs::graphics::deferred_surface_creation_system),
crate::ecs::graphics::cleanup_surface_on_commandlist_removed
.after(crate::ecs::graphics::mark_dirty_surfaces),
crate::ecs::graphics::visual_manager::window_visual_integration_system
.after(crate::ecs::graphics::cleanup_surface_on_commandlist_removed),
),
);
// RenderSurface: DComp パイプライン用 Surface 描画
schedules.add_systems(RenderSurface, crate::ecs::graphics::render_surface);
// Compositionスケジュール: WUC Visual階層同期(合成 commit は WUC 側で暗黙反映)
// visual_hierarchy_sync → visual_property_sync → clip_sync
schedules.add_systems(
Composition,
(
crate::ecs::graphics::visual_hierarchy_sync_system,
crate::ecs::graphics::visual_property_sync_system
.after(crate::ecs::graphics::visual_hierarchy_sync_system),
crate::ecs::graphics::clip_sync_system
.after(crate::ecs::graphics::visual_property_sync_system),
),
);
// CommitComposition: 現状システム無し(空スケジュール)
// Note: WUC への合成 commit は暗黙反映のため専用システム不要(要件 7.1)。旧 ULW 画面転送
// システムは撤去済み。schedule ラベルと try_run_schedule 呼び出しは tick 構成不変
// (要件 4.5 / 13 本固定順)を保つため残置し、zero-system の no-op として回す。
// FrameFinalizeスケジュール: 一時的ポインター状態クリア
schedules.add_systems(
FrameFinalize,
crate::ecs::pointer::clear_transient_pointer_state,
);
// FrameFinalizeスケジュール: ドラッグ中は次の画面更新を予約(tick の末尾)
schedules.add_systems(FrameFinalize, crate::ecs::drag::rearm_tick_while_dragging);
// FrameFinalizeスケジュール: Messagesの更新
schedules.add_systems(
FrameFinalize,
(
|world: &mut World| {
world
.resource_mut::<Messages<crate::ecs::drag::DragStartEvent>>()
.update()
},
|world: &mut World| {
world
.resource_mut::<Messages<crate::ecs::drag::DragEvent>>()
.update()
},
|world: &mut World| {
world
.resource_mut::<Messages<crate::ecs::drag::DragEndEvent>>()
.update()
},
),
);
}
// ツールチップ(資源と FrameFinalize の判定)の登録
crate::ecs::tooltip::install(&mut world);
Self {
world,
has_systems: true, // デフォルトシステムがあるのでtrue
message_window: None,
frame_count: 0,
last_log_time: None,
tick_diag: tick_diag::TickDiag::default(),
tick_gate_enabled: false,
frames_since_boot: 0,
frames_since_run: 0,
last_run_was_heartbeat: false,
}
}
/// メッセージウィンドウのHWNDを設定(格納のみ)。
///
/// NOTE: 旧経路では App リソースへも伝播していたが、`App::set_message_window`/
/// `WM_LAST_WINDOW_DESTROYED` PostMessage 経路の撤去(task 4.5)に伴い、本メソッドは
/// EcsWorld 側の HWND 格納のみを担う。
pub fn set_message_window(&mut self, hwnd: HWND) {
self.message_window = Some(hwnd);
}
/// メッセージウィンドウのHWNDを取得
pub fn message_window(&self) -> Option<HWND> {
self.message_window
}
/// Schedulesリソースへのアクセスを提供
pub fn schedules_mut(&mut self) -> Mut<'_, Schedules> {
self.has_systems = true;
self.world.resource_mut::<Schedules>()
}
/// 指定したスケジュールにシステムを追加
///
/// # Example
/// ```ignore
/// world.add_systems(Update, my_system);
/// ```
pub fn add_systems<M>(
&mut self,
schedule: impl ScheduleLabel,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) {
self.has_systems = true;
self.world
.resource_mut::<Schedules>()
.add_systems(schedule, systems);
}
/// 内部のWorldへの参照を取得
pub fn world(&self) -> &World {
&self.world
}
/// 内部のWorldへの可変参照を取得
pub fn world_mut(&mut self) -> &mut World {
&mut self.world
}
/// 非同期タスクを生成(WintfTaskPool経由)
///
/// タスク内で`CommandSender`を使ってECSコマンドを送信できる。
/// コマンドはInputスケジュールで自動的にWorldに適用される。
///
/// # Example
/// ```ignore
/// ecs_world.spawn(|tx| async move {
/// let result = some_async_work().await;
/// let cmd: BoxedCommand = Box::new(move |world: &mut World| {
/// // worldを操作
/// });
/// let _ = tx.send(cmd);
/// });
/// ```
pub fn spawn<F, Fut>(&self, f: F)
where
F: FnOnce(crate::ecs::widget::bitmap_source::CommandSender) -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + Send,
{
if let Some(task_pool) = self
.world
.get_resource::<crate::ecs::widget::bitmap_source::WintfTaskPool>()
{
task_pool.spawn(f);
}
}
/// フレームレート計測とログ出力(デバッグ用)
/// 10秒ごとにフレームレートをログ出力
fn measure_and_log_framerate(&mut self) {
self.frame_count += 1;
let now = Instant::now();
if let Some(last_log) = self.last_log_time {
let elapsed = now.duration_since(last_log);
if elapsed.as_secs() >= 10 {
let fps = self.frame_count as f64 / elapsed.as_secs_f64();
let avg_frame_time = elapsed.as_secs_f64() * 1000.0 / self.frame_count as f64;
trace!(
fps,
frame_count = self.frame_count,
elapsed_secs = format_args!("{:.2}", elapsed.as_secs_f64()),
avg_frame_time_ms = format_args!("{:.2}", avg_frame_time),
"Frame rate measurement"
);
self.frame_count = 0;
self.last_log_time = Some(now);
}
} else {
// 初回
self.last_log_time = Some(now);
}
}
// ────────────────────────────────────── tick の門(設計 C16・要件 3.2/3.7)
/// tick の門の有効を切り替える(起動時に 1 度・`AREKA_TICK_GATE` の上書き口)。
///
/// 無効なら判断そのものを行わず必ず回す(=門を入れる前と同じ挙動)。切り替えは
/// 起動時の 1 回を想定しているので、呼ばれたことを 1 行残す。
pub fn set_tick_gate(&mut self, enabled: bool) {
self.tick_gate_enabled = enabled;
info!("[set_tick_gate] tick gate enabled={enabled}");
}
/// tick の門が有効か。
pub fn tick_gate_enabled(&self) -> bool {
self.tick_gate_enabled
}
/// 数えている 2 つの回数(起動から, 前回回した tick から)。
///
/// 観測と決定論テストのための読み出し口で、判断には使わない。
pub fn tick_gate_counters(&self) -> (u32, u32) {
(self.frames_since_boot, self.frames_since_run)
}
/// 直前の判断が心拍だったか(観測行の `heartbeat=` へ載る印)。
#[cfg(test)]
pub(crate) fn last_run_was_heartbeat(&self) -> bool {
self.last_run_was_heartbeat
}
/// 画面更新 1 回ぶんの判断(旗を読んで倒す → 純関数 → 数を数える)。
///
/// 旗の読み取りは原子的な swap([`tick_wake::take`])で、**失敗しうる経路が無い**
/// ——錠を取らず、OS も呼ばず、`Result` も返さない。したがってここに `Err` の腕は
/// 存在しない。将来ここへ失敗しうる経路(別プロセスからの読み出し・OS 問い合わせ
/// など)を足すときは、要件 3.7 に従い **`error!` を残して「回す」側**
/// ([`TickDecision::Run`](tick_gate::TickDecision::Run)`(`[`RunReason::Disabled`](tick_gate::RunReason::Disabled)`)`)
/// へ倒すこと——黙って省略へ倒してはならない。作り物の `Result` は返さない。
pub fn decide_tick(&mut self, now: Instant) -> tick_gate::TickDecision {
self.decide_tick_with(tick_wake::take(now))
}
/// 読み取り済みの旗から判断する([`decide_tick`](Self::decide_tick) の中身)。
///
/// 旗を注入できるので、プロセス共有の実体に触れずに決定論テストが書ける。
/// 判断そのものは純関数 [`tick_gate::should_run`] に閉じており、ここがするのは
/// 入力を組むことと、答えに応じて 2 つの回数を進めることだけである。
pub fn decide_tick_with(
&mut self,
snapshot: tick_wake::WakeSnapshot,
) -> tick_gate::TickDecision {
let inputs = tick_gate::TickGateInputs::from_snapshot(
&snapshot,
self.frames_since_run,
self.frames_since_boot,
self.tick_gate_enabled,
);
let decision = tick_gate::should_run(&inputs);
// 起動からの回数は省略した回も含めて進む(起動直後の物差しなので頭打ちで十分)。
self.frames_since_boot = self.frames_since_boot.saturating_add(1);
match decision {
tick_gate::TickDecision::Run(reason) => {
self.frames_since_run = 0;
self.last_run_was_heartbeat = matches!(reason, tick_gate::RunReason::Heartbeat);
}
tick_gate::TickDecision::Skip => {
self.frames_since_run = self.frames_since_run.saturating_add(1);
}
}
decision
}
/// 省略した画面更新を 1 件記録する(門が [`Skip`](tick_gate::TickDecision::Skip) を返した回)。
///
/// `FrameCount`/`FrameTime`/`TickStart` は**進めない**し、スケジュールも 1 本も
/// 回さない。観測が点いているときだけ省略の数を積み、窓が閉じていれば 1 行出す
/// ——省略が長く続く間も窓は閉じる(`ticks=0` の窓は正当な観測結果である)。
/// 観測が消えているときは時刻を 1 度も取らない(要件 3.8)。
pub fn note_skipped_tick(&mut self) {
if !tick_diag::is_enabled() {
return;
}
let now = Instant::now();
self.tick_diag.record_skipped(now);
if let Some(line) = self.tick_diag.maybe_close(now) {
tick_diag::emit_line(&line);
}
}
/// システムを1回実行
/// システムが実行された場合はtrueを返す
pub fn try_tick_world(&mut self) -> bool {
// デバッグ: フレームレート計測(不要になったらコメントアウト)
self.measure_and_log_framerate();
// 相別観測の前置ガード(要件 3.8)。tick 1 回につきここで 1 度だけ評価し、
// 以後はこの bool を配る。偽のときこの観測のために時刻を取らず、行も組まない
// (兄弟テストが「ガードは最初の時刻取得より前」を本文の並びで固定する)。
let diag_on = tick_diag::is_enabled();
// システムが登録されていない場合はスキップ
if !self.has_systems {
return false;
}
// FrameCount をインクリメント、FrameTime を更新
//
// 整数境界(W7b-V 点検): FrameCount(u32) は毎 tick +1 で増加し、~60Hz では
// 2^32 到達に約828日の連続 tick を要する。到達時 debug ビルドは加算オーバーフローで
// panic、release はラップする。FrameCount.0 の消費は (1) 多数の tracing ログの
// `frame=` フィールドと (2) graphics/systems/surface.rs::mark_dirty_surfaces での
// `dirty.requested_frame = frame_count.0 as u64`(Changed<SurfaceGraphicsDirty>
// 検出をトリガーするための「変化ノンス」用途で、値自体は本番では読まれない。本番読み取りは
// 皆無で grep 上 graphics/tests.rs のみ)のみ。後者はラップ MAX→0 でも「前回値と異なる」
// 性質を満たすため Changed 検出は継続し、いずれの消費も算術上の大小比較・配列添字には
// 用いられないため、ラップしても観測挙動はログ値が 0 に戻るだけで正当性に影響しない。
// debug panic 化を避ける堅牢化(saturating/wrapping_add)は debug 挙動を変えるため
// P66 として記録。
//
// 遷移観測の刻印(D1): フレーム番号の権威はここで進む 1 系列だけである。
// 同一点で Resource `TickStart` を更新し、同じ値をスレッド局所の写しへ配る
// (`transition_diag::begin_tick`)。World を借りられる観測点は資源から、
// 借りられない観測点(一括 flush・wndproc)だけが写しから刻印を組む。
// スケジュール構成(13 本の順序)はここでは変えない。
let tick_start = Instant::now();
let frame = if let Some(mut frame_count) = self.world.get_resource_mut::<FrameCount>() {
frame_count.0 += 1;
frame_count.0
} else {
// FrameCount を持たない World(`EcsWorld::new` 以外で組まれた場合)では
// 番号を進める先が無いので 0 のまま配る(資源と写しが食い違わない)。
0
};
match self
.world
.get_resource_mut::<crate::ecs::window::transition_diag::TickStart>()
{
Some(mut start) => start.0 = tick_start,
None => self
.world
.insert_resource(crate::ecs::window::transition_diag::TickStart(tick_start)),
}
crate::ecs::window::transition_diag::begin_tick(frame, tick_start);
if let Some(mut frame_time) = self
.world
.get_resource_mut::<crate::ecs::graphics::FrameTime>()
{
frame_time.0 = dola::runtime::clock::now();
}
// WndProcスレッドのthread_localバッファからWorldに直接データを投入
// これにより、マルチスレッドで実行されるシステムでもデータにアクセス可能になる
crate::ecs::pointer::transfer_buffers_to_world(&mut self.world);
// 相別観測(既定 OFF): 点いているときだけ UI スレッドのハンドルを 1 度確保する。
if diag_on {
self.tick_diag.ensure_ui_thread();
}
// 各Scheduleを順番に実行
//
// 13 本の呼び出しと順序は不変(要件 3.4/既存の順序テスト 2 本)。間に挟まる
// `phase.lap()` は相別観測の区切りで、OFF のときは何もしない(時刻も取らない)。
let mut phase = tick_diag::PhaseTimer::start(diag_on);
let _ = self.world.try_run_schedule(Input);
phase.lap();
let _ = self.world.try_run_schedule(Update);
phase.lap();
let _ = self.world.try_run_schedule(PreLayout);
phase.lap();
let _ = self.world.try_run_schedule(Layout);
phase.lap();
let _ = self.world.try_run_schedule(PostLayout);
phase.lap();
let _ = self.world.try_run_schedule(UISetup);
phase.lap();
let _ = self.world.try_run_schedule(GraphicsSetup);
phase.lap();
let _ = self.world.try_run_schedule(Draw);
phase.lap();
let _ = self.world.try_run_schedule(PreRenderSurface);
phase.lap();
let _ = self.world.try_run_schedule(RenderSurface);
phase.lap();
let _ = self.world.try_run_schedule(Composition);
phase.lap();
let _ = self.world.try_run_schedule(CommitComposition);
phase.lap();
let _ = self.world.try_run_schedule(FrameFinalize);
phase.lap();
// Layout スケジュール実行後のタイミングで NCHITTEST キャッシュをクリア
crate::ecs::pointer::nchittest_cache::clear_nchittest_cache();
// 心拍で回した回かどうかは、門の判断(`decide_tick_with`)が置いた印を読む。
// 読んだら倒す——門を通さない呼び出し(旧経路・テスト)が前回の印を引き継がない。
let heartbeat = std::mem::take(&mut self.last_run_was_heartbeat);
// 相別観測の集計と、1 秒窓が閉じたときの 1 行(要件 2.5/2.6)。
if diag_on {
let ended = Instant::now();
let wall_us = ended.saturating_duration_since(tick_start).as_micros() as u64;
self.tick_diag
.record_run(ended, frame, wall_us, phase.per_schedule(), heartbeat);
if let Some(line) = self.tick_diag.maybe_close(ended) {
tick_diag::emit_line(&line);
}
}
true
}
/// VSYNCカウンターの変化を検知し、必要に応じてworld tickを実行
///
/// この関数は`run()`のWM_VSYNC処理と`WndProc`のWM_WINDOWPOSCHANGED処理の
/// 両方から呼び出され、重複実行を防ぐ。
///
/// # Returns
/// - `true`: tickが実行された
/// - `false`: tickがスキップされた(カウンター変化なし)
///
/// # 拡張ポイント
/// 他のモーダルループ関連メッセージ(WM_ENTERSIZEMOVEなど)で同様の問題が
/// 発見された場合、該当メッセージ処理でこの関数を呼び出すだけで対応可能。
pub fn try_tick_on_vsync(&mut self) -> bool {
use self::vsync::{LAST_VSYNC_TICK, VSYNC_TICK_COUNT};
use std::sync::atomic::Ordering;
// Ordering::Relaxed の妥当性(W7b-V 点検):
// - VSYNC_TICK_COUNT は単一生産者(VSync スレッドの fetch_add)・単一消費者
// (メインスレッドの本 load のみ)のモノトニックカウンタで、tick 要否は
// 「値が前回と異なるか」だけに依存する。カウンタ越しに共有データを渡さない
// (実 tick 処理は同一スレッドの Rc<RefCell> で逐次化され、VSync スレッドとの
// 同期は別途 PostMessageW のメッセージキューが担う)ため、acquire/release は不要。
// - LAST_VSYNC_TICK はメインスレッドのみが load/store する(他スレッドからの
// アクセスは存在しない)ため、より強い順序付けは観測挙動に影響しない。
// u64 カウンタの周回はおよそ 9.7×10^9 年規模(~60Hz)で実機到達不能。
let current_tick = VSYNC_TICK_COUNT.load(Ordering::Relaxed);
let last_tick = LAST_VSYNC_TICK.load(Ordering::Relaxed);
// カウンターが変化していなければスキップ
if current_tick == last_tick {
return false;
}
// 前回処理値を更新(try_tick_world()呼び出し前に更新することで、
// 再入時の重複tickを防止)
LAST_VSYNC_TICK.store(current_tick, Ordering::Relaxed);
// world tickを実行
self.try_tick_world()
}
}
impl Default for EcsWorld {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tick_order_tests {
//! 統合テスト(task 5.2・要件 4.2/4.5): tick の 13 本スケジュール **実行順序不変**。
//!
//! `try_tick_world` は 13 本のスケジュールを固定順で回す(`Input … FrameFinalize`)。
//! 起床ブリッジ(`AsyncTickTask`/`VsyncEventBridge`)の置換にあたり、この構成・順序を
//! **変更しない**ことが要件 4.5 の中核不変条件。本テストは各スケジュールへ実行順を記録する
//! プローブシステムを 1 本ずつ差し込み、空 World で `try_tick_world` を 1 周回して、
//! **実際の実行順序**が固定 13 本列と完全一致することを直接実証する(headless・GPU 不要:
//! 空 World では window/graphics システムは生成対象ゼロで no-op)。
//!
//! VSync 起床→tick→再待機の async ループ 1 周(`spawn_local` executor + 実 Event 必須)は
//! headless に再現不能のため E2E 5.3 へ委譲する。本テストはその 1 周の **核**(13 本 tick の
//! 構成・順序)を固定する(`tick_one_frame` の再入安全・借用スキップは
//! `runtime/tick_bridge.rs` のユニットテストが担保)。
use super::*;
/// 各スケジュールのプローブが実行順にラベル名を push する記録リソース。
#[derive(Resource, Default)]
struct TickOrderLog(Vec<&'static str>);
/// 固定 13 本スケジュールの期待実行順(設計 Testing Strategy / schedule_labels.rs の不変条件)。
const EXPECTED_ORDER: [&str; 13] = [
"Input",
"Update",
"PreLayout",
"Layout",
"PostLayout",
"UISetup",
"GraphicsSetup",
"Draw",
"PreRenderSurface",
"RenderSurface",
"Composition",
"CommitComposition",
"FrameFinalize",
];
/// (要件 4.5) 13 本スケジュールが実行順不変であることを、実 tick の実行順記録で検証する。
#[test]
fn try_tick_world_runs_thirteen_schedules_in_fixed_order() {
let mut ecs = EcsWorld::new();
// 記録リソースを挿入し、各スケジュールへ「自分のラベル名を push する」プローブを
// 1 本ずつ登録する。既存システムと同居しても push 順は schedule 境界で確定する
// (`try_tick_world` が schedule 単位で逐次 `try_run_schedule` するため)。
ecs.world_mut().insert_resource(TickOrderLog::default());
macro_rules! probe {
($label:expr, $name:literal) => {
ecs.add_systems($label, |mut log: ResMut<TickOrderLog>| {
log.0.push($name);
});
};
}
probe!(Input, "Input");
probe!(Update, "Update");
probe!(PreLayout, "PreLayout");
probe!(Layout, "Layout");
probe!(PostLayout, "PostLayout");
probe!(UISetup, "UISetup");
probe!(GraphicsSetup, "GraphicsSetup");
probe!(Draw, "Draw");
probe!(PreRenderSurface, "PreRenderSurface");
probe!(RenderSurface, "RenderSurface");
probe!(Composition, "Composition");
probe!(CommitComposition, "CommitComposition");
probe!(FrameFinalize, "FrameFinalize");
// 空 World を 1 周 tick(window/graphics は生成対象ゼロで no-op・headless 安全)。
let ticked = ecs.try_tick_world();
assert!(
ticked,
"システム登録済みの World は try_tick_world が true を返すべき"
);
let order = ecs.world().resource::<TickOrderLog>().0.clone();
assert_eq!(
order.len(),
13,
"tick は厳密に 13 本のスケジュールを実行すべき(構成不変・要件 4.5)"
);
assert_eq!(
order, EXPECTED_ORDER,
"13 本スケジュールの実行順序は固定列と完全一致すべき(順序不変・要件 4.5)"
);
}
/// (要件 4.2) 起床ごとの 1 tick が冪等に 13 本を回す(2 周目も同一順序で安定)。
#[test]
fn repeated_ticks_keep_thirteen_schedule_order_stable() {
let mut ecs = EcsWorld::new();
ecs.world_mut().insert_resource(TickOrderLog::default());
// 単一プローブで全スケジュールを記録(macro 再掲を避け Input のみ別関数で代表確認は
// 上テスト済み。ここは「2 周で順序がブレない」ことに焦点を当て全 13 本に再登録)。
macro_rules! probe {
($label:expr, $name:literal) => {
ecs.add_systems($label, |mut log: ResMut<TickOrderLog>| {
log.0.push($name);
});
};
}
probe!(Input, "Input");
probe!(Update, "Update");
probe!(PreLayout, "PreLayout");
probe!(Layout, "Layout");
probe!(PostLayout, "PostLayout");
probe!(UISetup, "UISetup");
probe!(GraphicsSetup, "GraphicsSetup");
probe!(Draw, "Draw");
probe!(PreRenderSurface, "PreRenderSurface");
probe!(RenderSurface, "RenderSurface");
probe!(Composition, "Composition");
probe!(CommitComposition, "CommitComposition");
probe!(FrameFinalize, "FrameFinalize");
ecs.try_tick_world();
ecs.try_tick_world();
let order = ecs.world().resource::<TickOrderLog>().0.clone();
assert_eq!(order.len(), 26, "2 周で 13×2 本が実行されるべき");
assert_eq!(&order[..13], EXPECTED_ORDER, "1 周目の順序が固定列と一致");
assert_eq!(
&order[13..],
EXPECTED_ORDER,
"2 周目も同一順序で安定(順序不変)"
);
}
}
/// プロセス共有の旗([`tick_wake`])を触る決定論テストが取る錠(テスト専用)。
///
/// 旗は 1 つしかない実体をプロセス全体で共有するため、並列に走るテストは互いの旗を
/// 読み倒してしまう。共有の旗([`tick_wake::mark`]/[`tick_wake::take`])に触る検査は、
/// **この 1 つ**の錠を取ってから触ること。
///
/// **錠は 1 つでなければ意味がない。** 守る対象が 1 つしか無いのに錠が 2 つあれば、
/// どちらを取っても相手を締め出せず、直列化は成立しない。共有の旗に触る検査は現在
/// 4 箇所——`world_tick_gate_tests.rs`(共有経由の判断)・`tick_wake_tests.rs`
/// (共有の入口の往復)・`runtime/tick_bridge.rs`(門が無効な 1 フレーム)・
/// `ecs/drag/systems.rs`(ドラッグ中の予約)——で、いずれもここを参照する。新たに
/// 共有の旗へ触る検査を足すときも、自前の錠を作らずここを使う。
///
/// **錠でも防げないものがある。** 旗を立てるのは本番の経路(窓書込指令の積み上げ・
/// ウィンドウメッセージの配送・Z 順の要求・ポインタ入力の投入)であり、そちらは錠を
/// 知らない。ゆえに「旗が立っていない**はず**」=省略になるはず、を共有の旗の上で
/// 主張してはならない。省略の側を確かめる検査は、読み取り結果を直に渡す差し替え口
/// (`runtime::tick_bridge::tick_one_frame_with`)を使うこと。共有の旗の上で主張して
/// よいのは「立てた旗が含まれる」「回る側になる」——余分な旗で結論が変わらない側だけ
/// である。
///
/// 取るときは毒化に耐えること(`lock().unwrap_or_else(|p| p.into_inner())`)——
/// 錠を持ったまま 1 本が落ちたときに、無関係な検査まで連鎖して赤くしないため。
#[cfg(test)]
pub(crate) static TICK_WAKE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
#[path = "world_tick_gate_tests.rs"]
mod tick_gate_wiring_tests;
impl std::fmt::Debug for EcsWorld {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EcsWorld").finish_non_exhaustive()
}
}