use super::*;
use crate::ecs::graphics::FrameTime;
use crate::ecs::window::transition_diag::TickStart;
use std::time::Instant;
use super::tick_diag::SCHEDULE_LABELS;
use super::tick_gate::{RunReason, TICK_GATE_WARMUP_FRAMES, TICK_HEARTBEAT_FRAMES, TickDecision};
use super::tick_wake::{PRESENT, WakeSnapshot};
fn no_wake() -> WakeSnapshot {
WakeSnapshot {
bits: 0,
deadline_due: false,
}
}
fn fast_forward_warmup(ecs: &mut EcsWorld) {
for i in 0..TICK_GATE_WARMUP_FRAMES {
assert_eq!(
ecs.decide_tick_with(no_wake()),
TickDecision::Run(RunReason::Warmup),
"起動直後の {i} 回目は無条件で回すべき"
);
}
}
#[derive(Resource, Default)]
struct TickOrderLog(Vec<&'static str>);
fn install_order_probes(ecs: &mut EcsWorld) {
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");
}
fn one_frame(ecs: &mut EcsWorld, snapshot: WakeSnapshot) -> TickDecision {
let decision = ecs.decide_tick_with(snapshot);
if decision.is_run() {
ecs.try_tick_world();
} else {
ecs.note_skipped_tick();
}
decision
}
#[test]
fn tick_gate_is_disabled_by_default_and_every_decision_runs() {
let mut ecs = EcsWorld::new();
assert!(
!ecs.tick_gate_enabled(),
"門の既定は無効(周 1 の A/B で採否を決めるまで)"
);
for i in 0..(TICK_GATE_WARMUP_FRAMES + TICK_HEARTBEAT_FRAMES + 5) {
assert_eq!(
ecs.decide_tick_with(no_wake()),
TickDecision::Run(RunReason::Disabled),
"門が無効なら {i} 回目も判断せず回すべき"
);
}
}
#[test]
fn set_tick_gate_toggles_and_reads_back() {
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
assert!(ecs.tick_gate_enabled());
ecs.set_tick_gate(false);
assert!(!ecs.tick_gate_enabled());
}
#[test]
fn counters_advance_on_every_frame_and_reset_only_on_run() {
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
fast_forward_warmup(&mut ecs);
let (since_boot, since_run) = ecs.tick_gate_counters();
assert_eq!(
since_boot, TICK_GATE_WARMUP_FRAMES,
"起動直後の全走ぶんだけ起動からの回数が進んでいるべき"
);
assert_eq!(since_run, 0, "全部回ったので前回実行からの回数は 0");
for _ in 0..3 {
assert_eq!(ecs.decide_tick_with(no_wake()), TickDecision::Skip);
}
assert_eq!(
ecs.tick_gate_counters(),
(TICK_GATE_WARMUP_FRAMES + 3, 3),
"省略でも起動からの回数は進み、前回実行からの回数も積み上がる"
);
assert!(
ecs.decide_tick_with(WakeSnapshot {
bits: PRESENT.bits(),
deadline_due: false,
})
.is_run()
);
assert_eq!(
ecs.tick_gate_counters(),
(TICK_GATE_WARMUP_FRAMES + 4, 0),
"回した回で前回実行からの回数は 0 へ戻る"
);
}
#[test]
fn gate_skips_after_warmup_when_nothing_asks_for_a_run() {
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
fast_forward_warmup(&mut ecs);
assert_eq!(
ecs.decide_tick_with(no_wake()),
TickDecision::Skip,
"起動直後を抜けて旗も期限も無ければ省略"
);
}
#[test]
fn heartbeat_runs_after_the_configured_number_of_skips() {
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
fast_forward_warmup(&mut ecs);
for i in 0..TICK_HEARTBEAT_FRAMES {
assert_eq!(
ecs.decide_tick_with(no_wake()),
TickDecision::Skip,
"{i} 回目はまだ心拍に届かない"
);
}
assert_eq!(
ecs.decide_tick_with(no_wake()),
TickDecision::Run(RunReason::Heartbeat),
"心拍の間隔に達したら理由が無くても 1 回回す"
);
assert!(
ecs.last_run_was_heartbeat(),
"心拍で回した印が立つ(観測行の heartbeat= に載る)"
);
ecs.try_tick_world();
assert!(
!ecs.last_run_was_heartbeat(),
"回し終えたら心拍の印は倒れているべき"
);
}
#[test]
fn skip_then_present_flag_makes_the_next_decision_run() {
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
fast_forward_warmup(&mut ecs);
assert_eq!(ecs.decide_tick_with(no_wake()), TickDecision::Skip);
let decision = ecs.decide_tick_with(WakeSnapshot {
bits: PRESENT.bits(),
deadline_due: false,
});
assert_eq!(
decision,
TickDecision::Run(RunReason::Wake(PRESENT.bits())),
"省略の直後に表示指令の旗が立てば、次の判定は回す(遅れは 1 画面更新まで)"
);
}
#[test]
fn present_flag_through_the_shared_registry_makes_the_decision_run() {
let _lock = TICK_WAKE_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
fast_forward_warmup(&mut ecs);
let _ = tick_wake::take(Instant::now());
tick_wake::mark(PRESENT);
let decision = ecs.decide_tick(Instant::now());
match decision {
TickDecision::Run(RunReason::Wake(bits)) => assert!(
bits & PRESENT.bits() != 0,
"立てた表示指令の旗が判断の理由に含まれるべき(bits={bits:#x})"
),
other => panic!("表示指令の旗が立っていれば回すべき: {other:?}"),
}
}
#[test]
fn skipped_frames_advance_neither_frame_count_nor_frame_time_nor_tick_start() {
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
fast_forward_warmup(&mut ecs);
ecs.try_tick_world();
let frame = ecs.world().resource::<FrameCount>().0;
let frame_time = ecs.world().resource::<FrameTime>().0;
let tick_start = ecs.world().resource::<TickStart>().0;
for i in 0..5 {
assert_eq!(
ecs.decide_tick_with(no_wake()),
TickDecision::Skip,
"{i} 回目は省略のはず"
);
ecs.note_skipped_tick();
}
assert_eq!(
ecs.world().resource::<FrameCount>().0,
frame,
"省略の回でフレーム番号は進まない"
);
assert_eq!(
ecs.world().resource::<FrameTime>().0,
frame_time,
"省略の回で表示時刻は進まない"
);
assert_eq!(
ecs.world().resource::<TickStart>().0,
tick_start,
"省略の回で tick の起点時刻は進まない"
);
ecs.try_tick_world();
assert_eq!(
ecs.world().resource::<FrameCount>().0,
frame + 1,
"省略のあとに回せばフレーム番号はちょうど 1 進む"
);
}
#[derive(Component)]
struct ChangeProbe(u32);
#[derive(Resource, Default)]
struct ChangedSeen(Vec<Entity>);
#[test]
fn changes_made_during_skipped_frames_are_seen_by_the_next_run() {
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
ecs.world_mut().insert_resource(ChangedSeen::default());
let entity = ecs.world_mut().spawn(ChangeProbe(0)).id();
ecs.add_systems(
Update,
|mut seen: ResMut<ChangedSeen>, q: Query<Entity, Changed<ChangeProbe>>| {
for e in q.iter() {
seen.0.push(e);
}
},
);
fast_forward_warmup(&mut ecs);
ecs.try_tick_world();
ecs.world_mut().resource_mut::<ChangedSeen>().0.clear();
ecs.world_mut()
.get_mut::<ChangeProbe>(entity)
.expect("印を付けた entity は生きているはず")
.0 = 1;
for _ in 0..3 {
assert_eq!(ecs.decide_tick_with(no_wake()), TickDecision::Skip);
ecs.note_skipped_tick();
}
assert!(
ecs.world().resource::<ChangedSeen>().0.is_empty(),
"省略の回はスケジュールを走らせないので、まだ誰も拾っていない"
);
ecs.try_tick_world();
assert_eq!(
ecs.world().resource::<ChangedSeen>().0,
vec![entity],
"省略を挟んでも、次に回した tick で変化がちょうど 1 件拾えるべき"
);
}
#[test]
fn skipped_frames_do_not_disturb_the_thirteen_schedule_order() {
let mut ecs = EcsWorld::new();
ecs.set_tick_gate(true);
ecs.world_mut().insert_resource(TickOrderLog::default());
install_order_probes(&mut ecs);
fast_forward_warmup(&mut ecs);
let present = WakeSnapshot {
bits: PRESENT.bits(),
deadline_due: false,
};
let force = WakeSnapshot {
bits: tick_wake::FORCE.bits(),
deadline_due: false,
};
assert_eq!(one_frame(&mut ecs, no_wake()), TickDecision::Skip);
assert_eq!(one_frame(&mut ecs, no_wake()), TickDecision::Skip);
assert!(one_frame(&mut ecs, present).is_run());
assert_eq!(one_frame(&mut ecs, no_wake()), TickDecision::Skip);
assert!(one_frame(&mut ecs, force).is_run());
let order = ecs.world().resource::<TickOrderLog>().0.clone();
assert_eq!(
order.len(),
SCHEDULE_LABELS.len() * 2,
"回ったのは 2 回だけ(省略の回は 1 本も走らない)"
);
assert_eq!(
&order[..SCHEDULE_LABELS.len()],
&SCHEDULE_LABELS[..],
"1 回目の 13 本は固定順のはず"
);
assert_eq!(
&order[SCHEDULE_LABELS.len()..],
&SCHEDULE_LABELS[..],
"省略を挟んだ 2 回目も同じ固定順のはず"
);
}