use std::cell::Cell;
use std::ffi::c_void;
use std::rc::Rc;
use std::sync::MutexGuard;
use crate::reactive::effect::effect;
use crate::reactive::{__reset_for_tests, flush, has_pending_work, Owner, RwSignal};
use crate::tasks;
fn lock<'a>() -> MutexGuard<'a, ()> {
crate::main_thread::host_test_lock()
}
thread_local! {
static VSYNC_RUNNING: Cell<bool> = const { Cell::new(false) };
static WAKE_COUNT: Cell<u32> = const { Cell::new(0) };
}
extern "C" fn on_request_frame(_user_data: *mut c_void) {
VSYNC_RUNNING.with(|v| v.set(true));
WAKE_COUNT.with(|c| c.set(c.get() + 1));
}
fn install_host() {
VSYNC_RUNNING.with(|v| v.set(false));
WAKE_COUNT.with(|c| c.set(0));
crate::host_wake::set_request_frame_callback(Some(on_request_frame), std::ptr::null_mut());
}
fn reset_all() {
__reset_for_tests();
tasks::__reset_for_tests();
crate::host_wake::__reset_for_tests();
VSYNC_RUNNING.with(|v| v.set(false));
WAKE_COUNT.with(|c| c.set(0));
}
const SETTLE_CAP: usize = 16;
fn tick_frame_body(settle_signal: RwSignal<i32>, reentry: &Cell<u32>) {
flush();
tasks::run_until_stalled();
flush();
let pending_reentry = reentry.replace(0);
for _ in 0..pending_reentry {
settle_signal.set(settle_signal.get_untracked() + 1);
}
let mut settle = 0;
while has_pending_work() && settle < SETTLE_CAP {
settle += 1;
flush();
}
}
fn tick_fixed(settle_signal: RwSignal<i32>, reentry: &Cell<u32>) -> bool {
tick_frame_body(settle_signal, reentry);
!has_pending_work()
}
fn tick_unfixed(settle_signal: RwSignal<i32>, reentry: &Cell<u32>) {
flush();
tasks::run_until_stalled();
flush();
let pending_reentry = reentry.replace(0);
for _ in 0..pending_reentry {
settle_signal.set(settle_signal.get_untracked() + 1);
}
}
#[test]
fn fixed_loop_drains_settle_and_tap_renders() {
let _g = lock();
reset_all();
install_host();
let owner = Owner::new(None);
owner.with(|| {
let count = RwSignal::new(0_i32);
let settle_signal = RwSignal::new(0_i32);
let render_count = Rc::new(Cell::new(0u32));
let rc = render_count.clone();
effect(move || {
count.get();
settle_signal.get();
rc.set(rc.get() + 1);
});
flush();
let baseline = render_count.get();
let reentry = Cell::new(1u32);
let mut vsync_idle = false;
for _ in 0..50 {
if VSYNC_RUNNING.with(|v| v.get()) {
vsync_idle = tick_fixed(settle_signal, &reentry);
if vsync_idle {
VSYNC_RUNNING.with(|v| v.set(false));
}
} else {
break;
}
}
assert!(
vsync_idle,
"fixed loop should reach idle after draining settle"
);
assert!(
render_count.get() > baseline,
"settle re-entry must have re-rendered (render_count advanced past baseline)"
);
let after_settle = render_count.get();
let wakes_before = WAKE_COUNT.with(|c| c.get());
count.set(1);
assert!(
WAKE_COUNT.with(|c| c.get()) > wakes_before,
"tap's set() must fire a host wake (queue was empty → non-empty)"
);
assert!(
VSYNC_RUNNING.with(|v| v.get()),
"wake must unpause the vsync loop"
);
let mut settled = false;
for _ in 0..50 {
if VSYNC_RUNNING.with(|v| v.get()) {
if tick_fixed(settle_signal, &reentry) {
VSYNC_RUNNING.with(|v| v.set(false));
settled = true;
break;
}
} else {
settled = true;
break;
}
}
assert!(settled, "resumed loop should settle");
assert!(
render_count.get() > after_settle,
"tap must re-render: render_count advanced after the user set()"
);
});
owner.dispose();
reset_all();
}
#[test]
fn unfixed_loop_wedges_when_pending_left_dirty() {
let _g = lock();
reset_all();
install_host();
let owner = Owner::new(None);
owner.with(|| {
let count = RwSignal::new(0_i32);
let settle_signal = RwSignal::new(0_i32);
let render_count = Rc::new(Cell::new(0u32));
let rc = render_count.clone();
effect(move || {
count.get();
settle_signal.get();
rc.set(rc.get() + 1);
});
flush();
let reentry = Cell::new(1u32);
let mut vsync_idle = false;
for _ in 0..50 {
if VSYNC_RUNNING.with(|v| v.get()) {
tick_unfixed(settle_signal, &reentry);
vsync_idle = true;
VSYNC_RUNNING.with(|v| v.set(false));
} else {
break;
}
}
assert!(vsync_idle, "unfixed loop pauses (frame reports idle)");
assert!(
has_pending_work(),
"pre-fix frame must leave the commit-time re-entry node undrained"
);
let render_before_tap = render_count.get();
let wakes_before = WAKE_COUNT.with(|c| c.get());
count.set(1);
assert_eq!(
WAKE_COUNT.with(|c| c.get()),
wakes_before,
"WEDGE: with a non-empty queue, tap's set() fires no edge-triggered wake"
);
assert!(
!VSYNC_RUNNING.with(|v| v.get()),
"WEDGE: vsync loop stays paused after the tap"
);
for _ in 0..50 {
if VSYNC_RUNNING.with(|v| v.get()) {
tick_unfixed(settle_signal, &reentry);
VSYNC_RUNNING.with(|v| v.set(false));
} else {
break;
}
}
assert_eq!(
render_count.get(),
render_before_tap,
"WEDGE: render counter never advances — the loop is permanently frozen"
);
});
owner.dispose();
reset_all();
}