use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::Instant;
use event_listener::Event;
use tracing::{debug, error, trace};
use windows::Win32::Graphics::Dwm::DwmFlush;
use crate::ecs::world::EcsWorld;
pub(crate) struct VsyncEventBridge {
vblank_event: Arc<event_listener::Event>,
stop_flag: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl VsyncEventBridge {
pub(crate) fn new() -> Self {
let vblank_event = Arc::new(event_listener::Event::new());
let stop_flag = Arc::new(AtomicBool::new(false));
let thread_event = Arc::clone(&vblank_event);
let thread_stop = Arc::clone(&stop_flag);
let handle = thread::Builder::new()
.name("wintf-vsync".to_owned())
.spawn(move || vsync_loop(thread_event, thread_stop))
.expect("failed to spawn wintf-vsync thread");
debug!("VsyncEventBridge started (wintf-vsync thread spawned)");
Self {
vblank_event,
stop_flag,
handle: Some(handle),
}
}
pub(crate) fn event(&self) -> &Arc<event_listener::Event> {
&self.vblank_event
}
fn stop(&mut self) {
if let Some(handle) = self.handle.take() {
self.stop_flag.store(true, Ordering::Release);
if handle.join().is_err() {
trace!("wintf-vsync thread join returned Err (thread panicked)");
}
debug!("VsyncEventBridge stopped (wintf-vsync thread joined)");
}
}
}
impl Drop for VsyncEventBridge {
fn drop(&mut self) {
self.stop();
}
}
fn vsync_loop(event: Arc<event_listener::Event>, stop: Arc<AtomicBool>) {
if let Err(e) = crate::ecs::world::thread_registry::register_current_thread(
crate::ecs::world::thread_registry::ROLE_VBLANK,
) {
error!(error = %e, "[vsync_loop] スレッド名簿への登録に失敗した(vblank 検出は継続)");
}
while !stop.load(Ordering::Acquire) {
let flushed = unsafe { DwmFlush() };
match flushed {
Ok(()) => {
event.notify(usize::MAX);
}
Err(_) => {
thread::sleep(std::time::Duration::from_millis(15));
}
}
}
}
pub(crate) struct AsyncTickTask;
impl AsyncTickTask {
pub(crate) fn spawn(
event: Arc<Event>,
world: Weak<RefCell<EcsWorld>>,
running: Rc<Cell<bool>>,
) -> crate::executor::JoinHandle<()> {
crate::executor::spawn_local(run_async_tick(event, world, running))
}
}
pub(crate) fn tick_one_frame(world: &Rc<RefCell<EcsWorld>>) -> bool {
tick_one_frame_with(world, |world| world.decide_tick(Instant::now()))
}
fn tick_one_frame_with(
world: &Rc<RefCell<EcsWorld>>,
decide: impl FnOnce(&mut EcsWorld) -> crate::ecs::world::tick_gate::TickDecision,
) -> bool {
let Some(_guard) = crate::ecs::world::engage_tick_flush_guard() else {
trace!("[tick_one_frame] Re-entry blocked by IS_TICK_FLUSH_IN_PROGRESS");
return false;
};
let processed = match world.try_borrow_mut() {
Ok(mut world) => {
if decide(&mut world).is_run() {
world.try_tick_world();
} else {
world.note_skipped_tick();
}
true
}
Err(_) => {
false
}
};
crate::ecs::window::flush_window_pos_commands();
processed
}
async fn run_async_tick(
event: Arc<Event>,
world: Weak<RefCell<EcsWorld>>,
running: Rc<Cell<bool>>,
) {
debug!("AsyncTickTask started (vblank-driven UI-thread tick loop)");
loop {
let listener = event.listen();
listener.await;
if !running.get() {
debug!("AsyncTickTask stopping (message loop has returned)");
return;
}
let Some(world) = world.upgrade() else {
debug!("AsyncTickTask stopping (world dropped — shutdown)");
return;
};
tick_one_frame(&world);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ecs::world::FrameCount;
use crate::ecs::world::thread_registry::{self, ROLE_VBLANK, ThreadEntry};
use crate::ecs::world::tick_gate::TICK_GATE_WARMUP_FRAMES;
use crate::ecs::world::tick_wake::WakeSnapshot;
use event_listener::Listener;
use std::time::Duration;
const VBLANK_DEADLINE_ENV: &str = "AREKA_WINTF_VBLANK_DEADLINE";
fn require_explicit_vblank_run() {
assert!(
std::env::var_os(VBLANK_DEADLINE_ENV).is_some(),
"{VBLANK_DEADLINE_ENV} が未設定である。実 vblank の壁時計期限は負荷で飢えて\
間欠的に赤くなるため明示実行へ隔離してある(判定は 1 行も走っていない)。\
走らせるには環境変数を与えて \
`cargo test -p wintf --lib <テスト名> -- --ignored --exact` とすること"
);
}
fn wait_for_role(role: &str, limit: Duration) -> Option<ThreadEntry> {
let deadline = std::time::Instant::now() + limit;
loop {
let found = thread_registry::snapshot()
.into_iter()
.find(|entry| entry.role == role);
if found.is_some() {
return found;
}
if std::time::Instant::now() >= deadline {
return None;
}
thread::sleep(Duration::from_millis(5));
}
}
#[test]
fn vsync_thread_registers_itself_with_the_vblank_role() {
let bridge = VsyncEventBridge::new();
let found = wait_for_role(ROLE_VBLANK, Duration::from_secs(5))
.expect("wintf-vsync スレッドは走り始めに vblank として名簿へ載るはず");
assert_eq!(
found.name.as_deref(),
Some("wintf-vsync"),
"名簿の OS 名は生成時に付けたスレッド名であるはず"
);
assert!(
thread_registry::is_known_role(ROLE_VBLANK),
"登録した役割名は固定語彙に含まれるはず"
);
drop(bridge);
}
#[test]
#[ignore = "実 vblank の 500ms 期限が負荷で飢えて間欠的に赤くなる(AREKA_WINTF_VBLANK_DEADLINE を与えて明示実行する)"]
fn vblank_notifies_listener_then_joins_on_drop() {
require_explicit_vblank_run();
let bridge = VsyncEventBridge::new();
let listener = bridge.event().listen();
let got = listener.wait_timeout(Duration::from_millis(500));
assert!(
got.is_some(),
"expected a vblank notify within 500ms (is DWM running?)"
);
drop(bridge);
}
#[test]
fn tick_one_frame_safe_skips_when_world_borrowed() {
use std::cell::RefCell;
use std::rc::Rc;
let world = Rc::new(RefCell::new(crate::ecs::world::EcsWorld::new()));
let _held = world.borrow_mut();
let ticked = tick_one_frame(&world);
assert!(
!ticked,
"borrow_mut が握られている間は安全スキップで false を返すべき"
);
assert!(
!crate::ecs::world::is_tick_flush_in_progress(),
"安全スキップ後は再入ガードが false に戻っているべき"
);
}
fn fast_forward_warmup(world: &Rc<RefCell<crate::ecs::world::EcsWorld>>) {
let mut world = world.borrow_mut();
for _ in 0..TICK_GATE_WARMUP_FRAMES {
let decision = world.decide_tick_with(WakeSnapshot {
bits: 0,
deadline_due: false,
});
assert!(decision.is_run(), "起動直後は無条件で回すべき");
}
}
#[test]
fn tick_one_frame_ticks_the_world_when_the_gate_is_disabled() {
let _lock = crate::ecs::world::TICK_WAKE_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let world = Rc::new(RefCell::new(crate::ecs::world::EcsWorld::new()));
assert!(
!world.borrow().tick_gate_enabled(),
"門の既定は無効であるべき"
);
let before = world.borrow().world().resource::<FrameCount>().0;
let processed = tick_one_frame(&world);
assert!(processed, "借用できたフレームは処理済みとして true を返す");
assert_eq!(
world.borrow().world().resource::<FrameCount>().0,
before + 1,
"門が無効なら 13 本を回してフレーム番号が 1 進む"
);
}
#[test]
fn tick_one_frame_skips_the_world_tick_when_the_gate_finds_no_reason() {
let world = Rc::new(RefCell::new(crate::ecs::world::EcsWorld::new()));
world.borrow_mut().set_tick_gate(true);
fast_forward_warmup(&world);
let before = world.borrow().world().resource::<FrameCount>().0;
let processed = tick_one_frame_with(&world, |world| {
world.decide_tick_with(WakeSnapshot {
bits: 0,
deadline_due: false,
})
});
assert!(
processed,
"省略も「このフレームは門が処理した」なので true を返す"
);
assert_eq!(
world.borrow().world().resource::<FrameCount>().0,
before,
"省略の回は 13 本を回さないのでフレーム番号は進まない"
);
assert!(
!crate::ecs::world::is_tick_flush_in_progress(),
"省略の回も flush まで進んでから再入ガードが外れているべき"
);
}
#[test]
fn reentry_guard_toggles_and_blocks_nested_engage() {
assert!(!crate::ecs::world::is_tick_flush_in_progress());
let guard = crate::ecs::world::engage_tick_flush_guard()
.expect("最初の engage はガードを獲得できる");
assert!(
crate::ecs::world::is_tick_flush_in_progress(),
"engage スコープ中は true"
);
assert!(
crate::ecs::world::engage_tick_flush_guard().is_none(),
"進行中の再 engage は None(安全スキップ)であるべき"
);
drop(guard);
assert!(
!crate::ecs::world::is_tick_flush_in_progress(),
"guard drop 後は false へ戻る"
);
}
#[test]
fn tick_task_stops_without_a_frame_once_the_loop_has_returned() {
let _lock = crate::ecs::world::TICK_WAKE_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let world = Rc::new(RefCell::new(crate::ecs::world::EcsWorld::new()));
let event = Arc::new(Event::new());
let running = Rc::new(Cell::new(true));
let frame = || world.borrow().world().resource::<FrameCount>().0;
let _task = AsyncTickTask::spawn(
Arc::clone(&event),
Rc::downgrade(&world),
Rc::clone(&running),
);
crate::executor::block_on(async {}); let held_while_waiting = Rc::strong_count(&world) - 1;
let before = frame();
event.notify(usize::MAX);
crate::executor::block_on(async {});
assert_eq!(
frame(),
before + 1,
"ループが回っている間の起床は 1 フレーム回すはず"
);
running.set(false);
let before = frame();
event.notify(usize::MAX);
crate::executor::block_on(async {});
let advanced_after_return = frame() - before;
assert_eq!(
(held_while_waiting, advanced_after_return),
(0, 0),
"(待ちの間に握っていた World の強参照の数, ループが戻った後に回ったフレーム数) は両方 0 のはず"
);
}
}