An idle app takes no loop turn: a session's pump blocks until there is news for it, wakes and GPU results reach only their own session, and a hosted app builds no frame while its window has no free shared image. Each window paces at its own display; a texture pass paints only when its window can show the result and is exactly its render target at every scale. The direct compositor renders on the discrete GPU when there is one; Mirror and Extended switch without an output rebuild. Vulkan: a full GPU never makes an app fall back, fail or spin; upload arenas are pooled; pipeline variants a batch needs compile before it; a grab of a hosted window no longer loses the device; the WM's own device is replaceable and apps hear Event::GpuReset. Every process lifts its open-file limit. Cameras: retry after a failed open, rotation kept, a synthetic test camera, Cx::video_inputs. Android: standalone app APKs run on the HAL again, file video decoding, system back gestures, audio underruns grow the buffer. Frame tracing names who keeps an app awake; one main-lag meter for macOS and Linux. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
870 lines
38 KiB
Rust
870 lines
38 KiB
Rust
//! Domains on the headless HAL: a privileged "wm" session starts two child domains through a
|
|
//! `DomainHost` (a fake host that runs a tiny fake app on its own thread per domain), composites
|
|
//! their embedded windows, forwards input, relaunches, stops, lists. Every session pumps and submits
|
|
//! from its own thread.
|
|
|
|
use makepad_hal::buffers::{pack_domains, pack_forward, read_domains, read_strs, unpack_forward, DomainInfo};
|
|
use makepad_hal::ledger::Kind;
|
|
use makepad_hal::*;
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
|
use std::sync::{Arc, Mutex, Weak};
|
|
use std::thread::JoinHandle;
|
|
use std::time::{Duration, Instant};
|
|
|
|
const WAIT: Duration = Duration::from_secs(10);
|
|
const WIN: Id = Id::new(0, 1);
|
|
|
|
fn open_req(key: u64, parent: u32) -> OpenReq {
|
|
OpenReq { backend: backend_id::HEADLESS, app_key: key, flags: 0, dpi_hint: 0.0, parent }
|
|
}
|
|
|
|
// ------------------------------------------------------------------ what a session sees
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
enum Ev {
|
|
ChildWindow { child: u32, window: Id, texture: Id, w: u32, h: u32 },
|
|
ChildClosed { child: u32, window: Id },
|
|
State { domain: u32, state: u32, req: u32, app: String, msg: String },
|
|
Listed { req: u32, list: Vec<DomainInfo> },
|
|
Other(String),
|
|
}
|
|
|
|
fn collect(hal: &Hal, store: &mut EventStore, sid: u32, out: &mut Vec<Ev>, timeout_ns: u64) {
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(timeout_ns, &mut buf);
|
|
for (_, e) in store.reader(n, buf.arena_len) {
|
|
out.push(match e {
|
|
HalEvent::ChildWindow { window, child, texture, w, h, .. } => Ev::ChildWindow { child, window, texture, w, h },
|
|
HalEvent::ChildWindowClosed { window, child } => Ev::ChildClosed { child, window },
|
|
HalEvent::DomainState { domain, state, req, text, .. } => {
|
|
let strs = read_strs(text, 2);
|
|
Ev::State { domain, state, req, app: strs[0].clone(), msg: strs[1].clone() }
|
|
}
|
|
HalEvent::DomainListed { req, count, list } => Ev::Listed { req, list: read_domains(list, count) },
|
|
other => Ev::Other(format!("{other:?}")),
|
|
});
|
|
}
|
|
let _ = sid;
|
|
}
|
|
|
|
/// Pumps `sid` until `pred` holds for everything seen so far.
|
|
fn wait_for(hal: &Hal, store: &mut EventStore, sid: u32, out: &mut Vec<Ev>, pred: impl Fn(&[Ev]) -> bool) {
|
|
let deadline = Instant::now() + WAIT;
|
|
while !pred(out) {
|
|
assert!(Instant::now() < deadline, "timed out; saw {out:#?}");
|
|
collect(hal, store, sid, out, 20_000_000);
|
|
}
|
|
}
|
|
|
|
fn windows_of(evs: &[Ev], child: u32) -> Vec<(Id, u32, u32)> {
|
|
evs.iter().filter_map(|e| if let Ev::ChildWindow { child: c, texture, w, h, .. } = e { (*c == child).then_some((*texture, *w, *h)) } else { None }).collect()
|
|
}
|
|
|
|
fn state_of(evs: &[Ev], domain: u32) -> Option<u32> {
|
|
evs.iter().rev().find_map(|e| if let Ev::State { domain: d, state, .. } = e { (*d == domain).then_some(*state) } else { None })
|
|
}
|
|
|
|
// ------------------------------------------------------------------ the fake host and its apps
|
|
|
|
struct Running {
|
|
stop: Arc<AtomicBool>,
|
|
join: JoinHandle<()>,
|
|
}
|
|
|
|
struct Fake {
|
|
hal: Weak<Hal>,
|
|
apps: Mutex<HashMap<u32, Running>>,
|
|
logs: Mutex<HashMap<u32, Arc<Mutex<Vec<String>>>>>,
|
|
starts: AtomicU32,
|
|
stops: Mutex<Vec<(u32, u32)>>,
|
|
/// a relaunch stops the old app and waits here for `release` before starting the new one
|
|
hold: AtomicBool,
|
|
held: Mutex<Option<DomainLaunch>>,
|
|
}
|
|
|
|
impl Fake {
|
|
fn new(hal: &Arc<Hal>) -> Arc<Fake> {
|
|
Arc::new(Fake {
|
|
hal: Arc::downgrade(hal),
|
|
apps: Mutex::new(HashMap::new()),
|
|
logs: Mutex::new(HashMap::new()),
|
|
starts: AtomicU32::new(0),
|
|
stops: Mutex::new(Vec::new()),
|
|
hold: AtomicBool::new(false),
|
|
held: Mutex::new(None),
|
|
})
|
|
}
|
|
|
|
fn log(&self, domain: u32) -> Arc<Mutex<Vec<String>>> {
|
|
self.logs.lock().unwrap().entry(domain).or_default().clone()
|
|
}
|
|
|
|
fn saw(&self, domain: u32, what: &str) -> bool {
|
|
self.log(domain).lock().unwrap().iter().any(|l| l.contains(what))
|
|
}
|
|
|
|
/// The app runs on its own thread: wait until its log has `what`.
|
|
fn wait_saw(&self, domain: u32, what: &str) {
|
|
let t0 = Instant::now();
|
|
while !self.saw(domain, what) {
|
|
assert!(t0.elapsed() < WAIT, "{what} never reached domain {domain}: {:?}", self.log(domain).lock().unwrap());
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
|
|
fn spawn(&self, l: DomainLaunch) {
|
|
let hal = self.hal.upgrade().expect("hal");
|
|
let stop = Arc::new(AtomicBool::new(false));
|
|
let log = self.log(l.domain);
|
|
let flag = stop.clone();
|
|
let domain = l.domain;
|
|
let join = std::thread::spawn(move || fake_app(hal, l, flag, log));
|
|
self.apps.lock().unwrap().insert(domain, Running { stop, join });
|
|
}
|
|
|
|
fn halt(&self, domain: u32) {
|
|
let r = self.apps.lock().unwrap().remove(&domain);
|
|
if let Some(r) = r {
|
|
r.stop.store(true, Ordering::SeqCst);
|
|
r.join.join().unwrap();
|
|
}
|
|
}
|
|
|
|
fn halt_all(&self) {
|
|
let ids: Vec<u32> = self.apps.lock().unwrap().keys().copied().collect();
|
|
for id in ids {
|
|
self.halt(id);
|
|
}
|
|
}
|
|
|
|
/// Stops and relaunches reach the host on a thread of the HAL's own: wait until it was told.
|
|
fn wait_stopped(&self, domain: u32, mode: u32) {
|
|
let t0 = Instant::now();
|
|
while !self.stops.lock().unwrap().contains(&(domain, mode)) {
|
|
assert!(t0.elapsed() < WAIT, "the host was never told to stop domain {domain} (mode {mode}): {:?}", self.stops.lock().unwrap());
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
|
|
/// Starts the relaunch the host is holding, once it reached the host.
|
|
fn release(&self) {
|
|
let t0 = Instant::now();
|
|
let l = loop {
|
|
if let Some(l) = self.held.lock().unwrap().take() {
|
|
break l;
|
|
}
|
|
assert!(t0.elapsed() < WAIT, "no relaunch reached the host");
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
};
|
|
self.spawn(l);
|
|
}
|
|
}
|
|
|
|
impl DomainHost for Fake {
|
|
fn start(&self, launch: DomainLaunch) -> Result<(), String> {
|
|
self.starts.fetch_add(1, Ordering::SeqCst);
|
|
self.spawn(launch);
|
|
Ok(())
|
|
}
|
|
|
|
fn stop(&self, domain: u32, mode: u32) {
|
|
self.stops.lock().unwrap().push((domain, mode));
|
|
if mode == domain_stop::KILL {
|
|
self.halt(domain);
|
|
}
|
|
}
|
|
|
|
fn relaunch(&self, launch: DomainLaunch) -> Result<(), String> {
|
|
self.halt(launch.domain);
|
|
if self.hold.load(Ordering::SeqCst) {
|
|
*self.held.lock().unwrap() = Some(launch);
|
|
} else {
|
|
self.spawn(launch);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A tiny app: a window, a texture and a pipeline of its own, a frame; a frame more per click;
|
|
/// quits when asked to.
|
|
fn fake_app(hal: Arc<Hal>, l: DomainLaunch, stop: Arc<AtomicBool>, log: Arc<Mutex<Vec<String>>>) {
|
|
let info = hal.open(&open_req(l.app_key, l.parent));
|
|
assert_eq!(info.session, l.domain, "the app opens the session the parent asked for");
|
|
let sid = info.session;
|
|
let mut w = CmdWriter::new(sid);
|
|
let mut frame = 1u64;
|
|
w.window_create(WIN, window_kind::NORMAL, Id::NONE, [0.0, 0.0, 200.0, 100.0], false);
|
|
w.texture_create(Id::new(0, 1), usage::RENDER_TARGET | usage::SAMPLED, format::RGBA8, 32, 32, [1, 1], Attr::default());
|
|
let blob = CmdWriter::pipeline_blob(PipelineDesc { name: 1, ..Default::default() }, b"code", &[], &[]);
|
|
w.pipeline_create(Id::new(0, 1), dialect::MSL, Attr::default(), &blob);
|
|
w.frame_begin(frame, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, ..Default::default() });
|
|
w.frame_end(WIN, target::WINDOW);
|
|
hal.submit(&w.buf());
|
|
w.clear();
|
|
let mut store = EventStore::new(sid, 64, 8192);
|
|
while !stop.load(Ordering::SeqCst) {
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(20_000_000, &mut buf);
|
|
let mut clicked = false;
|
|
let mut quit = false;
|
|
for (_, e) in store.reader(n, buf.arena_len) {
|
|
if !matches!(e, HalEvent::Wake {}) {
|
|
log.lock().unwrap().push(format!("{e:?}"));
|
|
}
|
|
clicked |= matches!(e, HalEvent::MouseDown { .. });
|
|
quit |= matches!(e, HalEvent::Lifecycle { kind } if kind == lifecycle::QUIT_REQUESTED);
|
|
}
|
|
if clicked {
|
|
frame += 1;
|
|
w.frame_begin(frame, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, ..Default::default() });
|
|
w.frame_end(WIN, target::WINDOW);
|
|
hal.submit(&w.buf());
|
|
w.clear();
|
|
}
|
|
if quit {
|
|
w.quit();
|
|
hal.submit(&w.buf());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A frame of the wm that draws the given textures (one draw each) with a pipeline of its own.
|
|
fn composite(w: &mut CmdWriter, frame: u64, win: Id, textures: &[Id]) {
|
|
w.frame_begin(frame, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, ..Default::default() });
|
|
for t in textures {
|
|
let tex = w.put_ids(&[*t]);
|
|
w.draw(Draw { pipeline: Id::new(0, 1), tex, ..Default::default() });
|
|
}
|
|
w.frame_end(win, target::WINDOW);
|
|
}
|
|
|
|
struct Rig {
|
|
hal: Arc<Hal>,
|
|
fake: Arc<Fake>,
|
|
wm: u32,
|
|
store: EventStore,
|
|
evs: Vec<Ev>,
|
|
w: CmdWriter,
|
|
frame: u64,
|
|
}
|
|
|
|
impl Rig {
|
|
fn new() -> Rig {
|
|
let hal = Hal::new();
|
|
let fake = Fake::new(&hal);
|
|
hal.set_domain_host(fake.clone());
|
|
hal.with_world(|w| w.trace_exec = true);
|
|
let wm = hal.open_domain(&open_req(1, 0), Caps { privileged: true }).session;
|
|
assert_ne!(wm, 0);
|
|
let mut w = CmdWriter::new(wm);
|
|
w.window_create(WIN, window_kind::NORMAL, Id::NONE, [0.0, 0.0, 800.0, 600.0], false);
|
|
let blob = CmdWriter::pipeline_blob(PipelineDesc { name: 1, ..Default::default() }, b"code", &[], &[]);
|
|
w.pipeline_create(Id::new(0, 1), dialect::MSL, Attr::default(), &blob);
|
|
hal.submit(&w.buf());
|
|
w.clear();
|
|
let store = EventStore::new(wm, 256, 1 << 16);
|
|
Rig { hal, fake, wm, store, evs: Vec::new(), w, frame: 0 }
|
|
}
|
|
|
|
fn submit(&mut self) {
|
|
self.hal.submit(&self.w.buf());
|
|
self.w.clear();
|
|
}
|
|
|
|
fn wait(&mut self, pred: impl Fn(&[Ev]) -> bool) {
|
|
wait_for(&self.hal, &mut self.store, self.wm, &mut self.evs, pred);
|
|
}
|
|
|
|
fn stats(&self, sid: u32) -> makepad_hal::session::Stats {
|
|
self.hal.with_world(|w| w.session(sid).expect("session").stats)
|
|
}
|
|
|
|
fn rows(&self, sid: u32) -> (usize, usize, usize, usize) {
|
|
self.hal.with_world(|w| {
|
|
let s = w.session(sid).expect("session");
|
|
(s.textures.len(), s.buffers.len(), s.pipelines.len(), s.ledger.len())
|
|
})
|
|
}
|
|
|
|
/// The wm draws the textures; returns the exec record of that frame.
|
|
fn composite(&mut self, textures: &[Id]) -> ExecRecord {
|
|
self.frame += 1;
|
|
composite(&mut self.w, self.frame, WIN, textures);
|
|
self.submit();
|
|
let (wm, frame) = (self.wm, self.frame);
|
|
self.hal.with_world(|w| w.exec_log.iter().rev().find(|r| r.session == wm && r.frame == frame).cloned().expect("the wm frame ran"))
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ tests
|
|
|
|
#[test]
|
|
fn a_wm_runs_two_child_domains() {
|
|
let mut rig = Rig::new();
|
|
let hal = rig.hal.clone();
|
|
let wm = rig.wm;
|
|
|
|
// an ordinary session cannot start domains, nor open itself under the wm
|
|
let plain = hal.open(&open_req(2, 0)).session;
|
|
let mut pw = CmdWriter::new(plain);
|
|
pw.str_def(1, b"calc");
|
|
pw.domain_start(5, 1, 0);
|
|
pw.domain_list(6);
|
|
hal.submit(&pw.buf());
|
|
assert_eq!(rig.stats(plain).denied, 2, "start and list from an unprivileged session");
|
|
assert_eq!(rig.fake.starts.load(Ordering::SeqCst), 0);
|
|
assert_eq!(hal.open(&open_req(0xdead_beef, wm)).session, 0, "a parent claim needs the key the wm's DomainStart produced");
|
|
assert_eq!(hal.open(&open_req(0, wm)).session, 0);
|
|
|
|
// the wm starts two apps
|
|
rig.w.str_def(1, b"calc");
|
|
rig.w.str_def(2, b"clock");
|
|
rig.w.domain_start(11, 1, 0);
|
|
rig.w.domain_start(12, 2, 0);
|
|
rig.submit();
|
|
assert_eq!(rig.fake.starts.load(Ordering::SeqCst), 2);
|
|
rig.wait(|e| e.iter().filter(|e| matches!(e, Ev::ChildWindow { .. })).count() >= 2 && e.iter().filter(|e| matches!(e, Ev::State { state: domain_state::RUNNING, .. })).count() >= 2);
|
|
let started: Vec<(u32, u32, String)> = rig.evs.iter().filter_map(|e| if let Ev::State { domain, req, app, state: domain_state::COMPILING, .. } = e { Some((*domain, *req, app.clone())) } else { None }).collect();
|
|
assert_eq!(started.len(), 2);
|
|
let (a, b) = (started.iter().find(|s| s.1 == 11).unwrap().0, started.iter().find(|s| s.1 == 12).unwrap().0);
|
|
assert_eq!((started.iter().find(|s| s.0 == a).unwrap().2.as_str(), started.iter().find(|s| s.0 == b).unwrap().2.as_str()), ("calc", "clock"));
|
|
assert!(a != b && a != wm && b != wm);
|
|
let (tex_a, tex_b) = (windows_of(&rig.evs, a)[0], windows_of(&rig.evs, b)[0]);
|
|
assert_eq!((tex_a.1, tex_a.2), (200, 100));
|
|
assert!(tex_a.0.index >= LENT_BASE && tex_b.0.index >= LENT_BASE && tex_a.0 != tex_b.0);
|
|
|
|
// both children rendered a frame before the wm composites them
|
|
for d in [a, b] {
|
|
assert!(hal.await_frame_done(d, 1, WAIT));
|
|
}
|
|
let rec = rig.composite(&[tex_a.0, tex_b.0]);
|
|
assert_eq!(rec.deps.len(), 2, "{rec:?}");
|
|
assert_eq!(rig.stats(wm).skipped_draws, 0);
|
|
// ordering: each child frame the wm sampled ran before the wm frame, and no newer one in between
|
|
let log = hal.with_world(|w| w.exec_log.clone());
|
|
for (child, window, frame) in &rec.deps {
|
|
let at = log.iter().position(|r| r.session == *child && r.frame == *frame && r.target == *window).expect("the sampled child frame ran");
|
|
assert!(log[at].seq < rec.seq);
|
|
assert!(!log[at + 1..rec.seq as usize].iter().any(|r| r.session == *child && r.frame > *frame), "a newer child frame ran in between and was not the one sampled");
|
|
}
|
|
|
|
// input: a click forwarded to a reaches a and not b; a answers with a new frame
|
|
let click = pack_forward(&RawEvent::mouse_down(Id::NONE, 10.0, 20.0, 1, 0), &[]);
|
|
rig.w.forward(WIN, a, &click);
|
|
rig.submit();
|
|
rig.fake.wait_saw(a, "MouseDown");
|
|
assert!(hal.await_frame_done(a, 2, WAIT));
|
|
std::thread::sleep(Duration::from_millis(100));
|
|
assert!(!rig.fake.saw(b, "MouseDown"));
|
|
assert_eq!(rig.stats(a).forwarded, 1);
|
|
assert_eq!(rig.stats(b).forwarded, 0);
|
|
let rec = rig.composite(&[tex_a.0, tex_b.0]);
|
|
assert!(rec.deps.contains(&(a, WIN, 2)) && rec.deps.contains(&(b, WIN, 1)), "{rec:?}");
|
|
// a session cannot forward into one that is not its child
|
|
let mut pw = CmdWriter::new(plain);
|
|
pw.forward(WIN, a, &click);
|
|
pw.child_focus(a);
|
|
hal.submit(&pw.buf());
|
|
assert_eq!(rig.stats(plain).denied, 4);
|
|
assert_eq!(rig.stats(a).forwarded, 1);
|
|
|
|
// focus: keyboard / IME / cursor / clipboard requests of a child count only while it has it
|
|
let ask = |hal: &Hal, d: u32| {
|
|
let mut cw = CmdWriter::new(d);
|
|
cw.ime_show(WIN, [0.0; 4], 0);
|
|
cw.cursor(WIN, 1);
|
|
cw.clip_set(b"x");
|
|
hal.submit(&cw.buf());
|
|
};
|
|
ask(&hal, a);
|
|
ask(&hal, b);
|
|
assert_eq!((rig.stats(a).input_applied, rig.stats(a).input_dropped), (0, 3));
|
|
rig.w.child_focus(a);
|
|
rig.submit();
|
|
ask(&hal, a);
|
|
ask(&hal, b);
|
|
assert_eq!((rig.stats(a).input_applied, rig.stats(a).input_dropped), (3, 3));
|
|
assert_eq!((rig.stats(b).input_applied, rig.stats(b).input_dropped), (0, 6));
|
|
rig.w.child_focus(b);
|
|
rig.submit();
|
|
ask(&hal, a);
|
|
ask(&hal, b);
|
|
assert_eq!((rig.stats(a).input_applied, rig.stats(a).input_dropped), (3, 6));
|
|
assert_eq!((rig.stats(b).input_applied, rig.stats(b).input_dropped), (3, 6));
|
|
rig.fake.wait_saw(a, "Focus { window: Id { index: 0, gen: 1 }, focused: true }");
|
|
rig.fake.wait_saw(a, "focused: false");
|
|
|
|
// ChildGeom resizes: a new texture for the window, the old one becomes stale
|
|
rig.w.child_geom(WIN, a, [300.0, 150.0], 2.0);
|
|
rig.submit();
|
|
rig.wait(|e| windows_of(e, a).len() >= 2);
|
|
let resized = *windows_of(&rig.evs, a).last().unwrap();
|
|
assert_eq!((resized.1, resized.2), (600, 300));
|
|
assert_ne!(resized.0, tex_a.0);
|
|
let before = rig.stats(wm);
|
|
let rec = rig.composite(&[tex_a.0, tex_b.0, resized.0]);
|
|
let after = rig.stats(wm);
|
|
assert_eq!((after.stale_lent_draws - before.stale_lent_draws, after.skipped_draws - before.skipped_draws), (1, 1));
|
|
assert_eq!(rec.deps.len(), 2, "the new surface and b's");
|
|
assert_eq!(hal.with_world(|w| w.session(a).unwrap().ledger.entries().filter(|e| e.kind == Kind::Texture).count()), 2, "own texture + resized surface, not both surfaces");
|
|
|
|
// relaunch a: its entries are freed, the wm's references to its surface go stale, b is untouched
|
|
let b_rows = rig.rows(b);
|
|
let b_report = hal.report(b, 0).unwrap().total_bytes;
|
|
rig.fake.hold.store(true, Ordering::SeqCst);
|
|
rig.w.domain_relaunch(a);
|
|
rig.submit();
|
|
rig.wait(|e| e.iter().any(|e| matches!(e, Ev::ChildClosed { child, .. } if *child == a)) && state_of(e, a) == Some(domain_state::COMPILING));
|
|
assert_eq!(rig.rows(a), (0, 0, 0, 0), "everything a owned is gone while the new app has not opened yet");
|
|
assert_eq!(hal.with_world(|w| w.session(a).unwrap().windows.len()), 0);
|
|
assert_eq!(rig.rows(b), b_rows);
|
|
assert_eq!(hal.report(b, 0).unwrap().total_bytes, b_report);
|
|
let before = rig.stats(wm);
|
|
let rec = rig.composite(&[resized.0, tex_b.0]);
|
|
let after = rig.stats(wm);
|
|
assert_eq!((after.stale_lent_draws - before.stale_lent_draws, after.skipped_draws - before.skipped_draws), (1, 1));
|
|
assert_eq!(rec.deps.len(), 1);
|
|
let windows_seen = windows_of(&rig.evs, a).len();
|
|
rig.fake.release();
|
|
rig.wait(|e| windows_of(e, a).len() > windows_seen && state_of(e, a) == Some(domain_state::RUNNING));
|
|
let again = *windows_of(&rig.evs, a).last().unwrap();
|
|
assert_eq!((again.1, again.2), (200, 100));
|
|
assert!(again.0 != resized.0 && again.0 != tex_a.0);
|
|
assert!(hal.await_frame_done(a, 1, WAIT));
|
|
assert_eq!(rig.rows(a), (2, 0, 1, 3), "the new app's own entries, not added to the old ones");
|
|
let rec = rig.composite(&[again.0, tex_b.0]);
|
|
assert_eq!(rec.deps.len(), 2);
|
|
|
|
// stop b by asking it to quit: it quits itself, the wm hears ChildWindowClosed and EXITED
|
|
rig.w.domain_stop(b, domain_stop::QUIT);
|
|
rig.submit();
|
|
rig.wait(|e| state_of(e, b) == Some(domain_state::EXITED) && e.iter().any(|e| matches!(e, Ev::ChildClosed { child, .. } if *child == b)));
|
|
assert!(hal.with_world(|w| w.session(b).is_none()));
|
|
let before = rig.stats(wm);
|
|
rig.composite(&[again.0, tex_b.0]);
|
|
assert_eq!(rig.stats(wm).stale_lent_draws - before.stale_lent_draws, 1);
|
|
|
|
// list: a running, b exited
|
|
rig.w.domain_list(77);
|
|
rig.submit();
|
|
rig.wait(|e| e.iter().any(|e| matches!(e, Ev::Listed { req: 77, .. })));
|
|
let Some(Ev::Listed { list, .. }) = rig.evs.iter().find(|e| matches!(e, Ev::Listed { req: 77, .. })).cloned() else { unreachable!() };
|
|
assert_eq!(list.iter().map(|d| (d.domain, d.state, d.app.as_str())).collect::<Vec<_>>(), [(a, domain_state::RUNNING, "calc"), (b, domain_state::EXITED, "clock")]);
|
|
assert!(list.iter().all(|d| d.parent == wm));
|
|
|
|
// kill a: freed at once, the host is told
|
|
rig.w.domain_stop(a, domain_stop::KILL);
|
|
rig.submit();
|
|
assert!(hal.with_world(|w| w.session(a).is_none()));
|
|
rig.fake.wait_stopped(a, domain_stop::KILL);
|
|
rig.wait(|e| state_of(e, a) == Some(domain_state::EXITED));
|
|
assert_eq!(hal.with_world(|w| w.count(Kind::Texture)), 0, "no surface or alias outlives its domain");
|
|
|
|
// closing the wm leaves only the host and the unrelated session
|
|
rig.fake.halt_all();
|
|
assert!(hal.close(wm));
|
|
assert_eq!(hal.with_world(|w| w.session_ids()), [0, plain]);
|
|
}
|
|
|
|
#[test]
|
|
fn closing_a_parent_closes_its_children_and_asks_the_host_to_stop_them() {
|
|
let mut rig = Rig::new();
|
|
rig.w.str_def(1, b"calc");
|
|
rig.w.domain_start(1, 1, 0);
|
|
rig.submit();
|
|
rig.wait(|e| !e.is_empty() && e.iter().any(|e| matches!(e, Ev::ChildWindow { .. })));
|
|
let child = rig.evs.iter().find_map(|e| if let Ev::State { domain, .. } = e { Some(*domain) } else { None }).unwrap();
|
|
assert!(rig.hal.close(rig.wm));
|
|
assert!(rig.hal.with_world(|w| w.session(child).is_none()));
|
|
rig.fake.wait_stopped(child, domain_stop::KILL);
|
|
rig.fake.halt_all();
|
|
assert_eq!(rig.hal.with_world(|w| w.count(Kind::Texture) + w.count(Kind::Window)), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_start_without_a_host_or_a_failing_host_reports_a_crash() {
|
|
let hal = Hal::new();
|
|
let wm = hal.open_domain(&open_req(1, 0), Caps { privileged: true }).session;
|
|
let mut store = EventStore::new(wm, 64, 4096);
|
|
let mut evs = Vec::new();
|
|
let mut w = CmdWriter::new(wm);
|
|
w.str_def(1, b"calc");
|
|
w.domain_start(3, 1, 0);
|
|
hal.submit(&w.buf());
|
|
wait_for(&hal, &mut store, wm, &mut evs, |e| e.iter().any(|e| matches!(e, Ev::State { state: domain_state::CRASHED, req: 3, .. })));
|
|
let msg = evs.iter().find_map(|e| if let Ev::State { state: domain_state::CRASHED, msg, .. } = e { Some(msg.clone()) } else { None }).unwrap();
|
|
assert!(msg.contains("no domain host"), "{msg}");
|
|
// the crashed domain's session is freed, its record stays for DomainList
|
|
assert_eq!(hal.with_world(|w| w.session_ids()), [0, wm]);
|
|
w.clear();
|
|
w.domain_list(4);
|
|
hal.submit(&w.buf());
|
|
wait_for(&hal, &mut store, wm, &mut evs, |e| e.iter().any(|e| matches!(e, Ev::Listed { req: 4, .. })));
|
|
let Some(Ev::Listed { list, .. }) = evs.iter().find(|e| matches!(e, Ev::Listed { .. })) else { unreachable!() };
|
|
assert_eq!(list.len(), 1);
|
|
assert_eq!(list[0].state, domain_state::CRASHED);
|
|
}
|
|
|
|
#[test]
|
|
fn sessions_pump_and_wake_independently_from_their_own_threads() {
|
|
let hal = Hal::new();
|
|
let a = hal.open(&open_req(1, 0)).session;
|
|
let b = hal.open(&open_req(2, 0)).session;
|
|
let run = |sid: u32, hal: Arc<Hal>| {
|
|
std::thread::spawn(move || {
|
|
let mut store = EventStore::new(sid, 16, 256);
|
|
let mut buf = store.buf();
|
|
hal.pump(0, &mut buf); // Startup
|
|
let t0 = Instant::now();
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(2_000_000_000, &mut buf);
|
|
let evs: Vec<String> = store.reader(n, buf.arena_len).map(|(_, e)| format!("{e:?}")).collect();
|
|
(t0.elapsed(), evs)
|
|
})
|
|
};
|
|
let (ta, tb) = (run(a, hal.clone()), run(b, hal.clone()));
|
|
std::thread::sleep(Duration::from_millis(150));
|
|
hal.wake_session(a);
|
|
let (took_a, evs_a) = ta.join().unwrap();
|
|
let (took_b, evs_b) = tb.join().unwrap();
|
|
assert_eq!(evs_a.len(), 1);
|
|
assert!(evs_a[0].starts_with("Wake"));
|
|
assert!(took_a < Duration::from_millis(1500), "{took_a:?}");
|
|
assert!(evs_b.is_empty() && took_b >= Duration::from_millis(1800), "b stays blocked: {evs_b:?} {took_b:?}");
|
|
// the global wake reaches both, and a wake before a pump is not lost
|
|
hal.wake();
|
|
for sid in [a, b] {
|
|
let mut store = EventStore::new(sid, 16, 256);
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(5_000_000_000, &mut buf);
|
|
assert_eq!(n, 1);
|
|
assert!(matches!(store.reader(n, buf.arena_len).next().unwrap().1, HalEvent::Wake {}));
|
|
}
|
|
// commands of one session are executed on whichever thread submits them
|
|
let hal2 = hal.clone();
|
|
std::thread::spawn(move || {
|
|
let mut w = CmdWriter::new(b);
|
|
w.texture_create(Id::new(1, 1), usage::SAMPLED, format::RGBA8, 4, 4, [1, 1], Attr::default());
|
|
hal2.submit(&w.buf());
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
assert_eq!(hal.with_world(|w| w.session(b).unwrap().textures.len()), 1);
|
|
}
|
|
|
|
/// Turns of `sid`'s pump so far.
|
|
fn turns(hal: &Hal, sid: u32) -> PumpCounts {
|
|
hal.pump_counts().into_iter().find(|(id, _)| *id == sid).map(|(_, c)| c).unwrap_or_default()
|
|
}
|
|
|
|
#[test]
|
|
fn an_idle_wm_with_domains_takes_no_pump_turns() {
|
|
// a WM and three child domains, each pumping from its own thread with nothing to do
|
|
let hal = Hal::new();
|
|
let wm = hal.open(&open_req(201, 0)).session;
|
|
let mut sids = vec![wm];
|
|
for key in 202..205 {
|
|
sids.push(hal.with_world(|w| w.open_session(key, wm, false, 1.0)));
|
|
}
|
|
for sid in &sids {
|
|
let mut store = EventStore::new(*sid, 16, 256);
|
|
let mut buf = store.buf();
|
|
hal.pump(0, &mut buf); // Startup
|
|
}
|
|
let before: Vec<PumpCounts> = sids.iter().map(|sid| turns(&hal, *sid)).collect();
|
|
let idle = Duration::from_millis(400);
|
|
let threads: Vec<_> = sids.iter().map(|sid| {
|
|
let (hal, sid) = (hal.clone(), *sid);
|
|
std::thread::spawn(move || {
|
|
let mut store = EventStore::new(sid, 16, 256);
|
|
let mut buf = store.buf();
|
|
hal.pump(idle.as_nanos() as u64, &mut buf)
|
|
})
|
|
}).collect();
|
|
for t in threads {
|
|
assert_eq!(t.join().unwrap(), 0, "nothing was due");
|
|
}
|
|
for (sid, b) in sids.iter().zip(before) {
|
|
let a = turns(&hal, *sid);
|
|
// one turn finds nothing, the wait ends at the deadline, the next turn returns
|
|
assert_eq!(a.calls - b.calls, 1);
|
|
assert!(a.turns - b.turns <= 2, "session {sid} turned {} times while idle: {a:?}", a.turns - b.turns);
|
|
assert_eq!((a.by_event - b.by_event, a.by_all - b.by_all, a.by_wake - b.by_wake), (0, 0, 0), "session {sid}: {a:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_idle_session_takes_no_turn_for_two_seconds() {
|
|
// a WM and two domains blocked in their pumps (Cx's idle loop: no timeout of its own)
|
|
let hal = Hal::new();
|
|
let wm = hal.open(&open_req(221, 0)).session;
|
|
let sids = [wm, hal.with_world(|w| w.open_session(222, wm, false, 1.0)), hal.with_world(|w| w.open_session(223, wm, false, 1.0))];
|
|
for sid in sids {
|
|
let mut store = EventStore::new(sid, 16, 256);
|
|
let mut buf = store.buf();
|
|
hal.pump(0, &mut buf); // Startup
|
|
}
|
|
let threads: Vec<_> = sids.iter().map(|sid| {
|
|
let (hal, sid) = (hal.clone(), *sid);
|
|
std::thread::spawn(move || {
|
|
let mut store = EventStore::new(sid, 16, 256);
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(u64::MAX, &mut buf);
|
|
store.reader(n, buf.arena_len).map(|(_, e)| format!("{e:?}")).collect::<Vec<_>>()
|
|
})
|
|
}).collect();
|
|
std::thread::sleep(Duration::from_millis(100));
|
|
let blocked: Vec<PumpCounts> = sids.iter().map(|sid| turns(&hal, *sid)).collect();
|
|
std::thread::sleep(Duration::from_secs(2));
|
|
for (sid, b) in sids.iter().zip(&blocked) {
|
|
assert_eq!(turns(&hal, *sid), *b, "session {sid} turned while idle");
|
|
}
|
|
// each one's own wake ends its wait, and only its
|
|
for (i, sid) in sids.iter().enumerate() {
|
|
hal.wake_session(*sid);
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
for (j, other) in sids.iter().enumerate().skip(i + 1) {
|
|
assert_eq!(turns(&hal, *other).turns, blocked[j].turns, "session {other} turned for {sid}'s wake");
|
|
}
|
|
}
|
|
for t in threads {
|
|
let evs = t.join().unwrap();
|
|
assert!(evs.len() == 1 && evs[0].starts_with("Wake"), "{evs:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn news_for_one_session_does_not_turn_another_sessions_pump() {
|
|
let hal = Hal::new();
|
|
let wm = hal.open(&open_req(211, 0)).session;
|
|
let busy = hal.with_world(|w| w.open_session(212, wm, false, 1.0));
|
|
let quiet = hal.with_world(|w| w.open_session(213, wm, false, 1.0));
|
|
for sid in [wm, busy, quiet] {
|
|
let mut store = EventStore::new(sid, 16, 256);
|
|
let mut buf = store.buf();
|
|
hal.pump(0, &mut buf); // Startup
|
|
}
|
|
let mut w = CmdWriter::new(busy);
|
|
w.window_create(WIN, window_kind::NORMAL, Id::NONE, [0.0, 0.0, 200.0, 100.0], false);
|
|
hal.submit(&w.buf());
|
|
let before = turns(&hal, quiet);
|
|
let blocked = {
|
|
let hal = hal.clone();
|
|
std::thread::spawn(move || {
|
|
let mut store = EventStore::new(quiet, 64, 4096);
|
|
let mut buf = store.buf();
|
|
let t0 = Instant::now();
|
|
let n = hal.pump(600_000_000, &mut buf);
|
|
(n, t0.elapsed())
|
|
})
|
|
};
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
// the busy child presents (its own FrameDone, the WM's ChildFrame), gets input forwarded
|
|
// and injected; the WM submits
|
|
for frame in 1..=20u64 {
|
|
let mut w = CmdWriter::new(busy);
|
|
w.frame_begin(frame, FrameMeta::default());
|
|
w.pass(Pass::default());
|
|
w.frame_end(WIN, target::WINDOW);
|
|
hal.submit(&w.buf());
|
|
hal.inject(busy, RawEvent::default());
|
|
hal.submit(&CmdWriter::new(wm).buf());
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
let (n, took) = blocked.join().unwrap();
|
|
assert_eq!(n, 0, "the quiet session had nothing");
|
|
assert!(took >= Duration::from_millis(550), "{took:?}");
|
|
let after = turns(&hal, quiet);
|
|
assert!(after.turns - before.turns <= 2, "the quiet pump turned for others' news: {before:?} -> {after:?}");
|
|
assert_eq!(after.by_event - before.by_event, 0);
|
|
// and the busy sessions did get theirs
|
|
let mut store = EventStore::new(wm, 256, 65536);
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(0, &mut buf);
|
|
assert!(store.reader(n, buf.arena_len).any(|(_, e)| matches!(e, HalEvent::ChildFrame { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn forward_and_domain_payloads_round_trip() {
|
|
let raw = RawEvent::scroll(Id::new(1, 2), 1.5, 2.5, -3.0, 4.0, 3, true, 2);
|
|
let packed = pack_forward(&raw, b"payload");
|
|
let (back, bytes) = unpack_forward(&packed).unwrap();
|
|
assert_eq!(bytes, b"payload");
|
|
assert_eq!((back.tag, back.a, back.b, back.time_ns), (raw.tag, raw.a, raw.b, raw.time_ns));
|
|
assert!(unpack_forward(&packed[..10]).is_none());
|
|
let list = vec![
|
|
DomainInfo { domain: 3, parent: 1, state: domain_state::RUNNING, phase: 2, pct: 0.5, app: "calc".into() },
|
|
DomainInfo { domain: 4, parent: 1, state: domain_state::CRASHED, phase: 0, pct: 0.0, app: String::new() },
|
|
];
|
|
assert_eq!(read_domains(&pack_domains(&list), 2), list);
|
|
assert_eq!(read_domains(&pack_domains(&list)[..30], 2), list[..1], "a cut record ends the list");
|
|
}
|
|
|
|
#[test]
|
|
fn hal_open_takes_the_launch_the_host_gave_the_thread() {
|
|
// the global HAL, through the op the apps call
|
|
struct Host(Mutex<Vec<JoinHandle<u32>>>);
|
|
impl DomainHost for Host {
|
|
fn start(&self, launch: DomainLaunch) -> Result<(), String> {
|
|
let t = std::thread::spawn(move || with_launch(&launch, || hal_open(&open_req(0, 0)).session));
|
|
self.0.lock().unwrap().push(t);
|
|
Ok(())
|
|
}
|
|
fn stop(&self, _: u32, _: u32) {}
|
|
fn relaunch(&self, _: DomainLaunch) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
}
|
|
let hal = Hal::global();
|
|
let host = Arc::new(Host(Mutex::new(Vec::new())));
|
|
hal.set_domain_host(host.clone());
|
|
let wm = hal.open_domain(&open_req(0x77, 0), Caps { privileged: true }).session;
|
|
let mut w = CmdWriter::new(wm);
|
|
w.str_def(1, b"calc");
|
|
w.domain_start(1, 1, 0);
|
|
hal.submit(&w.buf());
|
|
let opened = host.0.lock().unwrap().pop().unwrap().join().unwrap();
|
|
assert_ne!(opened, 0);
|
|
assert_eq!(hal.with_world(|w| w.session(opened).map(|s| (s.parent, s.privileged, s.attached))), Some((wm, false, true)));
|
|
// outside a launch the same request is a parent claim without the key: refused
|
|
assert_eq!(hal_open(&open_req(5, wm)).session, 0);
|
|
// and an app cannot open itself privileged: the op takes no capabilities
|
|
assert!(!hal.with_world(|w| w.session(opened).unwrap().privileged));
|
|
assert!(hal.close(wm));
|
|
}
|
|
|
|
#[test]
|
|
fn child_frames_notify_the_parent_and_coalesce_per_window() {
|
|
let hal = Hal::new();
|
|
let parent = hal.open(&open_req(101, 0)).session;
|
|
let child = hal.with_world(|w| w.open_session(102, parent, false, 1.0));
|
|
let other = hal.with_world(|w| w.open_session(103, parent, false, 1.0));
|
|
let plain = hal.open(&open_req(104, 0)).session;
|
|
for sid in [child, other, plain] {
|
|
let mut w = CmdWriter::new(sid);
|
|
w.window_create(WIN, window_kind::NORMAL, Id::NONE, [0.0, 0.0, 200.0, 100.0], false);
|
|
// Deliberately reuse the window id in the texture namespace.
|
|
w.texture_create(WIN, usage::RENDER_TARGET, format::RGBA8, 8, 8, [1, 1], Attr::default());
|
|
hal.submit(&w.buf());
|
|
}
|
|
let texture = hal.with_world(|w| w.session(child).unwrap().windows.get(child, WIN).unwrap().val.embedded.unwrap().alias);
|
|
let other_texture = hal.with_world(|w| w.session(other).unwrap().windows.get(other, WIN).unwrap().val.embedded.unwrap().alias);
|
|
let mut store = EventStore::new(parent, 64, 4096);
|
|
let pump = |store: &mut EventStore| {
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(0, &mut buf);
|
|
store.reader(n, buf.arena_len).filter_map(|(_, e)| match e {
|
|
HalEvent::ChildFrame { child, window, texture, frame } => Some((child, window, texture, frame)),
|
|
_ => None,
|
|
}).collect::<Vec<_>>()
|
|
};
|
|
assert!(pump(&mut store).is_empty());
|
|
let present = |sid, frame, kind| {
|
|
let mut w = CmdWriter::new(sid);
|
|
w.frame_begin(frame, FrameMeta::default());
|
|
w.pass(Pass::default());
|
|
w.frame_end(WIN, kind);
|
|
hal.submit(&w.buf());
|
|
};
|
|
present(child, 1, target::WINDOW);
|
|
assert_eq!(pump(&mut store), [(child, WIN, texture, 1)]);
|
|
let mut child_store = EventStore::new(child, 64, 4096);
|
|
let mut buf = child_store.buf();
|
|
let n = hal.pump(0, &mut buf);
|
|
assert_eq!(child_store.reader(n, buf.arena_len).filter(|(_, e)| matches!(e, HalEvent::FrameDone { window: WIN, frame: 1, .. })).count(), 1);
|
|
present(child, 2, target::WINDOW);
|
|
present(other, 1, target::WINDOW);
|
|
present(child, 3, target::WINDOW);
|
|
assert_eq!(pump(&mut store), [(other, WIN, other_texture, 1), (child, WIN, texture, 3)]);
|
|
assert!(pump(&mut store).is_empty());
|
|
present(plain, 1, target::WINDOW);
|
|
present(child, 4, target::TEXTURE);
|
|
assert!(pump(&mut store).is_empty(), "neither an ordinary window nor a texture target notifies the parent");
|
|
present(child, 5, target::WINDOW);
|
|
hal.with_world(|w| assert!(w.resize_embedded(child, WIN, [300.0, 150.0], 1.0, 0)));
|
|
present(child, 6, target::WINDOW);
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(0, &mut buf);
|
|
let events: Vec<_> = store.reader(n, buf.arena_len).map(|(_, e)| e).collect();
|
|
assert_eq!(events.len(), 2, "the old frame was replaced after the resize announcement");
|
|
let HalEvent::ChildWindow { texture: resized, .. } = events[0] else { panic!("resize must precede the frame") };
|
|
assert_ne!(resized, texture);
|
|
assert_eq!(events[1], HalEvent::ChildFrame { child, window: WIN, texture: resized, frame: 6 });
|
|
assert!(hal.close(parent));
|
|
assert!(hal.close(plain));
|
|
}
|
|
|
|
#[test]
|
|
fn child_scale_follows_the_parent_window_and_child_geom() {
|
|
let hal = Hal::new();
|
|
let parent = hal.open_domain(&open_req(201, 0), Caps { privileged: true }).session;
|
|
let mut pw = CmdWriter::new(parent);
|
|
pw.window_create(WIN, window_kind::NORMAL, Id::NONE, [0.0, 0.0, 800.0, 600.0], false);
|
|
hal.submit(&pw.buf());
|
|
hal.with_world(|w| {
|
|
w.window_geom(parent, WIN, [0.0, 0.0, 800.0, 600.0], 2.0, window_state::RESTORE);
|
|
// A session default must not select the scale of an already placed window.
|
|
w.session_mut(parent).unwrap().dpi = 1.0;
|
|
});
|
|
pw.clear();
|
|
pw.window_create(Id::new(1, 1), window_kind::NORMAL, Id::NONE, [0.0, 0.0, 200.0, 100.0], false);
|
|
hal.submit(&pw.buf());
|
|
hal.with_world(|w| w.window_geom(parent, Id::new(1, 1), [0.0, 0.0, 200.0, 100.0], 1.0, window_state::RESTORE));
|
|
let launch = hal.with_world(|w| w.start_domain(parent, 1, "child".into(), String::new()));
|
|
assert_eq!(hal.with_world(|w| w.session(launch.domain).unwrap().dpi), 2.0);
|
|
let mut req = open_req(launch.app_key, parent);
|
|
req.dpi_hint = 1.0; // Cx's default hint must not override the compositor.
|
|
let info = hal.open_domain(&req, Caps { privileged: false });
|
|
assert_eq!(info.session, launch.domain);
|
|
assert_eq!(info.dpi, 2.0);
|
|
let child = info.session;
|
|
let mut cw = CmdWriter::new(child);
|
|
cw.window_create(WIN, window_kind::NORMAL, Id::NONE, [0.0, 0.0, 300.0, 150.0], false);
|
|
hal.submit(&cw.buf());
|
|
let check_surface = |dpi, pixels| hal.with_world(|w| {
|
|
let s = w.session(child).unwrap();
|
|
assert_eq!(s.dpi, dpi);
|
|
let em = s.windows.get(child, WIN).unwrap().val.embedded.unwrap();
|
|
assert_eq!(em.dpi, dpi);
|
|
let t = &s.textures.get(child, em.tex).unwrap().val;
|
|
assert_eq!((t.w, t.h), pixels);
|
|
});
|
|
let check_events = |dpi, pixels| {
|
|
let mut child_store = EventStore::new(child, 64, 4096);
|
|
let mut buf = child_store.buf();
|
|
let n = hal.pump(0, &mut buf);
|
|
let mut display = false;
|
|
let mut geom = false;
|
|
for (_, e) in child_store.reader(n, buf.arena_len) {
|
|
match e {
|
|
HalEvent::Display { window, dpi: d, .. } if window == WIN => { assert_eq!(d, dpi); display = true; }
|
|
HalEvent::Geom { window, dpi: d, w, h, .. } if window == WIN => {
|
|
assert!(display, "Display must precede geometry");
|
|
assert_eq!((d, w, h), (dpi, 300.0, 150.0));
|
|
geom = true;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
assert!(display && geom);
|
|
let mut parent_store = EventStore::new(parent, 64, 4096);
|
|
let mut buf = parent_store.buf();
|
|
let n = hal.pump(0, &mut buf);
|
|
assert!(parent_store.reader(n, buf.arena_len).any(|(_, e)| matches!(e,
|
|
HalEvent::ChildWindow { child: c, dpi: d, w, h, .. }
|
|
if c == child && d == dpi && (w, h) == pixels)));
|
|
};
|
|
check_surface(2.0, (600, 300));
|
|
check_events(2.0, (600, 300));
|
|
pw.clear();
|
|
pw.child_geom(WIN, child, [300.0, 150.0], 1.0);
|
|
hal.submit(&pw.buf());
|
|
check_surface(1.0, (300, 150));
|
|
check_events(1.0, (300, 150));
|
|
assert!(hal.close(parent));
|
|
}
|